1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 09:08:32 +00:00

Resolve "Created by field type"

This commit is contained in:
Davide Silvestri 2023-12-11 10:02:00 +00:00
parent df49e52393
commit aa9a793dbe
37 changed files with 1368 additions and 42 deletions

View file

@ -21,6 +21,7 @@ markers =
field_boolean: All tests related to boolean field
field_date: All tests related to date field
field_created_on: All tests related to created on field
field_created_by: All tests related to created by field
field_last_modified: All tests related to last modified field
field_email: All tests related to email field
field_phone_number: All tests related to phone number field

View file

@ -108,6 +108,8 @@ class DatabaseApplicationType(ApplicationType):
model = table.get_model(fields=fields, add_dependencies=False)
serialized_rows = []
row_queryset = model.objects.all()
if table.created_by_column_added:
row_queryset = row_queryset.select_related("created_by")
if table.last_modified_by_column_added:
row_queryset = row_queryset.select_related("last_modified_by")
for row in row_queryset:
@ -116,6 +118,7 @@ class DatabaseApplicationType(ApplicationType):
order=str(row.order),
created_on=row.created_on.isoformat(),
updated_on=row.updated_on.isoformat(),
created_by=getattr(row, "created_by", None),
last_modified_by=getattr(row, "last_modified_by", None),
)
for field_object in model._field_objects.values():
@ -474,16 +477,27 @@ class DatabaseApplicationType(ApplicationType):
else:
updated_on = timezone.now()
modified_by_email = serialized_row.get("last_modified_by", None)
created_by_email = serialized_row.get("created_by", None)
created_by = (
user_email_mapping.get(created_by_email, None)
if created_by_email
else None
)
last_modified_by_email = serialized_row.get("last_modified_by", None)
last_modified_by = (
user_email_mapping.get(last_modified_by_email, None)
if last_modified_by_email
else None
)
row_instance = table_model(
id=serialized_row["id"],
order=serialized_row["order"],
created_on=created_on,
updated_on=updated_on,
last_modified_by=user_email_mapping.get(modified_by_email, None)
if modified_by_email
else None,
created_by=created_by,
last_modified_by=last_modified_by,
)
for serialized_field in serialized_table["fields"]:

View file

@ -200,6 +200,7 @@ class DatabaseConfig(AppConfig):
from .fields.field_types import (
BooleanFieldType,
CountFieldType,
CreatedByFieldType,
CreatedOnFieldType,
DateFieldType,
EmailFieldType,
@ -233,6 +234,7 @@ class DatabaseConfig(AppConfig):
field_type_registry.register(LastModifiedFieldType())
field_type_registry.register(LastModifiedByFieldType())
field_type_registry.register(CreatedOnFieldType())
field_type_registry.register(CreatedByFieldType())
field_type_registry.register(LinkRowFieldType())
field_type_registry.register(FileFieldType())
field_type_registry.register(SingleSelectFieldType())

View file

@ -15,9 +15,12 @@ class DatabaseExportSerializedStructure:
}
@staticmethod
def row(id, order, created_on, updated_on, last_modified_by=None):
def row(id, order, created_on, updated_on, created_by=None, last_modified_by=None):
optional = {}
if created_by:
optional["created_by"] = created_by.email
if last_modified_by:
optional["last_modified_by"] = last_modified_by.email

View file

@ -131,6 +131,11 @@ def construct_all_possible_field_kwargs(
"name": "last_modified_by",
}
],
"created_by": [
{
"name": "created_by",
}
],
"link_row": [
{"name": "link_row", "link_row_table": link_table},
{

View file

@ -65,8 +65,6 @@ from baserow.contrib.database.api.fields.serializers import (
)
from baserow.contrib.database.db.functions import RandomUUID
from baserow.contrib.database.export_serialized import DatabaseExportSerializedStructure
from baserow.contrib.database.fields.field_cache import FieldCache
from baserow.contrib.database.fields.fields import SyncedLastModifiedByForeignKeyField
from baserow.contrib.database.formula import (
BASEROW_FORMULA_TYPE_ALLOWED_FIELDS,
BaserowExpression,
@ -131,6 +129,7 @@ from .exceptions import (
SelfReferencingLinkRowCannotHaveRelatedField,
)
from .expressions import extract_jsonb_array_values_to_single_string
from .field_cache import FieldCache
from .field_filters import (
AnnotatedQ,
contains_filter,
@ -143,12 +142,14 @@ from .fields import (
BaserowLastModifiedField,
MultipleSelectManyToManyField,
SingleSelectForeignKey,
SyncedUserForeignKeyField,
)
from .handler import FieldHandler
from .models import (
AbstractSelectOption,
BooleanField,
CountField,
CreatedByField,
CreatedOnField,
DateField,
EmailField,
@ -1246,7 +1247,7 @@ class LastModifiedByFieldType(ReadOnlyFieldType):
kwargs["null"] = True
kwargs["blank"] = True
kwargs.update(self.model_field_kwargs)
return SyncedLastModifiedByForeignKeyField(
return SyncedUserForeignKeyField(
User,
on_delete=models.SET_NULL,
related_name="+",
@ -1268,7 +1269,9 @@ class LastModifiedByFieldType(ReadOnlyFieldType):
if not table.last_modified_by_column_added:
table_to_update = TableHandler().get_table_for_update(table.id)
TableHandler().create_last_modified_by_field(table_to_update)
TableHandler().create_created_by_and_last_modified_by_fields(
table_to_update
)
table.refresh_from_db()
def after_create(self, field, model, user, connection, before, field_kwargs):
@ -1437,6 +1440,212 @@ class LastModifiedByFieldType(ReadOnlyFieldType):
)
class CreatedByFieldType(ReadOnlyFieldType):
type = "created_by"
model_class = CreatedByField
can_be_in_form_view = False
keep_data_on_duplication = True
source_field_name = "created_by"
model_field_kwargs = {"sync_with_add": "created_by"}
def get_model_field(self, instance, **kwargs):
kwargs["null"] = True
kwargs["blank"] = True
kwargs.update(self.model_field_kwargs)
return SyncedUserForeignKeyField(
User,
on_delete=models.SET_NULL,
related_name="+",
related_query_name="+",
db_constraint=False,
**kwargs,
)
def get_serializer_field(self, instance, **kwargs):
return CollaboratorSerializer(required=False, **kwargs)
def before_create(
self, table, primary, allowed_field_values, order, user, field_kwargs
):
"""
If created_by column is still not present on the table,
we need to create it first.
"""
if not table.created_by_column_added:
table_to_update = TableHandler().get_table_for_update(table.id)
TableHandler().create_created_by_and_last_modified_by_fields(
table_to_update
)
table.refresh_from_db()
def after_create(self, field, model, user, connection, before, field_kwargs):
"""
Immediately after the field has been created, we need to populate the values
with the already existing source_field_name column.
"""
model.objects.all().update(
**{f"{field.db_column}": models.F(self.source_field_name)}
)
def after_update(
self,
from_field,
to_field,
from_model,
to_model,
user,
connection,
altered_column,
before,
to_field_kwargs,
):
"""
If the field type has changed, we need to update the values from
the source_field_name column.
"""
if not isinstance(from_field, self.model_class):
to_model.objects.all().update(
**{f"{to_field.db_column}": models.F(self.source_field_name)}
)
def enhance_queryset(self, queryset, field, name):
return queryset.select_related(name)
def should_backup_field_data_for_same_type_update(
self, old_field, new_field_attrs: Dict[str, Any]
) -> bool:
return False
def random_value(self, instance, fake, cache):
return None
def get_export_serialized_value(
self,
row: "GeneratedTableModel",
field_name: str,
cache: Dict[str, Any],
files_zip: Optional[ZipFile] = None,
storage: Optional[Storage] = None,
) -> Any:
"""
Exported value will be the user's email address.
"""
user = self.get_internal_value_from_db(row, field_name)
return user.email if user else None
def set_import_serialized_value(
self,
row: "GeneratedTableModel",
field_name: str,
value: Any,
id_mapping: Dict[str, Any],
cache: Dict[str, Any],
files_zip: Optional[ZipFile] = None,
storage: Optional[Storage] = None,
):
"""
Importing will use the value from source_field_name column.
"""
value = getattr(row, self.source_field_name)
setattr(row, field_name, value)
def get_internal_value_from_db(
self, row: "GeneratedTableModel", field_name: str
) -> Any:
return getattr(row, field_name)
def get_export_value(
self, value: Any, field_object: "FieldObject", rich_value: bool = False
) -> Any:
"""
Exported value will be the user's email address.
"""
user = value
return user.email if user else None
def get_order(
self, field, field_name, order_direction
) -> OptionallyAnnotatedOrderBy:
"""
If the user wants to sort the results they expect them to be ordered
alphabetically based on the user's name.
"""
name = f"{field_name}__first_name"
order = collate_expression(F(name))
if order_direction == "ASC":
order = order.asc(nulls_first=True)
else:
order = order.desc(nulls_last=True)
return OptionallyAnnotatedOrderBy(order=order)
def get_value_for_filter(self, row: "GeneratedTableModel", field: Field) -> any:
value = getattr(row, field.db_column)
return value
def get_search_expression(self, field: Field, queryset: QuerySet) -> Expression:
return Subquery(
queryset.filter(pk=OuterRef("pk")).values(f"{field.db_column}__first_name")[
:1
]
)
def is_searchable(self, field: Field) -> bool:
return True
def contains_query(self, field_name, value, model_field, field):
value = value.strip()
if value == "":
return Q()
return Q(**{f"{field_name}__first_name__icontains": value})
def get_alter_column_prepare_new_value(self, connection, from_field, to_field):
"""
When converting to created by field type we won't preserve any
values.
"""
# fmt: off
sql = (
f"""
p_in = NULL;
""" # nosec b608
)
# fmt: on
return sql, {}
def get_alter_column_prepare_old_value(self, connection, from_field, to_field):
"""
When converting to another field type we won't preserve any values.
"""
to_field_type = field_type_registry.get_by_model(to_field)
if to_field_type.type != self.type and connection.vendor == "postgresql":
with connection.cursor() as cursor:
cursor.execute("SET CONSTRAINTS ALL IMMEDIATE")
# fmt: off
sql = (
f"""
p_in = NULL;
""" # nosec b608
)
# fmt: on
return sql, {}
return super().get_alter_column_prepare_old_value(
connection, from_field, to_field
)
class LinkRowFieldType(FieldType):
"""
The link row field can be used to link a field to a row of another table. Because

View file

@ -318,10 +318,9 @@ class DurationFieldUsingPostgresFormatting(models.DurationField):
return sql + "::text", params
class SyncedLastModifiedByForeignKeyField(IgnoreMissingForeignKey):
requires_refresh_after_update = True
class SyncedUserForeignKeyField(IgnoreMissingForeignKey):
def __init__(self, to, sync_with=None, sync_with_add=None, *args, **kwargs):
self.requires_refresh_after_update = sync_with is not None
self.sync_with = sync_with
self.sync_with_add = sync_with_add
if sync_with or sync_with_add:

View file

@ -342,6 +342,10 @@ class CreatedOnField(Field, BaseDateMixin):
pass
class CreatedByField(Field):
pass
class LinkRowField(Field):
THROUGH_DATABASE_TABLE_PREFIX = LINK_ROW_THROUGH_TABLE_PREFIX
link_row_table = models.ForeignKey(

View file

@ -0,0 +1,43 @@
# Generated by Django 3.2.21 on 2023-12-01 13:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("database", "0142_add_multiple_select_to_formulafield"),
]
operations = [
migrations.CreateModel(
name="CreatedByField",
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",
),
),
],
options={
"abstract": False,
},
bases=("database.field",),
),
migrations.AddField(
model_name="table",
name="created_by_column_added",
field=models.BooleanField(
default=False,
null=True,
help_text="Indicates whether the table has had the created_by column added.",
),
preserve_default=False,
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 3.2.21 on 2023-12-01 13:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("database", "0143_add_table_created_by_column_added"),
]
operations = [
migrations.AlterField(
model_name="table",
name="created_by_column_added",
field=models.BooleanField(
default=True,
help_text="Indicates whether the table has had the created_by column added.",
null=True,
),
),
]

View file

@ -59,6 +59,7 @@ from baserow.core.utils import Progress, get_non_unique_values, grouper
from ..search.handler import SearchHandler
from ..table.constants import (
CREATED_BY_COLUMN_NAME,
LAST_MODIFIED_BY_COLUMN_NAME,
ROW_NEEDS_BACKGROUND_UPDATE_COLUMN_NAME,
)
@ -768,10 +769,15 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
prepared_values, model
)
row_values["order"] = self.get_unique_orders_before_row(before, model)[0]
if getattr(model, CREATED_BY_COLUMN_NAME, None):
row_values[CREATED_BY_COLUMN_NAME] = user if user and user.id else None
if getattr(model, LAST_MODIFIED_BY_COLUMN_NAME, None):
row_values[LAST_MODIFIED_BY_COLUMN_NAME] = (
user if user and user.id else None
)
instance = model.objects.create(**row_values)
rows_created_counter.add(1)
@ -1100,8 +1106,13 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
):
row_values, manytomany_values = self.extract_manytomany_values(row, model)
row_values["order"] = unique_orders[index]
if getattr(model, CREATED_BY_COLUMN_NAME, None):
row_values[CREATED_BY_COLUMN_NAME] = user if user.id else None
if getattr(model, LAST_MODIFIED_BY_COLUMN_NAME, None):
row_values[LAST_MODIFIED_BY_COLUMN_NAME] = user if user.id else None
instance = model(**row_values)
relations = {

View file

@ -19,3 +19,4 @@ ROW_NEEDS_BACKGROUND_UPDATE_COLUMN_NAME = "needs_background_update"
TSV_FIELD_PREFIX = "tsv_field"
LAST_MODIFIED_BY_COLUMN_NAME = "last_modified_by"
CREATED_BY_COLUMN_NAME = "created_by"

View file

@ -39,6 +39,7 @@ from baserow.core.trash.handler import TrashHandler
from baserow.core.utils import ChildProgressBuilder, Progress, find_unused_name
from .constants import (
CREATED_BY_COLUMN_NAME,
LAST_MODIFIED_BY_COLUMN_NAME,
ROW_NEEDS_BACKGROUND_UPDATE_COLUMN_NAME,
TABLE_CREATION,
@ -738,22 +739,35 @@ class TableHandler(metaclass=baserow_trace_methods(tracer)):
table.save(update_fields=("needs_background_update_column_added",))
def create_last_modified_by_field(self, table: "Table") -> None:
def create_created_by_and_last_modified_by_fields(self, table: "Table") -> None:
"""
Creates last_modified_by field for the provided table if
it has not yet been created.
Creates the created_by and last_modified_by fields for the provided
table if they have not yet been created.
:param table: Table that should have last_modified_by field.
:param table: Table that should have created_by and last_modified_by
fields.
"""
if table.last_modified_by_column_added:
last_modified_by_column_added = table.last_modified_by_column_added
created_by_column_added = table.created_by_column_added
if last_modified_by_column_added and created_by_column_added:
return
table.last_modified_by_column_added = True
model = table.get_model(use_cache=False)
table.created_by_column_added = True
model = table.get_model(use_cache=False, field_ids=[])
with safe_django_schema_editor(atomic=False) as schema_editor:
last_modified_by_field = model._meta.get_field(LAST_MODIFIED_BY_COLUMN_NAME)
schema_editor.add_field(model, last_modified_by_field)
if not last_modified_by_column_added:
last_modified_by_field = model._meta.get_field(
LAST_MODIFIED_BY_COLUMN_NAME
)
schema_editor.add_field(model, last_modified_by_field)
table.save(update_fields=["last_modified_by_column_added"])
if not created_by_column_added:
created_by_field = model._meta.get_field(CREATED_BY_COLUMN_NAME)
schema_editor.add_field(model, created_by_field)
table.save(
update_fields=["created_by_column_added", "last_modified_by_column_added"]
)

View file

@ -41,6 +41,7 @@ from baserow.contrib.database.table.cache import (
set_cached_model_field_attrs,
)
from baserow.contrib.database.table.constants import (
CREATED_BY_COLUMN_NAME,
LAST_MODIFIED_BY_COLUMN_NAME,
ROW_NEEDS_BACKGROUND_UPDATE_COLUMN_NAME,
TSV_FIELD_PREFIX,
@ -761,6 +762,11 @@ class Table(
help_text="Indicates whether the table has had the last_modified_by "
"column added.",
)
created_by_column_added = models.BooleanField(
default=True,
null=True,
help_text="Indicates whether the table has had the created_by column added.",
)
class Meta:
ordering = ("order",)
@ -977,6 +983,9 @@ class Table(
if self.needs_background_update_column_added:
self._add_needs_background_update_column(field_attrs, indexes)
if self.created_by_column_added:
self._add_created_by(field_attrs, indexes)
if self.last_modified_by_column_added:
self._add_last_modified_by(field_attrs, indexes)
@ -1024,6 +1033,17 @@ class Table(
indexes.append(get_row_needs_background_update_index(self))
def _add_created_by(self, field_attrs, indexes):
field_attrs[CREATED_BY_COLUMN_NAME] = IgnoreMissingForeignKey(
User,
null=True,
related_name="+",
related_query_name="+",
db_constraint=False,
on_delete=models.SET_NULL,
help_text="Stores information about the user that created the row.",
)
def _add_last_modified_by(self, field_attrs, indexes):
field_attrs[LAST_MODIFIED_BY_COLUMN_NAME] = IgnoreMissingForeignKey(
User,

View file

@ -187,13 +187,10 @@ def setup_new_background_update_and_search_columns(self, table_id: int):
logger.debug(f"Postgres full-text search is disabled.")
@app.task(
bind=True,
queue="export",
)
def setup_last_modified_by_column(self, table_id: int):
@app.task(bind=True, queue="export")
def setup_created_by_and_last_modified_by_column(self, table_id: int):
from baserow.contrib.database.table.handler import TableHandler
with transaction.atomic():
table = TableHandler().get_table_for_update(table_id)
TableHandler().create_last_modified_by_field(table)
TableHandler().create_created_by_and_last_modified_by_fields(table)

View file

@ -79,9 +79,13 @@ def update_view_index_if_view_group_by_changes(sender, view_group_by, **kwargs):
@receiver(view_loaded)
def view_loaded_create_indexes_and_columns(sender, view, table_model, **kwargs):
from baserow.contrib.database.table.tasks import setup_last_modified_by_column
from baserow.contrib.database.table.tasks import (
setup_created_by_and_last_modified_by_column,
)
from baserow.contrib.database.views.handler import ViewIndexingHandler
ViewIndexingHandler.schedule_index_creation_if_needed(view, table_model)
if not view.table.last_modified_by_column_added:
setup_last_modified_by_column.delay(table_id=view.table.id)
table = view.table
if not table.last_modified_by_column_added or not table.created_by_column_added:
setup_created_by_and_last_modified_by_column.delay(table_id=view.table.id)

View file

@ -21,6 +21,7 @@ from baserow.contrib.database.fields.field_filters import (
)
from baserow.contrib.database.fields.field_types import (
BooleanFieldType,
CreatedByFieldType,
CreatedOnFieldType,
DateFieldType,
EmailFieldType,
@ -1305,7 +1306,7 @@ class UserIsViewFilterType(ViewFilterType):
"""
type = "user_is"
compatible_field_types = [LastModifiedByFieldType.type]
compatible_field_types = [CreatedByFieldType.type, LastModifiedByFieldType.type]
USER_KEY = f"users"

View file

@ -3,6 +3,7 @@ from baserow.contrib.database.fields.dependencies.handler import FieldDependency
from baserow.contrib.database.fields.field_cache import FieldCache
from baserow.contrib.database.fields.models import (
BooleanField,
CreatedByField,
CreatedOnField,
DateField,
EmailField,
@ -260,6 +261,16 @@ class FieldFixtures:
return field
def create_created_by_field(self, user=None, create_field=True, **kwargs):
self.set_test_field_kwarg_defaults(user, kwargs)
field = CreatedByField.objects.create(**kwargs)
if create_field:
self.create_model_field(kwargs["table"], field)
return field
def create_created_on_field(self, user=None, create_field=True, **kwargs):
if "date_include_time" not in kwargs:
kwargs["date_include_time"] = False

View file

@ -194,6 +194,7 @@ def setup_interesting_test_table(
"created_on_date_eu": None,
"created_on_datetime_eu_tzone": None,
"last_modified_by": None,
"created_by": None,
# We will setup link rows manually later
"link_row": None,
"self_link_row": None,

View file

@ -385,6 +385,20 @@
"primary": false,
"number_decimal_places": 2,
"number_negative": false
},
{
"id": 4698,
"type": "last_modified_by",
"name": "Last modified by",
"order": 27,
"primary": false
},
{
"id": 4699,
"type": "created_by",
"name": "Created by",
"order": 28,
"primary": false
}
],
"views": [

View file

@ -0,0 +1,301 @@
from django.shortcuts import reverse
import pytest
from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.models import CreatedByField
from baserow.test_utils.helpers import is_dict_subset
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_api_create_created_by_field_type(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1"
)
database = data_fixture.create_database_application(user=user, name="Placeholder")
table = data_fixture.create_database_table(name="Example", database=database)
response = api_client.post(
reverse("api:database:fields:list", kwargs={"table_id": table.id}),
{
"name": "created by",
"type": "created_by",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json["name"] == "created by"
assert response_json["type"] == "created_by"
assert CreatedByField.objects.all().count() == 1
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_api_update_created_by_field_type(api_client, data_fixture):
workspace = data_fixture.create_workspace()
user, token = data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
workspace=workspace,
)
database = data_fixture.create_database_application(
user=user, name="Placeholder", workspace=workspace
)
table = data_fixture.create_database_table(name="Example", database=database)
field = data_fixture.create_created_by_field(
table=table, order=1, name="created by", primary=True
)
response = api_client.patch(
reverse("api:database:fields:item", kwargs={"field_id": field.id}),
{"name": "created by renamed"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json["name"] == "created by renamed"
assert response_json["type"] == "created_by"
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_api_delete_created_by_field_type(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1"
)
database = data_fixture.create_database_application(user=user, name="Placeholder")
table = data_fixture.create_database_table(name="Example", database=database)
field = data_fixture.create_created_by_field(
table=table,
order=1,
name="created by",
)
response = api_client.delete(
reverse("api:database:fields:item", kwargs={"field_id": field.id}),
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
field.refresh_from_db()
assert field.trashed is True
@pytest.mark.field_created_by
@pytest.mark.api_rows
@pytest.mark.django_db
def test_api_create_created_by_field_type_row(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
database = data_fixture.create_database_application(user=user, name="Placeholder")
table = data_fixture.create_database_table(name="Example", database=database)
text_field = data_fixture.create_text_field(table=table)
field_handler = FieldHandler()
field = field_handler.create_field(
user=user,
table=table,
type_name="created_by",
name="created by",
)
# can't set the field value directly
response = api_client.post(
reverse("api:database:rows:list", kwargs={"table_id": table.id}),
{f"field_{field.id}": user.id},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
# value will be set when other field value is set
response = api_client.post(
reverse("api:database:rows:list", kwargs={"table_id": table.id}),
{f"field_{text_field.id}": "text"},
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}"] == {
"name": user.first_name,
"id": user.id,
}
@pytest.mark.field_created_by
@pytest.mark.api_rows
@pytest.mark.django_db
def test_update_row_dont_change_created_by_fields_values(api_client, data_fixture):
creator = data_fixture.create_user()
user, token = data_fixture.create_user_and_token()
database = data_fixture.create_database_application(user=user, name="Placeholder")
table = data_fixture.create_database_table(name="Example", database=database)
text_field = data_fixture.create_text_field(table=table)
field_handler = FieldHandler()
field = field_handler.create_field(
user=user,
table=table,
type_name="created_by",
name="created by",
)
model = table.get_model()
row = model.objects.create(**{f"field_{text_field.id}": "text"}, created_by=creator)
# can't set the field value directly
response = api_client.patch(
reverse(
"api:database:rows:item", kwargs={"table_id": table.id, "row_id": row.id}
),
{f"field_{field.id}": user.id},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
# value will not change updating the row
response = api_client.patch(
reverse(
"api:database:rows:item", kwargs={"table_id": table.id, "row_id": row.id}
),
{f"field_{text_field.id}": "updated text"},
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}"] == {
"id": creator.id,
"name": creator.first_name,
}
@pytest.mark.field_created_by
@pytest.mark.api_rows
@pytest.mark.django_db
def test_created_by_field_type_batch_insert_rows(api_client, data_fixture):
workspace = data_fixture.create_workspace()
user, token = data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
workspace=workspace,
)
database = data_fixture.create_database_application(
user=user, workspace=workspace, name="Placeholder"
)
table = data_fixture.create_database_table(name="Example", database=database)
field = data_fixture.create_created_by_field(
table=table, order=1, name="created by", primary=True
)
text_field = data_fixture.create_text_field(table=table, order=2, name="Text")
# can't set the field value directly
response = api_client.post(
reverse("api:database:rows:batch", kwargs={"table_id": table.id}),
{
"items": [
{f"field_{field.id}": user.id},
]
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
# value will be set when other field value is set
response = api_client.post(
reverse("api:database:rows:batch", kwargs={"table_id": table.id}),
{
"items": [
{f"field_{text_field.id}": "text"},
]
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
assert is_dict_subset(
{"items": [{f"field_{field.id}": {"id": user.id, "name": user.first_name}}]},
response.json(),
)
@pytest.mark.field_created_by
@pytest.mark.api_rows
@pytest.mark.django_db
def test_created_by_field_type_dont_change_when_batch_update_rows(
api_client, data_fixture
):
workspace = data_fixture.create_workspace()
user, token = data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
workspace=workspace,
)
user2 = data_fixture.create_user(workspace=workspace)
user3 = data_fixture.create_user(workspace=workspace)
database = data_fixture.create_database_application(
user=user, workspace=workspace, name="Placeholder"
)
table = data_fixture.create_database_table(name="Example", database=database)
field = data_fixture.create_created_by_field(
table=table, order=1, name="created by", primary=True
)
text_field = data_fixture.create_text_field(table=table, order=2, name="Text")
model = table.get_model()
row1 = model.objects.create(created_by=user2)
row2 = model.objects.create(created_by=user3)
row3 = model.objects.create(created_by=user3)
# can't set the field value directly
response = api_client.patch(
reverse("api:database:rows:batch", kwargs={"table_id": table.id}),
{
"items": [
{"id": row1.id, f"field_{text_field.id}": "new text"},
{"id": row2.id, f"field_{field.id}": user.id},
]
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
# value will be set when other field value is set
response = api_client.patch(
reverse("api:database:rows:batch", kwargs={"table_id": table.id}),
{
"items": [
{"id": row1.id, f"field_{text_field.id}": "new text"},
{"id": row2.id, f"field_{text_field.id}": "new text"},
{"id": row3.id, f"field_{text_field.id}": "new text"},
]
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
response_json = response.json()
assert response_json["items"][0][f"field_{field.id}"]["id"] == user2.id
assert response_json["items"][1][f"field_{field.id}"]["id"] == user3.id
assert response_json["items"][2][f"field_{field.id}"]["id"] == user3.id

View file

@ -296,5 +296,5 @@ def test_last_modified_by_field_type_batch_update_rows(api_client, data_fixture)
# assert response_json == {}
assert response_json["items"][0][f"field_{field.id}"]["id"] == user.id
assert response_json["items"][0][f"field_{field.id}"]["id"] == user.id
assert response_json["items"][1][f"field_{field.id}"]["id"] == user.id
assert getattr(row3, f"field_{field.id}") == user3

View file

@ -233,6 +233,7 @@ def test_get_row_serializer_with_user_field_names(data_fixture):
"last_modified_datetime_eu": "2021-01-02T12:00:00Z",
"last_modified_datetime_us": "2021-01-02T12:00:00Z",
"last_modified_datetime_eu_tzone": "2021-01-02T12:00:00Z",
"created_by": {"id": user.id, "name": user.first_name},
"last_modified_by": {"id": user.id, "name": user.first_name},
"created_on_date_eu": "2021-01-02",
"created_on_date_us": "2021-01-02",

View file

@ -226,21 +226,21 @@ def test_can_export_every_interesting_different_field_to_csv(
"last_modified_datetime_us,last_modified_date_us,last_modified_datetime_eu,"
"last_modified_date_eu,last_modified_datetime_eu_tzone,created_on_datetime_us,"
"created_on_date_us,created_on_datetime_eu,created_on_date_eu,created_on_datetime_eu_tzone,"
"last_modified_by,link_row,self_link_row,link_row_without_related,decimal_link_row,"
"last_modified_by,created_by,link_row,self_link_row,link_row_without_related,decimal_link_row,"
"file_link_row,file,single_select,multiple_select,multiple_collaborators,"
"phone_number,formula_text,formula_int,formula_bool,formula_decimal,formula_dateinterval,"
"formula_date,formula_singleselect,formula_email,formula_link_with_label,"
"formula_link_url_only,formula_multipleselect,count,rollup,lookup,uuid\r\n"
"1,,,,,,,,,0,False,,,,,,,01/02/2021 12:00,01/02/2021,02/01/2021 12:00,02/01/2021,"
"02/01/2021 13:00,01/02/2021 12:00,01/02/2021,02/01/2021 12:00,02/01/2021,"
"02/01/2021 13:00,user@example.com,,,,,,,,,,,test FORMULA,1,True,33.3333333333,1 day,"
"2020-01-01,,,label (https://google.com),https://google.com,,0,0.000,,"
"02/01/2021 13:00,user@example.com,user@example.com,,,,,,,,,,,test FORMULA,1,True,33.3333333333,"
"1 day,2020-01-01,,,label (https://google.com),https://google.com,,0,0.000,,"
"00000000-0000-4000-8000-000000000001\r\n"
"2,text,long_text,https://www.google.com,test@example.com,-1,1,-1.2,1.2,3,True,"
"02/01/2020 01:23,02/01/2020,01/02/2020 01:23,01/02/2020,01/02/2020 02:23,"
"01/02/2020 02:23,01/02/2021 12:00,01/02/2021,02/01/2021 12:00,02/01/2021,"
"02/01/2021 13:00,01/02/2021 12:00,01/02/2021,02/01/2021 12:00,02/01/2021,"
'02/01/2021 13:00,user@example.com,"linked_row_1,linked_row_2,unnamed row 3",unnamed row 1,'
'02/01/2021 13:00,user@example.com,user@example.com,"linked_row_1,linked_row_2,unnamed row 3",unnamed row 1,'
'"linked_row_1,linked_row_2","1.234,-123.456,unnamed row 3",'
'"name.txt (http://localhost:8000/media/user_files/test_hash.txt),unnamed row 2",'
'"a.txt (http://localhost:8000/media/user_files/hashed_name.txt),'

View file

@ -0,0 +1,468 @@
from io import BytesIO
from django.core.exceptions import ValidationError
import pytest
from baserow.contrib.database.fields.deferred_field_fk_updater import (
DeferredFieldFkUpdater,
)
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.rows.handler import RowHandler
from baserow.contrib.database.views.handler import ViewHandler
from baserow.core.handler import CoreHandler
from baserow.core.registries import ImportExportConfig
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_create_created_by_field(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(
table=table, order=1, name="name", primary=True
)
row_handler = RowHandler()
model = table.get_model()
row_handler.create_row(
user=user, table=table, values={f"field_{text_field.id}": "Row 1"}, model=model
)
row_handler.create_row(
user=user, table=table, values={f"field_{text_field.id}": "Row 2"}, model=model
)
row_handler.create_row(
user=user, table=table, values={f"field_{text_field.id}": "Row 3"}, model=model
)
field_handler = FieldHandler()
field = field_handler.create_field(
user=user,
table=table,
type_name="created_by",
name="created by",
)
model = table.get_model()
rows = list(model.objects.all())
assert getattr(rows[0], f"field_{field.id}") == user
assert getattr(rows[1], f"field_{field.id}") == user
assert getattr(rows[2], f"field_{field.id}") == user
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_create_created_by_field_force_create_created_by_column(
data_fixture,
):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user, created_by_column_added=False)
text_field = data_fixture.create_text_field(
table=table, order=1, name="name", primary=True
)
row_handler = RowHandler()
model = table.get_model()
row_handler.create_row(
user=user, table=table, values={f"field_{text_field.id}": "Row 1"}, model=model
)
field_handler = FieldHandler()
field = field_handler.create_field(
user=user,
table=table,
type_name="created_by",
name="created by",
)
model = table.get_model()
rows = list(model.objects.all())
assert getattr(rows[0], f"field_{field.id}") is None
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_create_row_created_by(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field = data_fixture.create_created_by_field(
table=table, order=1, name="created_by_field", primary=True
)
row_handler = RowHandler()
model = table.get_model()
row_1 = row_handler.create_row(user=user, table=table, values={}, model=model)
assert getattr(row_1, f"field_{field.id}") == user
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_prevent_create_row_created_by(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field = data_fixture.create_created_by_field(
table=table, order=1, name="created_by_field", primary=True
)
row_handler = RowHandler()
model = table.get_model()
with pytest.raises(ValidationError):
row_handler.create_row(
user=user,
table=table,
values={f"field_{field.id}": user.id},
model=model,
)
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_create_rows_created_by(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field = data_fixture.create_created_by_field(
table=table, order=1, name="created_by_field", primary=True
)
row_handler = RowHandler()
model = table.get_model()
rows = row_handler.create_rows(
user=user, table=table, rows_values=[{}, {}], model=model
)
assert getattr(rows[0], f"field_{field.id}") == user
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_prevent_create_rows_created_by(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field = data_fixture.create_created_by_field(
table=table, order=1, name="created_by_field", primary=True
)
row_handler = RowHandler()
model = table.get_model()
with pytest.raises(ValidationError):
row_handler.create_rows(
user=user,
table=table,
rows_values=[{f"field_{field.id}": user.id}, {}],
model=model,
)
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_update_row_dont_update_created_by(data_fixture):
creator = data_fixture.create_user()
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(table=table, order=2, name="text")
field = data_fixture.create_created_by_field(
table=table, order=1, name="created_by_field", primary=True
)
row_handler = RowHandler()
model = table.get_model()
row_1 = model.objects.create(
**{f"field_{text_field.id}": "text"}, created_by=creator
)
assert getattr(row_1, f"field_{field.id}") == creator
updated_row = row_handler.update_row(
user=user, table=table, row=row_1, values={"text": "test"}, model=model
)
assert getattr(updated_row, f"field_{field.id}") == creator
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_prevent_update_row_created_by(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(table=table, order=2, name="text")
field = data_fixture.create_created_by_field(
table=table, order=1, name="created_by_field", primary=True
)
row_handler = RowHandler()
model = table.get_model()
row_1 = model.objects.create(**{f"field_{text_field.id}": "text"})
with pytest.raises(ValidationError):
row_handler.update_row(
user=user,
table=table,
row=row_1,
values={f"field_{field.id}": user.id},
model=model,
)
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_update_rows_dont_update_created_by(data_fixture):
creator = data_fixture.create_user()
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(table=table, order=2, name="text")
field = data_fixture.create_created_by_field(
table=table, order=1, name="created_by_field", primary=True
)
row_handler = RowHandler()
model = table.get_model()
row_1 = model.objects.create(
**{f"field_{text_field.id}": "text"}, created_by=creator
)
assert getattr(row_1, f"field_{field.id}") == creator
row_handler.update_rows(
user=user,
table=table,
rows_values=[{"id": row_1.id, f"field_{text_field.id}": "changed"}],
model=model,
)
row_1.refresh_from_db()
assert getattr(row_1, f"field_{field.id}") == creator
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_prevent_update_rows_created_by(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(table=table, order=2, name="text")
field = data_fixture.create_created_by_field(
table=table, order=1, name="created_by_field", primary=True
)
row_handler = RowHandler()
model = table.get_model()
row_1 = model.objects.create(**{f"field_{text_field.id}": "text"})
with pytest.raises(ValidationError):
row_handler.update_rows(
user=user,
table=table,
rows_values=[{"id": row_1.id, f"field_{field.id}": user.id}],
model=model,
)
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_import_export_created_by_field(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field_handler = FieldHandler()
field = field_handler.create_field(
user=user,
table=table,
type_name="created_by",
name="modified by",
)
field_type = field_type_registry.get_by_model(field)
field_serialized = field_type.export_serialized(field)
id_mapping = {}
field_imported = field_type.import_serialized(
table,
field_serialized,
ImportExportConfig(include_permission_data=True),
id_mapping,
DeferredFieldFkUpdater(),
)
assert field_imported.id != field.id
assert field_serialized == {
"id": field.id,
"name": "modified by",
"order": field.order,
"primary": False,
"type": "created_by",
}
@pytest.mark.field_created_by
@pytest.mark.django_db(transaction=True)
def test_get_set_export_serialized_value_created_by_field(data_fixture):
user = data_fixture.create_user()
workspace = data_fixture.create_workspace(user=user)
imported_workspace = data_fixture.create_workspace(user=user)
database = data_fixture.create_database_application(workspace=workspace)
table = data_fixture.create_database_table(database=database)
field = data_fixture.create_created_by_field(table=table)
core_handler = CoreHandler()
model = table.get_model()
row_1 = model.objects.create(created_by=user)
row_2 = model.objects.create()
row_3 = model.objects.create(created_by=user)
config = ImportExportConfig(include_permission_data=False)
exported_applications = core_handler.export_workspace_applications(
workspace, BytesIO(), config
)
imported_applications, _ = core_handler.import_applications_to_workspace(
imported_workspace, exported_applications, BytesIO(), config, None
)
imported_database = imported_applications[0]
imported_table = imported_database.table_set.all()[0]
imported_field = imported_table.field_set.all().first().specific
assert imported_table.id != table.id
assert imported_field.id != field.id
imported_model = imported_table.get_model()
all = imported_model.objects.all()
assert len(all) == 3
imported_row_1 = all[0]
imported_row_2 = all[1]
imported_row_3 = all[2]
assert getattr(imported_row_1, f"field_{imported_field.id}") == user
assert getattr(imported_row_2, f"field_{imported_field.id}") is None
assert getattr(imported_row_3, f"field_{imported_field.id}") == user
@pytest.mark.field_created_by
@pytest.mark.django_db(transaction=True)
def test_duplicate_created_by_field(data_fixture):
workspace = data_fixture.create_workspace()
user, token = data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
workspace=workspace,
)
user2 = data_fixture.create_user(workspace=workspace)
user3 = data_fixture.create_user(workspace=workspace)
database = data_fixture.create_database_application(
user=user, workspace=workspace, name="Placeholder"
)
table = data_fixture.create_database_table(name="Example", database=database)
field = data_fixture.create_created_by_field(
table=table, order=1, name="created by", primary=True
)
model = table.get_model()
row1 = model.objects.create(created_by=user)
row2 = model.objects.create(created_by=user2)
row3 = model.objects.create(created_by=user3)
new_field, _ = FieldHandler().duplicate_field(user, field)
model = table.get_model()
rows = model.objects.all()
assert getattr(rows[0], f"field_{new_field.id}") == user
assert getattr(rows[1], f"field_{new_field.id}") == user2
assert getattr(rows[2], f"field_{new_field.id}") == user3
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_trash_restore_created_by_field(data_fixture):
workspace = data_fixture.create_workspace()
user = data_fixture.create_user(
email="test@test.nl",
password="password",
first_name="Test1",
workspace=workspace,
)
user2 = data_fixture.create_user(workspace=workspace)
user3 = data_fixture.create_user(workspace=workspace)
database = data_fixture.create_database_application(
user=user, workspace=workspace, name="Placeholder"
)
table = data_fixture.create_database_table(name="Example", database=database)
field = data_fixture.create_created_by_field(
table=table, order=1, name="created by"
)
text_field = data_fixture.create_text_field(table=table, order=2, name="Text")
model = table.get_model()
row1 = model.objects.create(created_by=user2)
row2 = model.objects.create(created_by=user2)
row3 = model.objects.create(created_by=user3)
row4 = model.objects.create(created_by=user3)
FieldHandler().delete_field(user, field)
# last_updated_by in the table will be changed by making
# changes to rows. This needs to be reflected after the
# field is restored
RowHandler().update_row_by_id(
user, table, row_id=row1.id, values={f"field_{text_field.id}": "new text"}
)
RowHandler().update_rows(
user=user,
table=table,
rows_values=[
{"id": row2.id, f"field_{text_field.id}": "updated"},
{"id": row3.id, f"field_{text_field.id}": "updated"},
],
)
FieldHandler().restore_field(field)
model = table.get_model()
rows = model.objects.all()
assert getattr(rows[0], f"field_{field.id}") == user2
assert getattr(rows[1], f"field_{field.id}") == user2
assert getattr(rows[2], f"field_{field.id}") == user3
assert getattr(rows[3], f"field_{field.id}") == user3
@pytest.mark.field_created_by
@pytest.mark.django_db
def test_created_by_field_type_sorting(data_fixture):
user_a = data_fixture.create_user(email="user1@baserow.io", first_name="User a")
user_b = data_fixture.create_user(email="user2@baserow.io", first_name="User b")
user_c = data_fixture.create_user(email="user3@baserow.io", first_name="User c")
database = data_fixture.create_database_application(user=user_a, name="Placeholder")
data_fixture.create_user_workspace(workspace=database.workspace, user=user_b)
data_fixture.create_user_workspace(workspace=database.workspace, user=user_c)
table = data_fixture.create_database_table(name="Example", database=database)
grid_view = data_fixture.create_grid_view(table=table)
field = data_fixture.create_created_by_field(
user=user_a, table=table, name="created by"
)
model = table.get_model()
view_handler = ViewHandler()
row1 = model.objects.create(created_by=user_c)
row2 = model.objects.create(created_by=user_b)
row3 = model.objects.create(created_by=user_a)
row4 = model.objects.create(created_by=user_c)
row5 = model.objects.create(created_by=None)
sort = data_fixture.create_view_sort(view=grid_view, field=field, order="ASC")
rows = view_handler.apply_sorting(grid_view, model.objects.all())
row_ids = [row.id for row in rows]
assert row_ids == [row5.id, row3.id, row2.id, row1.id, row4.id]
sort.order = "DESC"
sort.save()
rows = view_handler.apply_sorting(grid_view, model.objects.all())
row_ids = [row.id for row in rows]
assert row_ids == [row1.id, row4.id, row2.id, row3.id, row5.id]

View file

@ -28,6 +28,7 @@ from baserow.contrib.database.fields.field_helpers import (
from baserow.contrib.database.fields.field_types import (
BooleanFieldType,
CountFieldType,
CreatedByFieldType,
CreatedOnFieldType,
DateFieldType,
EmailFieldType,
@ -337,6 +338,13 @@ def test_field_conversion_last_modified_by(data_fixture):
_test_can_convert_between_fields(data_fixture, LastModifiedByFieldType.type)
@pytest.mark.field_created_by
@pytest.mark.disabled_in_ci
@pytest.mark.django_db
def test_field_conversion_created_by(data_fixture):
_test_can_convert_between_fields(data_fixture, CreatedByFieldType.type)
@pytest.mark.django_db
def test_get_field(data_fixture):
user = data_fixture.create_user()

View file

@ -754,7 +754,7 @@ def test_create_last_modified_by_field(data_fixture):
with pytest.raises(FieldDoesNotExist):
model._meta.get_field(LAST_MODIFIED_BY_COLUMN_NAME)
TableHandler().create_last_modified_by_field(table)
TableHandler().create_created_by_and_last_modified_by_fields(table)
table.refresh_from_db()
assert table.last_modified_by_column_added

View file

@ -50,7 +50,7 @@ def test_workspace_user_get_next_order(data_fixture):
@pytest.mark.django_db
def test_get_table_model(data_fixture):
default_model_fields_count = 6
default_model_fields_count = 7
table = data_fixture.create_database_table(name="Cars")
text_field = data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
@ -186,6 +186,7 @@ def test_get_table_model_with_fulltext_search_enabled(data_fixture):
"updated_on",
"trashed",
"order",
"created_by",
"last_modified_by",
]
added_fields = [

View file

@ -19,7 +19,7 @@ def test_view_loaded_creates_last_modified_by_column(indexing_handler, data_fixt
# won't schedule column creation if already added
with patch(
"baserow.contrib.database.table.tasks.setup_last_modified_by_column"
"baserow.contrib.database.table.tasks.setup_created_by_and_last_modified_by_column"
) as setup:
view_loaded_create_indexes_and_columns(
None, view, table_model, table=table, user=user
@ -30,7 +30,7 @@ def test_view_loaded_creates_last_modified_by_column(indexing_handler, data_fixt
table.last_modified_by_column_added = False
table.save()
with patch(
"baserow.contrib.database.table.tasks.setup_last_modified_by_column"
"baserow.contrib.database.table.tasks.setup_created_by_and_last_modified_by_column"
) as setup:
view_loaded_create_indexes_and_columns(
None, view, table_model, table=table, user=user

View file

@ -1172,6 +1172,17 @@ def test_local_baserow_table_service_generate_schema_with_interesting_test_table
"title": "last_modified_by",
"type": "object",
},
field_db_column_by_name["created_by"]: {
"default": None,
"metadata": {},
"original_type": "created_by",
"properties": {
"id": {"title": "id", "type": "number"},
"name": {"title": "name", "type": "string"},
},
"title": "created_by",
"type": "object",
},
field_db_column_by_name["link_row"]: {
"title": "link_row",
"default": None,

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Add the created_by field type.",
"issue_number": 624,
"bullet_points": [],
"created_at": "2023-12-04"
}

View file

@ -65,6 +65,7 @@ def test_can_export_every_interesting_different_field_to_json(
"created_on_date_eu": "02/01/2021",
"created_on_datetime_eu_tzone": "02/01/2021 13:00",
"last_modified_by": "user@example.com",
"created_by": "user@example.com",
"link_row": [],
"self_link_row": [],
"link_row_without_related": [],
@ -125,6 +126,7 @@ def test_can_export_every_interesting_different_field_to_json(
"created_on_date_eu": "02/01/2021",
"created_on_datetime_eu_tzone": "02/01/2021 13:00",
"last_modified_by": "user@example.com",
"created_by": "user@example.com",
"link_row": [
"linked_row_1",
"linked_row_2",
@ -308,6 +310,7 @@ def test_can_export_every_interesting_different_field_to_xml(
<created-on-date-eu>02/01/2021</created-on-date-eu>
<created-on-datetime-eu-tzone>02/01/2021 13:00</created-on-datetime-eu-tzone>
<last-modified-by>user@example.com</last-modified-by>
<created-by>user@example.com</created-by>
<link-row/>
<self-link-row/>
<link-row-without-related/>
@ -368,6 +371,7 @@ def test_can_export_every_interesting_different_field_to_xml(
<created-on-date-eu>02/01/2021</created-on-date-eu>
<created-on-datetime-eu-tzone>02/01/2021 13:00</created-on-datetime-eu-tzone>
<last-modified-by>user@example.com</last-modified-by>
<created-by>user@example.com</created-by>
<link-row>
<item>linked_row_1</item>
<item>linked_row_2</item>

View file

@ -91,6 +91,7 @@
"lastModified": "Last modified",
"lastModifiedBy": "Last modified by",
"createdOn": "Created on",
"createdBy": "Created by",
"url": "URL",
"email": "Email",
"file": "File",
@ -137,6 +138,7 @@
"lastModifiedReadOnly": "The last modified field is a read only field.",
"lastModifiedBy": "The last modified by field is a read only field.",
"createdOnReadOnly": "The created on field is a read only field.",
"createdBy": "The created by field is a read only field showing the user that created the row.",
"url": "Accepts a string that must be a URL.",
"email": "Accepts a string that must be an email address.",
"file": "Accepts an array of objects containing at least the name of the user file. You can use the \"File uploads\" endpoints to upload the file. The response of those calls can be provided directly as object here. The endpoints can be found in the left sidebar.",

View file

@ -2021,6 +2021,144 @@ export class LastModifiedByFieldType extends FieldType {
}
}
export class CreatedByFieldType extends FieldType {
static getType() {
return 'created_by'
}
getIconClass() {
return 'iconoir-user'
}
getName() {
const { i18n } = this.app
return i18n.t('fieldType.createdBy')
}
getFormViewFieldComponents(field) {
return {}
}
getIsReadOnly() {
return true
}
shouldFetchDataWhenAdded() {
return true
}
getGridViewFieldComponent() {
return GridViewFieldLastModifiedBy
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldLastModifiedBy
}
getRowEditFieldComponent(field) {
return RowEditFieldLastModifiedBy
}
getCardComponent() {
return RowCardFieldLastModifiedBy
}
getCanSortInView(field) {
return true
}
getSort(name, order) {
return (a, b) => {
let userNameA = a[name] === null ? '' : a[name].name
let userNameB = b[name] === null ? '' : b[name].name
const workspaces = this.app.store.getters['workspace/getAll']
const workspaceAvailable = workspaces.length > 0
if (workspaceAvailable) {
if (a[name] !== null) {
const workspaceUserA = this.app.store.getters[
'workspace/getUserById'
](a[name].id)
userNameA = workspaceUserA ? workspaceUserA.name : userNameA
}
if (b[name] !== null) {
const workspaceUserB = this.app.store.getters[
'workspace/getUserById'
](b[name].id)
userNameB = workspaceUserB ? workspaceUserB.name : userNameB
}
}
return collatedStringCompare(userNameA, userNameB, order)
}
}
canBeReferencedByFormulaField() {
return false
}
_getCurrentUserValue() {
return {
id: this.app.store.getters['auth/getUserId'],
name: this.app.store.getters['auth/getName'],
}
}
getNewRowValue() {
return this._getCurrentUserValue()
}
onRowChange(row, currentField, currentFieldValue) {
return currentFieldValue
}
prepareValueForCopy(field, value) {
if (value === undefined || value === null) {
return ''
}
const name = value.name
const workspaces = this.app.store.getters['workspace/getAll']
if (workspaces.length > 0) {
const workspaceUser = this.app.store.getters['workspace/getUserById'](
value.id
)
return workspaceUser ? workspaceUser.name : name
}
return name
}
toHumanReadableString(field, value, delimiter = ', ') {
return this.prepareValueForCopy(field, value)
}
toSearchableString(field, value, delimiter = ', ') {
return this.toHumanReadableString(field, value, delimiter)
}
getContainsFilterFunction() {
return genericContainsFilter
}
getDocsDataType(field) {
return 'object'
}
getDocsDescription(field) {
return this.app.i18n.t('fieldDocs.createdBy')
}
getDocsRequestExample() {
return {
id: 1,
name: 'John',
}
}
}
export class URLFieldType extends FieldType {
static getType() {
return 'url'

View file

@ -22,6 +22,7 @@ import {
MultipleSelectFieldType,
PhoneNumberFieldType,
CreatedOnFieldType,
CreatedByFieldType,
FormulaFieldType,
CountFieldType,
RollupFieldType,
@ -443,6 +444,7 @@ export default (context) => {
app.$registry.register('field', new LastModifiedFieldType(context))
app.$registry.register('field', new LastModifiedByFieldType(context))
app.$registry.register('field', new CreatedOnFieldType(context))
app.$registry.register('field', new CreatedByFieldType(context))
app.$registry.register('field', new URLFieldType(context))
app.$registry.register('field', new EmailFieldType(context))
app.$registry.register('field', new FileFieldType(context))

View file

@ -1614,7 +1614,7 @@ export class UserIsFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['last_modified_by']
return ['created_by', 'last_modified_by']
}
isAllowedInPublicViews() {
@ -1650,7 +1650,7 @@ export class UserIsNotFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['last_modified_by']
return ['created_by', 'last_modified_by']
}
isAllowedInPublicViews() {

View file

@ -206,6 +206,14 @@ const mockedFields = {
table_id: 42,
type: 'last_modified_by',
},
created_by: {
id: 22,
name: 'created_by',
order: 22,
primary: false,
table_id: 42,
type: 'created_by',
},
}
const valuesToCall = [null, undefined]