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

Resolve "A Field can end up with multiple field options"

This commit is contained in:
Bram Wiepjes 2022-01-06 16:47:50 +00:00
parent 9f46b24da2
commit eec028a198
11 changed files with 85 additions and 38 deletions
backend/src/baserow/contrib/database
changelog.md
premium/backend/src/baserow_premium
api/views/kanban
views

View file

@ -137,7 +137,9 @@ class GalleryViewView(APIView):
if field_options:
context = {"fields": [o["field"] for o in model._field_objects.values()]}
serializer_class = view_type.get_field_options_serializer_class()
serializer_class = view_type.get_field_options_serializer_class(
create_if_missing=True
)
response.data.update(**serializer_class(view, context=context).data)
return response

View file

@ -186,7 +186,9 @@ class GridViewView(APIView):
if field_options:
context = {"fields": [o["field"] for o in model._field_objects.values()]}
serializer_class = view_type.get_field_options_serializer_class()
serializer_class = view_type.get_field_options_serializer_class(
create_if_missing=True
)
response.data.update(**serializer_class(view, context=context).data)
if row_metadata:

View file

@ -19,8 +19,9 @@ class FieldOptionsField(serializers.Field):
"invalid_value": "Must be valid field options.",
}
def __init__(self, serializer_class, **kwargs):
def __init__(self, serializer_class, create_if_missing=True, **kwargs):
self.serializer_class = serializer_class
self.create_if_missing = create_if_missing
self._spectacular_annotation = {
"field": serializers.DictField(
child=serializer_class(),
@ -83,7 +84,9 @@ class FieldOptionsField(serializers.Field):
fields = self.context.get("fields")
return {
field_options.field_id: self.serializer_class(field_options).data
for field_options in value.get_field_options(True, fields)
for field_options in value.get_field_options(
self.create_if_missing, fields
)
}
else:
return value

View file

@ -985,7 +985,9 @@ class ViewFieldOptionsView(APIView):
view_type = view_type_registry.get_by_model(view)
try:
serializer_class = view_type.get_field_options_serializer_class()
serializer_class = view_type.get_field_options_serializer_class(
create_if_missing=True
)
except ValueError:
raise ViewDoesNotSupportFieldOptions(
"The view type does not have a `field_options_serializer_class`"
@ -1034,7 +1036,9 @@ class ViewFieldOptionsView(APIView):
handler = ViewHandler()
view = handler.get_view(view_id).specific
view_type = view_type_registry.get_by_model(view)
serializer_class = view_type.get_field_options_serializer_class()
serializer_class = view_type.get_field_options_serializer_class(
create_if_missing=True
)
data = validate_data(serializer_class, request.data)
with view_type.map_api_exceptions():

View file

@ -1,7 +1,7 @@
import secrets
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db import models, transaction
from baserow.core.utils import get_model_reference_field_name
from baserow.core.models import UserFile
@ -110,8 +110,8 @@ class View(
them for a second time. This is only needed if `create_if_not_exists` is
True.
:type fields: list
:return: A list of field options instances related to this grid view.
:rtype: list or QuerySet
:return: A queryset containing all the field options of view.
:rtype: QuerySet
"""
view_type = view_type_registry.get_by_model(self.specific_class)
@ -130,20 +130,49 @@ class View(
"any descendants."
)
field_options = through_model.objects.filter(**{field_name: self})
def get_queryset():
return through_model.objects.filter(**{field_name: self})
field_options = get_queryset()
if create_if_not_exists:
field_options = list(field_options)
if not fields:
fields = Field.objects.filter(table=self.table)
fields_queryset = Field.objects.filter(table_id=self.table.id)
existing_field_ids = [options.field_id for options in field_options]
for field in fields:
if field.id not in existing_field_ids:
field_option = through_model.objects.create(
**{field_name: self, "field": field}
if fields is None:
field_count = fields_queryset.count()
else:
field_count = len(fields)
# The check there are missing field options must be as efficient as
# possible because this is being done a lot.
if len(field_options) < field_count:
if fields is None:
fields = fields_queryset
with transaction.atomic():
# Lock the view so concurrent calls to this method wont create
# duplicate field options.
View.objects.filter(id=self.id).select_for_update().first()
# Invalidate the field options because they could have been
# changed concurrently.
field_options = get_queryset()
# In the case when field options are missing, we can be more
# in-efficient because this rarely happens. The most important part
# is that the check is fast.
existing_field_ids = [options.field_id for options in field_options]
through_model.objects.bulk_create(
[
through_model(**{field_name: self, "field": field})
for field in fields
if field.id not in existing_field_ids
]
)
field_options.append(field_option)
# Invalidate the field options because new ones have been created and
# we always want to return a queryset.
field_options = get_queryset()
return field_options

View file

@ -254,12 +254,15 @@ class ViewType(
model = view.table.get_model()
return model._field_objects.values(), model
def get_field_options_serializer_class(self):
def get_field_options_serializer_class(self, create_if_missing):
"""
Generates a serializer that has the `field_options` property as a
`FieldOptionsField`. This serializer can be used by the API to validate or list
the field options.
:param create_if_missing: Whether or not to create any missing field options
when looking them up during serialization.
:type create_if_missing: bool
:raises ValueError: When the related view type does not have a field options
serializer class.
:return: The generated serializer.
@ -283,7 +286,8 @@ class ViewType(
attrs = {
"Meta": meta,
"field_options": FieldOptionsField(
serializer_class=self.field_options_serializer_class
serializer_class=self.field_options_serializer_class,
create_if_missing=create_if_missing,
),
}
@ -350,7 +354,9 @@ class ViewTypeRegistry(
def get_field_options_serializer_map(self):
return {
view_type.type: view_type.get_field_options_serializer_class()
view_type.type: view_type.get_field_options_serializer_class(
create_if_missing=False
)
for view_type in self.registry.values()
}

View file

@ -102,13 +102,11 @@ class GridViewType(ViewType):
grid_view = ViewHandler().get_view(view.id, view_model=GridView)
# Ensure all fields have options created before we then query directly off the
# options table below.
grid_view.get_field_options(create_if_not_exists=True)
ordered_field_objects = []
ordered_visible_fields = (
grid_view.get_field_options()
# Ensure all fields have options created before we then query directly off
# the options table below.
grid_view.get_field_options(create_if_not_exists=True)
.filter(hidden=False)
.order_by("-field__primary", "order", "field__id")
.values_list("field__id", flat=True)
@ -189,8 +187,9 @@ class GalleryViewType(ViewType):
visible.
"""
field_options = view.get_field_options(create_if_not_exists=True)
field_options.sort(key=lambda x: x.field_id)
field_options = view.get_field_options(create_if_not_exists=True).order_by(
"field__id"
)
ids_to_update = [f.id for f in field_options[0:3]]
if ids_to_update:

View file

@ -178,7 +178,9 @@ def view_sort_deleted(sender, view_sort_id, view_sort, user, **kwargs):
def view_field_options_updated(sender, view, user, **kwargs):
table_page_type = page_registry.get("table")
view_type = view_type_registry.get_by_model(view.specific_class)
serializer_class = view_type.get_field_options_serializer_class()
serializer_class = view_type.get_field_options_serializer_class(
create_if_missing=False
)
transaction.on_commit(
lambda: table_page_type.broadcast(
{

View file

@ -18,6 +18,7 @@
* Allow changing the text of the submit button in the form view.
* Fixed reordering of single select options when initially creating the field.
* Improved performance by not rendering cells that are out of the view port.
* Fix bug where field options in rare situations could have been duplicated.
## Released (2021-11-25)

View file

@ -188,7 +188,9 @@ class KanbanViewView(APIView):
if field_options:
view_type = view_type_registry.get_by_model(view)
context = {"fields": [o["field"] for o in model._field_objects.values()]}
serializer_class = view_type.get_field_options_serializer_class()
serializer_class = view_type.get_field_options_serializer_class(
create_if_missing=True
)
response.update(**serializer_class(view, context=context).data)
return Response(response)

View file

@ -126,13 +126,10 @@ class KanbanViewType(ViewType):
When a kanban view is created, we want to set the first three fields as visible.
"""
field_options = view.get_field_options(create_if_not_exists=True)
field_options.sort(key=lambda x: x.field_id)
ids_to_update = [
field_option.id
for index, field_option in enumerate(field_options)
if index < 3
]
field_options = view.get_field_options(create_if_not_exists=True).order_by(
"field__id"
)
ids_to_update = [f.id for f in field_options[0:3]]
if len(ids_to_update) > 0:
KanbanViewFieldOptions.objects.filter(id__in=ids_to_update).update(