1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-01 03:42:47 +00:00
bramw_baserow/backend/src/baserow/contrib/database/views/registries.py
2022-04-26 09:37:37 +00:00

727 lines
26 KiB
Python

from typing import TYPE_CHECKING, Callable, Union, List, Iterable, Tuple
from django.contrib.auth.models import User as DjangoUser
from django.db import models as django_models
from rest_framework.fields import CharField
from rest_framework.serializers import Serializer
from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
from baserow.core.registry import (
Instance,
Registry,
ModelInstanceMixin,
ModelRegistryMixin,
CustomFieldsInstanceMixin,
CustomFieldsRegistryMixin,
APIUrlsRegistryMixin,
APIUrlsInstanceMixin,
ImportExportMixin,
MapAPIExceptionsInstanceMixin,
)
from baserow.contrib.database.fields import models as field_models
if TYPE_CHECKING:
from baserow.contrib.database.views.models import View
from .exceptions import (
ViewTypeAlreadyRegistered,
ViewTypeDoesNotExist,
ViewFilterTypeAlreadyRegistered,
ViewFilterTypeDoesNotExist,
AggregationTypeDoesNotExist,
AggregationTypeAlreadyRegistered,
)
class ViewType(
MapAPIExceptionsInstanceMixin,
APIUrlsInstanceMixin,
CustomFieldsInstanceMixin,
ModelInstanceMixin,
ImportExportMixin,
Instance,
):
"""
This abstract class represents a custom view type that can be added to the
view type registry. It must be extended so customisation can be done. Each view type
will have his own model that must extend the View model, this is needed so that the
user can set custom settings per view instance he has created.
The added API urls will be available under the namespace 'api:database:views'.
So if a url with name 'example' is returned by the method it will available under
reverse('api:database:views:example').
Example:
from baserow.contrib.database.views.models import View
from baserow.contrib.database.views.registry import ViewType, view_type_registry
class ExampleViewModel(ViewType):
pass
class ExampleViewType(ViewType):
type = 'unique-view-type'
model_class = ExampleViewModel
allowed_fields = ['example_ordering']
serializer_field_names = ['example_ordering']
serializer_field_overrides = {
'example_ordering': serializers.CharField()
}
def get_api_urls(self):
return [
path('view-type/', include(api_urls, namespace=self.type)),
]
view_type_registry.register(ExampleViewType())
"""
can_filter = True
"""
Indicates if the view supports filters. If not, it will not be possible to add
filter to the view.
"""
can_sort = True
"""
Indicates if the view support sortings. If not, it will not be possible to add a
sort to the view.
"""
can_aggregate_field = False
"""
Indicates if the view supports field aggregation. If not, it will not be possible
to compute fields aggregation for this view type.
"""
can_decorate = False
"""
Indicates if the view supports decoration. If not, it will not be possible
to create decoration for this view type.
"""
can_share = False
"""
Indicates if the view supports being shared via a public link.
"""
field_options_model_class = None
"""
The model class of the through table that contains the field options. The model
must have a foreign key to the field and to the view.
"""
field_options_serializer_class = None
"""
The serializer class to serialize the field options model. It will be used in the
API to update and list the field option, but it is also used to broadcast field
option changes.
"""
restrict_link_row_public_view_sharing = True
"""
If a view is shared publicly, the `PublicViewLinkRowFieldLookupView` exposes all
the primary values of every visible `link_row` field in the view. This property
indicates whether the values should be restricted to existing relationships to
the view.
"""
when_shared_publicly_requires_realtime_events = True
"""
If a view is shared publicly, this controls whether or not realtime row, field
and view events will be available to subscribe to and sent to said subscribers.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.can_share:
self.allowed_fields = self.allowed_fields + ["public"]
self.serializer_field_names = self.serializer_field_names + [
"public",
"slug",
]
self.serializer_field_overrides = {
**self.serializer_field_overrides,
"slug": CharField(
read_only=True,
help_text="The unique slug that can be used to construct a public "
"URL.",
),
}
def export_serialized(self, view, files_zip, storage):
"""
Exports the view to a serialized dict that can be imported by the
`import_serialized` method. This dict is also JSON serializable.
:param view: The view instance that must be exported.
:type view: View
:param files_zip: A zip file buffer where the files related to the export
must be copied into.
:type files_zip: ZipFile
:param storage: The storage where the files can be loaded from.
:type storage: Storage or None
:return: The exported view.
:rtype: dict
"""
serialized = {
"id": view.id,
"type": self.type,
"name": view.name,
"order": view.order,
}
if self.can_filter:
serialized["filter_type"] = view.filter_type
serialized["filters_disabled"] = view.filters_disabled
serialized["filters"] = [
{
"id": view_filter.id,
"field_id": view_filter.field_id,
"type": view_filter.type,
"value": view_filter_type_registry.get(
view_filter.type
).get_export_serialized_value(view_filter.value),
}
for view_filter in view.viewfilter_set.all()
]
if self.can_sort:
serialized["sortings"] = [
{"id": sort.id, "field_id": sort.field_id, "order": sort.order}
for sort in view.viewsort_set.all()
]
if self.can_decorate:
serialized["decorations"] = [
{
"id": deco.id,
"type": deco.type,
"value_provider_type": deco.value_provider_type,
"value_provider_conf": deco.value_provider_conf,
"order": deco.order,
}
for deco in view.viewdecoration_set.all()
]
if self.can_share:
serialized["public"] = view.public
return serialized
def import_serialized(
self, table, serialized_values, id_mapping, files_zip, storage
):
"""
Imported an exported serialized view dict that was exported via the
`export_serialized` method. Note that all the fields must be imported first
because we depend on the new field id to be in the mapping.
:param table: The table where the view should be added to.
:type table: Table
:param serialized_values: The exported serialized view values that need to
be imported.
:type serialized_values: dict
:param id_mapping: The map of exported ids to newly created ids that must be
updated when a new instance has been created.
:type id_mapping: dict
:param files_zip: A zip file buffer where files related to the export can be
extracted from.
:type files_zip: ZipFile
:param storage: The storage where the files can be copied to.
:type storage: Storage or None
:return: The newly created view instance.
:rtype: View
"""
from .models import ViewFilter, ViewSort, ViewDecoration
if "database_views" not in id_mapping:
id_mapping["database_views"] = {}
id_mapping["database_view_filters"] = {}
id_mapping["database_view_sortings"] = {}
id_mapping["database_view_decorations"] = {}
serialized_copy = serialized_values.copy()
view_id = serialized_copy.pop("id")
serialized_copy.pop("type")
filters = serialized_copy.pop("filters") if self.can_filter else []
sortings = serialized_copy.pop("sortings") if self.can_sort else []
decorations = (
serialized_copy.pop("decorations", []) if self.can_decorate else []
)
view = self.model_class.objects.create(table=table, **serialized_copy)
id_mapping["database_views"][view_id] = view.id
if self.can_filter:
for view_filter in filters:
view_filter_type = view_filter_type_registry.get(view_filter["type"])
view_filter_copy = view_filter.copy()
view_filter_id = view_filter_copy.pop("id")
view_filter_copy["field_id"] = id_mapping["database_fields"][
view_filter_copy["field_id"]
]
view_filter_copy[
"value"
] = view_filter_type.set_import_serialized_value(
view_filter_copy["value"], id_mapping
)
view_filter_object = ViewFilter.objects.create(
view=view, **view_filter_copy
)
id_mapping["database_view_filters"][
view_filter_id
] = view_filter_object.id
if self.can_sort:
for view_decoration in sortings:
view_sort_copy = view_decoration.copy()
view_sort_id = view_sort_copy.pop("id")
view_sort_copy["field_id"] = id_mapping["database_fields"][
view_sort_copy["field_id"]
]
view_sort_object = ViewSort.objects.create(view=view, **view_sort_copy)
id_mapping["database_view_sortings"][view_sort_id] = view_sort_object.id
if self.can_decorate:
def _update_field_id(node):
"""Update field ids deeply inside a deep object."""
if isinstance(node, list):
return [_update_field_id(subnode) for subnode in node]
if isinstance(node, dict):
res = {}
for key, value in node.items():
if key in ["field_id", "field"] and isinstance(value, int):
res[key] = id_mapping["database_fields"][value]
else:
res[key] = _update_field_id(value)
return res
return node
for view_decoration in decorations:
view_decoration_copy = view_decoration.copy()
view_decoration_id = view_decoration_copy.pop("id")
# Deeply update field ids to new one in value provider conf
view_decoration_copy["value_provider_conf"] = _update_field_id(
view_decoration_copy["value_provider_conf"]
)
view_decoration_object = ViewDecoration.objects.create(
view=view, **view_decoration_copy
)
id_mapping["database_view_decorations"][
view_decoration_id
] = view_decoration_object.id
return view
def get_visible_fields_and_model(self, view):
"""
Returns the field objects for the provided view. Depending on the view type this
will only return the visible or appropriate fields as different view types can
hide or show fields based on their configuration.
:param view: The view to get the fields for.
:type view: View
:return: An ordered list of field objects for this view and the model for the
view.
:rtype list, Model
"""
model = view.table.get_model()
return model._field_objects.values(), model
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.
:rtype: Serializer
"""
from baserow.contrib.database.api.views.serializers import FieldOptionsField
if not self.field_options_serializer_class:
raise ValueError(
f"The view type {self.type} does not have a field options serializer "
f"class."
)
meta = type(
"Meta",
(),
{"ref_name": self.type + "_view_field_options"},
)
attrs = {
"Meta": meta,
"field_options": FieldOptionsField(
serializer_class=self.field_options_serializer_class,
create_if_missing=create_if_missing,
),
}
return type(
str("Generated" + self.type.capitalize() + "ViewFieldOptionsSerializer"),
(Serializer,),
attrs,
)
def before_field_options_update(self, view, field_options, fields):
"""
Called before the field options are updated related to the provided view.
:param view: The view for which the field options need to be updated.
:type view: View
:param field_options: A dict with the field ids as the key and a dict
containing the values that need to be updated as value.
:type field_options: dict
:param fields: Optionally a list of fields can be provided so that they don't
have to be fetched again.
:return: The updated field options.
:rtype: dict
"""
return field_options
def after_field_type_change(self, field: field_models.Field) -> None:
"""
This hook is called after the type of a field has changed and gives the
possibility to check compatibility with view stuff like specific field options.
:param field: The concerned field.
"""
def prepare_values(self, values: dict, table, user: DjangoUser):
"""
The prepare_values hook gives the possibility to change the provided values
just before they are going to be used to create or update the instance. For
example if an ID is provided, it can be converted to a model instance. Or to
convert a certain date string to a date object.
:param values: The provided values.
:param table: The table where the view is created in.
:type table: Table
:param user: The user on whose behalf the change is made.
:return: The updates values.
:type: dict
"""
return values
def view_created(self, view):
"""
A hook that's called when a new view is created.
:param view: The newly created view instance.
"""
def get_visible_field_options_in_order(self, view):
"""
Should return a queryset of all field options which are visible in the
provided view and in the order they appear in the view.
:param view: The view to query.
:type view: View
:return: A queryset of the views specific view options which are 'visible'
and in order.
"""
raise NotImplementedError(
"An exportable or publicly sharable view must implement "
"`get_visible_field_options_in_order`"
)
def get_hidden_field_options(self, view):
"""
Should return a queryset of all field options which are hidden in the
provided view.
:param view: The view to query.
:type view: View
:return: A queryset of the views specific view options which are 'hidden'.
"""
raise NotImplementedError(
"An exportable or publicly sharable view must implement "
"`get_hidden_field_options`"
)
def get_aggregations(
self, view: "View"
) -> Iterable[Tuple[django_models.Field, str]]:
"""
Should return the aggregation list for the specified view.
returns a list of tuple (Field, aggregation_type)
"""
raise NotImplementedError(
"If the view supports field aggregation it must implement "
"`get_aggregations` method."
)
def after_field_value_update(
self, updated_fields: Union[Iterable[field_models.Field], field_models.Field]
):
"""
Triggered for each field table value modification. This method is generally
called after a row modification, creation, deletion and is called for each
directly or indirectly modified field value. The method can be called multiple
times for an event but with different fields. This hook gives a view type the
opportunity to react on any value change for a field.
:param update_fields: a unique or a list of affected field.
"""
def after_field_update(
self, updated_fields: Union[Iterable[field_models.Field], field_models.Field]
):
"""
Triggered after a field has been updated, created, deleted. This method is
called for each group of field directly or indirectly modified this way.
This hook gives a view type the opportunity to react on any change for a field.
:param update_fields: a unique or a list of modified field.
"""
def after_filter_update(self, view: "View"):
"""
Triggered after a view filter change. This hook gives a view type the
opportunity to react on any filter update.
:param view: the view which filter has changed.
"""
class ViewTypeRegistry(
APIUrlsRegistryMixin, CustomFieldsRegistryMixin, ModelRegistryMixin, Registry
):
"""
With the view type registry it is possible to register new view types. A view type
is an abstraction made specifically for Baserow. If added to the registry a user can
create new views based on this type.
"""
name = "view"
does_not_exist_exception_class = ViewTypeDoesNotExist
already_registered_exception_class = ViewTypeAlreadyRegistered
def get_field_options_serializer_map(self):
return {
view_type.type: view_type.get_field_options_serializer_class(
create_if_missing=False
)
for view_type in self.registry.values()
}
class ViewFilterType(Instance):
"""
This abstract class represents a view filter type that can be added to the view
filter type registry. It must be extended so customisation can be done. Each view
filter type will have its own type names and rules. The get_filter method should
be overwritten and should return a Q object that can be applied to the queryset
later.
Example:
from baserow.contrib.database.views.registry import (
ViewFilterType, view_filter_type_registry
)
class ExampleViewFilterType(ViewFilterType):
type = 'equal'
compatible_field_types = ['text', 'long_text']
def get_filter(self, field_name, value):
return Q(**{
field_name: value
})
view_filter_type_registry.register(ExampleViewFilterType())
"""
compatible_field_types: List[
Union[str, Callable[["field_models.Field"], bool]]
] = []
"""
Defines which field types are compatible with the filter. Only the supported ones
can be used in combination with the field. The values in this list can either be
the literal field_type.type string, or a callable which takes the field being
checked and returns True if compatible or False if not.
"""
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
"""
Should return either a Q object or and AnnotatedQ containing the requested
filtering and annotations based on the provided arguments.
:param field_name: The name of the field that needs to be filtered.
:type field_name: str
:param value: The value that the field must be compared to.
:type value: str
:param model_field: The field extracted from the model.
:type model_field: models.Field
:param field: The instance of the underlying baserow field.
:type field: Field
:return: A Q or AnnotatedQ filter for this specific field, which will be then
later combined with other filters to generate the final total view filter.
"""
raise NotImplementedError("Each must have his own get_filter method.")
def get_preload_values(self, view_filter) -> dict:
"""
Optionally a view filter type can preload certain values for displaying
purposes. This is for example used by the `link_row_has` filter type to
preload the name of the chosen row.
:param view_filter: The view filter instance object where the values must be
preloaded for.
:type view_filter: ViewFilter
:return: The preloaded values as a dict.
"""
return {}
def get_export_serialized_value(self, value) -> str:
"""
This method is called before the filter value is exported. Here it can
optionally be modified.
:param value: The original value.
:type value: str
:return: The updated value.
:rtype: str
"""
return value
def set_import_serialized_value(self, value, id_mapping) -> str:
"""
This method is called before a field is imported. It can optionally be
modified. If the value for example points to a field or select option id, it
can be replaced with the correct value by doing a lookup in the id_mapping.
:param value: The original exported value.
:type value: str
:param id_mapping: The map of exported ids to newly created ids that must be
updated when a new instance has been created.
:type id_mapping: dict
:return: The new value that will be imported.
:rtype: str
"""
return value
def field_is_compatible(self, field):
"""
Given a particular instance of a field returns a list of Type[FieldType] which
are compatible with this particular field type.
Works by checking the field_type against this view filters list of compatible
field types or compatibility checking functions defined in
self.allowed_field_types.
:param field: The field to check.
:return: True if the field is compatible, False otherwise.
"""
from baserow.contrib.database.fields.registries import field_type_registry
field_type = field_type_registry.get_by_model(field.specific_class)
return any(
callable(t) and t(field) or t == field_type.type
for t in self.compatible_field_types
)
class ViewFilterTypeRegistry(Registry):
"""
With the view filter type registry is is possible to register new view filter
types. A view filter type is an abstractions that allows different types of
filtering for rows in a view. It is possible to add multiple view filters to a view
and all the rows must match those filters.
"""
name = "view_filter"
does_not_exist_exception_class = ViewFilterTypeDoesNotExist
already_registered_exception_class = ViewFilterTypeAlreadyRegistered
class ViewAggregationType(Instance):
"""
If you want to aggregate the values of fields in a view, you can use a field
aggregation. For example you can compute a sum of all values of a field in a table.
"""
def get_aggregation(
self,
field_name: str,
model_field: django_models.Field,
field: field_models.Field,
) -> django_models.Aggregate:
"""
Should return the requested django aggregation object based on
the provided arguments.
:param field_name: The name of the field that needs to be aggregated.
:type field_name: str
:param model_field: The field extracted from the model.
:type model_field: django_models.Field
:param field: The instance of the underlying baserow field.
:type field: Field
:return: A django aggregation object for this specific field.
"""
raise NotImplementedError(
"Each aggregation type must have his own get_aggregation method."
)
def field_is_compatible(self, field: field_models.Field) -> bool:
"""
Given a particular instance of a field returns whether the field is supported
by this aggregation type or not.
:param field: The field to check.
:return: True if the field is compatible, False otherwise.
"""
from baserow.contrib.database.fields.registries import field_type_registry
field_type = field_type_registry.get_by_model(field.specific_class)
return any(
callable(t) and t(field) or t == field_type.type
for t in self.compatible_field_types
)
class ViewAggregationTypeRegistry(Registry):
"""
This registry contains all the available field aggregation operators. A field
aggregation allow to summarize all the values for a specific field of a table.
"""
name = "field_aggregation"
does_not_exist_exception_class = AggregationTypeDoesNotExist
already_registered_exception_class = AggregationTypeAlreadyRegistered
# A default view type registry is created here, this is the one that is used
# throughout the whole Baserow application to add a new view type.
view_type_registry = ViewTypeRegistry()
view_filter_type_registry = ViewFilterTypeRegistry()
view_aggregation_type_registry = ViewAggregationTypeRegistry()