mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-17 18:32:35 +00:00
Realtime Public View Sharing
This commit is contained in:
parent
b450ac0393
commit
60eb85da4f
44 changed files with 3012 additions and 142 deletions
backend
src/baserow
config/settings
contrib/database
ws
tests/baserow
contrib/database
migrations
trash
view
ws/public
performance
ws
docs
premium/backend/src/baserow_premium/views
web-frontend
modules
test/helpers
|
@ -378,3 +378,7 @@ WEBHOOKS_REQUEST_TIMEOUT_SECONDS = 5
|
|||
# https://stackoverflow.com/questions/62337379/how-to-append-nginx-ip-to-x-forwarded
|
||||
# -for-in-kubernetes-nginx-ingress-controller
|
||||
# SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS = bool(
|
||||
os.getenv("DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS", "")
|
||||
)
|
||||
|
|
|
@ -190,9 +190,10 @@ class DatabaseConfig(AppConfig):
|
|||
|
||||
application_type_registry.register(DatabaseApplicationType())
|
||||
|
||||
from .ws.pages import TablePageType
|
||||
from .ws.pages import TablePageType, PublicViewPageType
|
||||
|
||||
page_registry.register(TablePageType())
|
||||
page_registry.register(PublicViewPageType())
|
||||
|
||||
from .export.table_exporters.csv_table_exporter import CsvTableExporter
|
||||
|
||||
|
|
|
@ -28,7 +28,13 @@ from .exceptions import (
|
|||
)
|
||||
from .models import Field, SelectOption
|
||||
from .registries import field_type_registry, field_converter_registry
|
||||
from .signals import field_created, field_updated, field_deleted, field_restored
|
||||
from .signals import (
|
||||
field_created,
|
||||
field_updated,
|
||||
field_deleted,
|
||||
field_restored,
|
||||
before_field_deleted,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -491,6 +497,13 @@ class FieldHandler:
|
|||
field_cache=update_collector
|
||||
)
|
||||
|
||||
before_return = before_field_deleted.send(
|
||||
self,
|
||||
field_id=field.id,
|
||||
field=field,
|
||||
user=user,
|
||||
)
|
||||
|
||||
TrashHandler.trash(
|
||||
user,
|
||||
group,
|
||||
|
@ -521,6 +534,7 @@ class FieldHandler:
|
|||
field=field,
|
||||
related_fields=updated_fields,
|
||||
user=user,
|
||||
before_return=before_return,
|
||||
)
|
||||
update_collector.send_additional_field_updated_signals()
|
||||
return updated_fields
|
||||
|
|
|
@ -5,3 +5,4 @@ field_created = Signal()
|
|||
field_restored = Signal()
|
||||
field_updated = Signal()
|
||||
field_deleted = Signal()
|
||||
before_field_deleted = Signal()
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.2.6 on 2022-01-11 11:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("database", "0058_fix_hanging_formula_field_metadata"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="view",
|
||||
name="public",
|
||||
field=models.BooleanField(
|
||||
db_index=True,
|
||||
default=False,
|
||||
help_text="Indicates whether the view is publicly accessible to "
|
||||
"visitors.",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -460,18 +460,25 @@ class RowHandler:
|
|||
except model.DoesNotExist:
|
||||
raise RowDoesNotExist(f"The row with id {row_id} does not exist.")
|
||||
|
||||
updated_fields = []
|
||||
updated_field_ids = set()
|
||||
for field_id, field in model._field_objects.items():
|
||||
if field_id in values or field["name"] in values:
|
||||
updated_field_ids.add(field_id)
|
||||
updated_fields.append(field["field"])
|
||||
|
||||
before_return = before_row_update.send(
|
||||
self, row=row, user=user, table=table, model=model
|
||||
self,
|
||||
row=row,
|
||||
user=user,
|
||||
table=table,
|
||||
model=model,
|
||||
updated_field_ids=updated_field_ids,
|
||||
)
|
||||
if user_field_names:
|
||||
values = self.map_user_field_name_dict_to_internal(
|
||||
model._field_objects, values
|
||||
)
|
||||
updated_fields = [
|
||||
field["field"]
|
||||
for field_id, field in model._field_objects.items()
|
||||
if field_id in values or field["name"] in values
|
||||
]
|
||||
values = self.prepare_values(model._field_objects, values)
|
||||
values, manytomany_values = self.extract_manytomany_values(values, model)
|
||||
|
||||
|
@ -508,6 +515,7 @@ class RowHandler:
|
|||
table=table,
|
||||
model=model,
|
||||
before_return=before_return,
|
||||
updated_field_ids=updated_field_ids,
|
||||
)
|
||||
|
||||
return row
|
||||
|
@ -544,7 +552,7 @@ class RowHandler:
|
|||
raise RowDoesNotExist(f"The row with id {row_id} does not exist.")
|
||||
|
||||
before_return = before_row_update.send(
|
||||
self, row=row, user=user, table=table, model=model
|
||||
self, row=row, user=user, table=table, model=model, updated_field_ids=[]
|
||||
)
|
||||
|
||||
row.order = self.get_order_before_row(before, model)
|
||||
|
@ -574,6 +582,7 @@ class RowHandler:
|
|||
table=table,
|
||||
model=model,
|
||||
before_return=before_return,
|
||||
updated_field_ids=[],
|
||||
)
|
||||
|
||||
return row
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from typing import Dict, Any, List, Optional, Iterable
|
||||
|
||||
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||
from django.db.models import F
|
||||
|
||||
|
@ -44,6 +48,7 @@ from .signals import (
|
|||
view_field_options_updated,
|
||||
)
|
||||
from .validators import EMPTY_VALUES
|
||||
from ..table.models import Table, GeneratedTableModel
|
||||
|
||||
|
||||
class ViewHandler:
|
||||
|
@ -921,3 +926,147 @@ class ViewHandler:
|
|||
)
|
||||
|
||||
return instance
|
||||
|
||||
def get_public_views_row_checker(self, table, model, updated_field_ids=None):
|
||||
"""
|
||||
Returns a CachingPublicViewRowChecker object which will have precalculated
|
||||
information about the public views in the provided table to aid with quickly
|
||||
checking which views a row in that table is visible in. If you will be updating
|
||||
the row and reusing the checker you must provide an iterable of the field ids
|
||||
that you will be updating in the row, otherwise the checker will cache the
|
||||
first check per view/row.
|
||||
|
||||
:param table: The table the row is in.
|
||||
:param model: The model of the table including all fields.
|
||||
:param updated_field_ids: An optional iterable of field ids which will be
|
||||
updated on rows passed to the checker. If the checker is used on the same
|
||||
row multiple times and that row has been updated it will return invalid
|
||||
results unless you have correctly populated this argument.
|
||||
:return: A list of non-specific public view instances.
|
||||
"""
|
||||
|
||||
return CachingPublicViewRowChecker(table, model, updated_field_ids)
|
||||
|
||||
def restrict_row_for_view(
|
||||
self, view: View, serialized_row: Dict[str, Any]
|
||||
) -> Dict[Any, Any]:
|
||||
"""
|
||||
Removes any fields which are hidden in the view from the provided serialized
|
||||
row ensuring no data is leaked according to the views field options.
|
||||
|
||||
:param view: The view to restrict the row by.
|
||||
:param serialized_row: A python dictionary which is the result of serializing
|
||||
the row containing `field_XXX` keys per field value. It must not be a
|
||||
serialized using user_field_names=True.
|
||||
:return: A copy of the serialized_row with all hidden fields removed.
|
||||
"""
|
||||
return self.restrict_rows_for_view(view, [serialized_row])[0]
|
||||
|
||||
def restrict_rows_for_view(
|
||||
self, view: View, serialized_rows: List[Dict[str, Any]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Removes any fields which are hidden in the view from the provided serialized
|
||||
row ensuring no data is leaked according to the views field options.
|
||||
|
||||
:param view: The view to restrict the row by.
|
||||
:param serialized_rows: A list of python dictionaries which are the result of
|
||||
serializing the rows containing `field_XXX` keys per field value. They
|
||||
must not be serialized using user_field_names=True.
|
||||
:return: A copy of the serialized_row with all hidden fields removed.
|
||||
"""
|
||||
|
||||
view_type = view_type_registry.get_by_model(view.specific_class)
|
||||
hidden_field_options = view_type.get_hidden_field_options(view)
|
||||
restricted_rows = []
|
||||
for serialized_row in serialized_rows:
|
||||
row_copy = deepcopy(serialized_row)
|
||||
for hidden_field_option in hidden_field_options:
|
||||
row_copy.pop(f"field_{hidden_field_option.field_id}", None)
|
||||
restricted_rows.append(row_copy)
|
||||
return restricted_rows
|
||||
|
||||
|
||||
class CachingPublicViewRowChecker:
|
||||
"""
|
||||
A helper class to check which public views a row is visible in. Will pre-calculate
|
||||
upfront for a specific table which public views are always visible, which public
|
||||
views can have row check results cached for and finally will pre-construct and
|
||||
reuse querysets for performance reasons.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
table: Table,
|
||||
model: GeneratedTableModel,
|
||||
updated_field_ids: Optional[Iterable[int]] = None,
|
||||
):
|
||||
self._public_views = (
|
||||
table.view_set.filter(public=True).prefetch_related("viewfilter_set").all()
|
||||
)
|
||||
self._updated_field_ids = updated_field_ids
|
||||
self._views_with_filters = []
|
||||
self._always_visible_views = []
|
||||
self._view_row_check_cache = defaultdict(dict)
|
||||
handler = ViewHandler()
|
||||
for view in self._public_views:
|
||||
if len(view.viewfilter_set.all()) == 0:
|
||||
# If there are no view filters for this view then any row must always
|
||||
# be visible in this view
|
||||
self._always_visible_views.append(view)
|
||||
else:
|
||||
filter_qs = handler.apply_filters(view, model.objects)
|
||||
self._views_with_filters.append(
|
||||
(
|
||||
view,
|
||||
filter_qs,
|
||||
self._view_row_checks_can_be_cached(view),
|
||||
)
|
||||
)
|
||||
|
||||
def get_public_views_where_row_is_visible(self, row):
|
||||
"""
|
||||
WARNING: If you are reusing the same checker and calling this method with the
|
||||
same row multiple times you must have correctly set which fields in the row
|
||||
might be updated in the checkers initials `updated_field_ids` attribute. This
|
||||
is because for a given view, if we know none of the fields it filters on
|
||||
will be updated we can cache the first check of if that row exists as any
|
||||
further changes to the row wont be affecting filtered fields. Hence
|
||||
`updated_field_ids` needs to be set if you are ever changing the row and
|
||||
reusing the same CachingPublicViewRowChecker instance.
|
||||
|
||||
:param row: A row in the checkers table.
|
||||
:return: A list of views where the row is visible for this checkers table.
|
||||
"""
|
||||
views = []
|
||||
for view, filter_qs, can_use_cache in self._views_with_filters:
|
||||
if can_use_cache:
|
||||
if row.id not in self._view_row_check_cache[view.id]:
|
||||
self._view_row_check_cache[view.id][
|
||||
row.id
|
||||
] = self._check_row_visible(filter_qs, row)
|
||||
if self._view_row_check_cache[view.id][row.id]:
|
||||
views.append(view)
|
||||
elif self._check_row_visible(filter_qs, row):
|
||||
views.append(view)
|
||||
|
||||
return views + self._always_visible_views
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def _check_row_visible(self, filter_qs, row):
|
||||
return filter_qs.filter(id=row.id).exists()
|
||||
|
||||
def _view_row_checks_can_be_cached(self, view):
|
||||
if self._updated_field_ids is None:
|
||||
return True
|
||||
for view_filter in view.viewfilter_set.all():
|
||||
if view_filter.field_id in self._updated_field_ids:
|
||||
# We found a view filter for a field which will be updated hence we
|
||||
# need to check both before and after a row update occurs
|
||||
return False
|
||||
# Every single updated field does not have a filter on it, hence
|
||||
# we only need to check if a given row is visible in the view once
|
||||
# as any changes to the fields in said row wont be for fields with
|
||||
# filters and so the result of the first check will be still
|
||||
# valid for any subsequent checks.
|
||||
return True
|
||||
|
|
|
@ -80,6 +80,7 @@ class View(
|
|||
public = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Indicates whether the view is publicly accessible to visitors.",
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
def rotate_slug(self):
|
||||
|
@ -93,22 +94,21 @@ class View(
|
|||
queryset = View.objects.filter(table=table)
|
||||
return cls.get_highest_order_of_queryset(queryset) + 1
|
||||
|
||||
def get_field_options(self, create_if_not_exists=False, fields=None):
|
||||
def get_field_options(self, create_if_missing=False, fields=None):
|
||||
"""
|
||||
Each field can have unique options per view. This method returns those
|
||||
options per field type and can optionally create the missing ones. This method
|
||||
only works if the `field_options` property is a ManyToManyField with a relation
|
||||
to a field options model.
|
||||
|
||||
:param create_if_not_exists: If true the missing GridViewFieldOptions are
|
||||
:param create_if_missing: If true the missing GridViewFieldOptions are
|
||||
going to be created. If a fields has been created at a later moment it
|
||||
could be possible that they don't exist yet. If this value is True, the
|
||||
missing relationships are created in that case.
|
||||
:type create_if_not_exists: bool
|
||||
:type create_if_missing: bool
|
||||
:param fields: If all the fields related to the table of this grid view have
|
||||
already been fetched, they can be provided here to avoid having to fetch
|
||||
them for a second time. This is only needed if `create_if_not_exists` is
|
||||
True.
|
||||
them for a second time. This is only needed if `create_if_missing` is True.
|
||||
:type fields: list
|
||||
:return: A queryset containing all the field options of view.
|
||||
:rtype: QuerySet
|
||||
|
@ -135,7 +135,7 @@ class View(
|
|||
|
||||
field_options = get_queryset()
|
||||
|
||||
if create_if_not_exists:
|
||||
if create_if_missing:
|
||||
fields_queryset = Field.objects.filter(table_id=self.table.id)
|
||||
|
||||
if fields is None:
|
||||
|
|
|
@ -338,6 +338,35 @@ class ViewType(
|
|||
: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 this"
|
||||
)
|
||||
|
||||
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 this"
|
||||
)
|
||||
|
||||
|
||||
class ViewTypeRegistry(
|
||||
APIUrlsRegistryMixin, CustomFieldsRegistryMixin, ModelRegistryMixin, Registry
|
||||
|
|
|
@ -118,11 +118,14 @@ class GridViewType(ViewType):
|
|||
|
||||
def get_visible_field_options_in_order(self, grid_view):
|
||||
return (
|
||||
grid_view.get_field_options(create_if_not_exists=True)
|
||||
grid_view.get_field_options(create_if_missing=True)
|
||||
.filter(hidden=False)
|
||||
.order_by("-field__primary", "order", "field__id")
|
||||
)
|
||||
|
||||
def get_hidden_field_options(self, grid_view):
|
||||
return grid_view.get_field_options(create_if_missing=False).filter(hidden=True)
|
||||
|
||||
|
||||
class GalleryViewType(ViewType):
|
||||
type = "gallery"
|
||||
|
@ -231,7 +234,7 @@ class GalleryViewType(ViewType):
|
|||
visible.
|
||||
"""
|
||||
|
||||
field_options = view.get_field_options(create_if_not_exists=True).order_by(
|
||||
field_options = view.get_field_options(create_if_missing=True).order_by(
|
||||
"field__id"
|
||||
)
|
||||
ids_to_update = [f.id for f in field_options[0:3]]
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
from django.dispatch import receiver
|
||||
from typing import Iterable, Optional, Type
|
||||
|
||||
from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from baserow.ws.registries import page_registry
|
||||
|
||||
from baserow.contrib.database.fields import signals as field_signals
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.contrib.database.api.fields.serializers import FieldSerializer
|
||||
from baserow.contrib.database.fields import signals as field_signals
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.ws.registries import page_registry
|
||||
|
||||
|
||||
@receiver(field_signals.field_created)
|
||||
|
@ -13,16 +16,10 @@ def field_created(sender, field, related_fields, user, **kwargs):
|
|||
table_page_type = page_registry.get("table")
|
||||
transaction.on_commit(
|
||||
lambda: table_page_type.broadcast(
|
||||
{
|
||||
"type": "field_created",
|
||||
"field": field_type_registry.get_serializer(
|
||||
field, FieldSerializer
|
||||
).data,
|
||||
"related_fields": [
|
||||
field_type_registry.get_serializer(f, FieldSerializer).data
|
||||
for f in related_fields
|
||||
],
|
||||
},
|
||||
RealtimeFieldMessages.field_created(
|
||||
field,
|
||||
related_fields,
|
||||
),
|
||||
getattr(user, "web_socket_id", None),
|
||||
table_id=field.table_id,
|
||||
)
|
||||
|
@ -34,16 +31,10 @@ def field_restored(sender, field, related_fields, user, **kwargs):
|
|||
table_page_type = page_registry.get("table")
|
||||
transaction.on_commit(
|
||||
lambda: table_page_type.broadcast(
|
||||
{
|
||||
"type": "field_restored",
|
||||
"field": field_type_registry.get_serializer(
|
||||
field, FieldSerializer
|
||||
).data,
|
||||
"related_fields": [
|
||||
field_type_registry.get_serializer(f, FieldSerializer).data
|
||||
for f in related_fields
|
||||
],
|
||||
},
|
||||
RealtimeFieldMessages.field_restored(
|
||||
field,
|
||||
related_fields,
|
||||
),
|
||||
getattr(user, "web_socket_id", None),
|
||||
table_id=field.table_id,
|
||||
)
|
||||
|
@ -55,17 +46,10 @@ def field_updated(sender, field, related_fields, user, **kwargs):
|
|||
table_page_type = page_registry.get("table")
|
||||
transaction.on_commit(
|
||||
lambda: table_page_type.broadcast(
|
||||
{
|
||||
"type": "field_updated",
|
||||
"field_id": field.id,
|
||||
"field": field_type_registry.get_serializer(
|
||||
field, FieldSerializer
|
||||
).data,
|
||||
"related_fields": [
|
||||
field_type_registry.get_serializer(f, FieldSerializer).data
|
||||
for f in related_fields
|
||||
],
|
||||
},
|
||||
RealtimeFieldMessages.field_updated(
|
||||
field,
|
||||
related_fields,
|
||||
),
|
||||
getattr(user, "web_socket_id", None),
|
||||
table_id=field.table_id,
|
||||
)
|
||||
|
@ -73,20 +57,110 @@ def field_updated(sender, field, related_fields, user, **kwargs):
|
|||
|
||||
|
||||
@receiver(field_signals.field_deleted)
|
||||
def field_deleted(sender, field_id, field, related_fields, user, **kwargs):
|
||||
def field_deleted(
|
||||
sender, field_id, field, related_fields, user, before_return, **kwargs
|
||||
):
|
||||
table_page_type = page_registry.get("table")
|
||||
transaction.on_commit(
|
||||
lambda: table_page_type.broadcast(
|
||||
{
|
||||
"type": "field_deleted",
|
||||
"table_id": field.table_id,
|
||||
"field_id": field_id,
|
||||
"related_fields": [
|
||||
field_type_registry.get_serializer(f, FieldSerializer).data
|
||||
for f in related_fields
|
||||
],
|
||||
},
|
||||
RealtimeFieldMessages.field_deleted(
|
||||
field.table_id,
|
||||
field_id,
|
||||
related_fields,
|
||||
),
|
||||
getattr(user, "web_socket_id", None),
|
||||
table_id=field.table_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RealtimeFieldMessages:
|
||||
"""
|
||||
A collection of functions which construct the payloads for the realtime
|
||||
websocket messages related to fields.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def serialize_field_for_websockets(
|
||||
field: Field, field_serializer_class: Optional[Type[Serializer]] = None
|
||||
):
|
||||
if field_serializer_class is None:
|
||||
field_serializer_class = FieldSerializer
|
||||
return field_type_registry.get_serializer(field, field_serializer_class).data
|
||||
|
||||
@staticmethod
|
||||
def serialize_fields_for_websockets(
|
||||
fields: Iterable[Field],
|
||||
field_serializer_class: Optional[Type[Serializer]] = None,
|
||||
):
|
||||
if field_serializer_class is None:
|
||||
field_serializer_class = FieldSerializer
|
||||
return [
|
||||
field_type_registry.get_serializer(f, field_serializer_class).data
|
||||
for f in fields
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def field_created(
|
||||
field: Field,
|
||||
related_fields: Iterable[Field],
|
||||
field_serializer_class: Optional[Type[Serializer]] = None,
|
||||
):
|
||||
return {
|
||||
"type": "field_created",
|
||||
"field": RealtimeFieldMessages.serialize_field_for_websockets(
|
||||
field, field_serializer_class=field_serializer_class
|
||||
),
|
||||
"related_fields": RealtimeFieldMessages.serialize_fields_for_websockets(
|
||||
related_fields, field_serializer_class=field_serializer_class
|
||||
),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def field_restored(
|
||||
field: Field,
|
||||
related_fields: Iterable[Field],
|
||||
field_serializer_class: Optional[Type[Serializer]] = None,
|
||||
):
|
||||
return {
|
||||
"type": "field_restored",
|
||||
"field": RealtimeFieldMessages.serialize_field_for_websockets(
|
||||
field, field_serializer_class=field_serializer_class
|
||||
),
|
||||
"related_fields": RealtimeFieldMessages.serialize_fields_for_websockets(
|
||||
related_fields, field_serializer_class=field_serializer_class
|
||||
),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def field_updated(
|
||||
field: Field,
|
||||
related_fields: Iterable[Field],
|
||||
field_serializer_class: Optional[Type[Serializer]] = None,
|
||||
):
|
||||
return {
|
||||
"type": "field_updated",
|
||||
"field_id": field.id,
|
||||
"field": RealtimeFieldMessages.serialize_field_for_websockets(
|
||||
field, field_serializer_class=field_serializer_class
|
||||
),
|
||||
"related_fields": RealtimeFieldMessages.serialize_fields_for_websockets(
|
||||
related_fields, field_serializer_class=field_serializer_class
|
||||
),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def field_deleted(
|
||||
table_id: int,
|
||||
field_id: int,
|
||||
related_fields: Iterable[Field],
|
||||
field_serializer_class: Optional[Type[Serializer]] = None,
|
||||
):
|
||||
return {
|
||||
"type": "field_deleted",
|
||||
"table_id": table_id,
|
||||
"field_id": field_id,
|
||||
"related_fields": RealtimeFieldMessages.serialize_fields_for_websockets(
|
||||
related_fields, field_serializer_class=field_serializer_class
|
||||
),
|
||||
}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
from django.conf import settings
|
||||
from rest_framework.exceptions import NotAuthenticated
|
||||
|
||||
from baserow.contrib.database.views.exceptions import ViewDoesNotExist
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
from baserow.ws.registries import PageType
|
||||
|
||||
from baserow.core.exceptions import UserNotInGroup
|
||||
|
@ -22,10 +27,42 @@ class TablePageType(PageType):
|
|||
handler = TableHandler()
|
||||
table = handler.get_table(table_id)
|
||||
table.database.group.has_user(user, raise_error=True)
|
||||
except (UserNotInGroup, TableDoesNotExist):
|
||||
except (UserNotInGroup, TableDoesNotExist, NotAuthenticated):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_group_name(self, table_id, **kwargs):
|
||||
return f"table-{table_id}"
|
||||
|
||||
|
||||
class PublicViewPageType(PageType):
|
||||
type = "view"
|
||||
parameters = ["slug"]
|
||||
|
||||
def can_add(self, user, web_socket_id, slug, **kwargs):
|
||||
"""
|
||||
The user should only have access to this page if the view exists and it is
|
||||
public or they have access to the group.
|
||||
"""
|
||||
|
||||
if settings.DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS:
|
||||
return False
|
||||
|
||||
if not slug:
|
||||
return False
|
||||
|
||||
try:
|
||||
handler = ViewHandler()
|
||||
handler.get_public_view_by_slug(user, slug)
|
||||
except ViewDoesNotExist:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_group_name(self, slug, **kwargs):
|
||||
return f"view-{slug}"
|
||||
|
||||
def broadcast_to_views(self, payload, view_slugs):
|
||||
for view_slug in view_slugs:
|
||||
self.broadcast(payload, ignore_web_socket_id=None, slug=view_slug)
|
||||
|
|
148
backend/src/baserow/contrib/database/ws/public/fields/signals.py
Normal file
148
backend/src/baserow/contrib/database/ws/public/fields/signals.py
Normal file
|
@ -0,0 +1,148 @@
|
|||
from typing import List, Tuple, Set, Optional, Dict, Any, Iterable
|
||||
|
||||
from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
|
||||
from baserow.contrib.database.api.constants import PUBLIC_PLACEHOLDER_ENTITY_ID
|
||||
from baserow.contrib.database.api.views.grid.serializers import PublicFieldSerializer
|
||||
from baserow.contrib.database.fields import signals as field_signals
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
from baserow.contrib.database.views.models import View
|
||||
from baserow.contrib.database.views.registries import view_type_registry
|
||||
from baserow.contrib.database.ws.fields.signals import RealtimeFieldMessages
|
||||
from baserow.ws.registries import page_registry
|
||||
|
||||
|
||||
def _broadcast_payload_to_views_with_restricted_related_fields(
|
||||
payload: Dict[str, Any],
|
||||
serialized_related_fields: List[Dict[str, Any]],
|
||||
views_with_hidden_fields: List[Tuple[View, Set[int]]],
|
||||
):
|
||||
view_page_type = page_registry.get("view")
|
||||
for view, hidden_fields in views_with_hidden_fields:
|
||||
payload["related_fields"] = [
|
||||
f for f in serialized_related_fields if f["id"] not in hidden_fields
|
||||
]
|
||||
view_page_type.broadcast(
|
||||
payload,
|
||||
None,
|
||||
slug=view.slug,
|
||||
)
|
||||
|
||||
|
||||
def _send_payload_to_public_views_where_field_not_hidden(
|
||||
field: Field, payload: Dict[str, Any]
|
||||
):
|
||||
related_fields = payload.pop("related_fields", [])
|
||||
related_field_ids = [f["id"] for f in related_fields]
|
||||
|
||||
views_with_hidden_fields = _get_views_where_field_visible_and_hidden_fields_in_view(
|
||||
field,
|
||||
# Only bother calculating the hidden_fields set for the related_fields
|
||||
hidden_fields_field_ids_filter=related_field_ids,
|
||||
)
|
||||
_broadcast_payload_to_views_with_restricted_related_fields(
|
||||
payload, related_fields, views_with_hidden_fields
|
||||
)
|
||||
|
||||
|
||||
def _get_views_where_field_visible_and_hidden_fields_in_view(
|
||||
field: Field,
|
||||
hidden_fields_field_ids_filter: Optional[Iterable[int]] = None,
|
||||
) -> List[Tuple[View, Set[int]]]:
|
||||
"""
|
||||
Finds all views where field is visible and also attaches the set of fields which
|
||||
are hidden in said view.
|
||||
|
||||
:param field: All views where this field is visible will be returned.
|
||||
:param hidden_fields_field_ids_filter: An optional filter which restricts the
|
||||
calculation of whether a field is hidden or not in a returned view down to just
|
||||
checking the fields in this iterable.
|
||||
:return: A list of tuples where the first value is a view where field is visible
|
||||
and the second is the set of field ids which are hidden in said view.
|
||||
"""
|
||||
|
||||
views_where_field_was_visible = []
|
||||
for view in field.table.view_set.filter(public=True):
|
||||
view = view.specific
|
||||
view_type = view_type_registry.get_by_model(view)
|
||||
|
||||
hidden_field_options_qs = view_type.get_hidden_field_options(view)
|
||||
if hidden_fields_field_ids_filter is not None:
|
||||
hidden_field_options_qs = hidden_field_options_qs.filter(
|
||||
field_id__in=[field.id, *hidden_fields_field_ids_filter]
|
||||
)
|
||||
hidden_fields = set(hidden_field_options_qs.values_list("field_id", flat=True))
|
||||
|
||||
if field.id not in hidden_fields:
|
||||
views_where_field_was_visible.append((view, hidden_fields))
|
||||
return views_where_field_was_visible
|
||||
|
||||
|
||||
@receiver(field_signals.field_created)
|
||||
def public_field_created(sender, field, related_fields, user, **kwargs):
|
||||
transaction.on_commit(
|
||||
lambda: _send_payload_to_public_views_where_field_not_hidden(
|
||||
field,
|
||||
RealtimeFieldMessages.field_created(
|
||||
field, related_fields, field_serializer_class=PublicFieldSerializer
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(field_signals.field_restored)
|
||||
def public_field_restored(sender, field, related_fields, user, **kwargs):
|
||||
transaction.on_commit(
|
||||
lambda: _send_payload_to_public_views_where_field_not_hidden(
|
||||
field,
|
||||
RealtimeFieldMessages.field_restored(
|
||||
field, related_fields, field_serializer_class=PublicFieldSerializer
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(field_signals.field_updated)
|
||||
def public_field_updated(sender, field, related_fields, user, **kwargs):
|
||||
transaction.on_commit(
|
||||
lambda: _send_payload_to_public_views_where_field_not_hidden(
|
||||
field,
|
||||
RealtimeFieldMessages.field_updated(
|
||||
field, related_fields, field_serializer_class=PublicFieldSerializer
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(field_signals.before_field_deleted)
|
||||
def public_before_field_deleted(sender, field_id, field, user, **kwargs):
|
||||
# We have to check where the field is visible before it is deleted.
|
||||
return _get_views_where_field_visible_and_hidden_fields_in_view(
|
||||
field,
|
||||
# We don't know yet which fields will be related_fields so calculate the
|
||||
# hidden_fields set for all fields in the view as any could potentially be
|
||||
# a related_field.
|
||||
hidden_fields_field_ids_filter=None,
|
||||
)
|
||||
|
||||
|
||||
@receiver(field_signals.field_deleted)
|
||||
def public_field_deleted(
|
||||
sender, field_id, field, related_fields, user, before_return, **kwargs
|
||||
):
|
||||
def send_deleted():
|
||||
views = dict(before_return)[public_before_field_deleted]
|
||||
payload = RealtimeFieldMessages.field_deleted(
|
||||
PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
field_id,
|
||||
related_fields,
|
||||
field_serializer_class=PublicFieldSerializer,
|
||||
)
|
||||
serialized_related_fields = payload.pop("related_fields", [])
|
||||
|
||||
_broadcast_payload_to_views_with_restricted_related_fields(
|
||||
payload, serialized_related_fields, views
|
||||
)
|
||||
|
||||
transaction.on_commit(send_deleted)
|
182
backend/src/baserow/contrib/database/ws/public/rows/signals.py
Normal file
182
backend/src/baserow/contrib/database/ws/public/rows/signals.py
Normal file
|
@ -0,0 +1,182 @@
|
|||
from typing import Optional, Any, Dict, Iterable
|
||||
|
||||
from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
|
||||
from baserow.contrib.database.api.constants import PUBLIC_PLACEHOLDER_ENTITY_ID
|
||||
from baserow.contrib.database.api.rows.serializers import (
|
||||
get_row_serializer_class,
|
||||
RowSerializer,
|
||||
)
|
||||
from baserow.contrib.database.rows import signals as row_signals
|
||||
from baserow.contrib.database.table.models import GeneratedTableModel
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
from baserow.contrib.database.views.models import View
|
||||
from baserow.contrib.database.ws.rows.signals import RealtimeRowMessages
|
||||
from baserow.ws.registries import page_registry
|
||||
|
||||
|
||||
def _serialize_row(model, row):
|
||||
return get_row_serializer_class(model, RowSerializer, is_response=True)(row).data
|
||||
|
||||
|
||||
def _send_row_created_event_to_views(
|
||||
serialized_row: Dict[Any, Any],
|
||||
before: Optional[GeneratedTableModel],
|
||||
public_views: Iterable[View],
|
||||
):
|
||||
view_page_type = page_registry.get("view")
|
||||
handler = ViewHandler()
|
||||
for public_view in public_views:
|
||||
restricted_serialized_row = handler.restrict_row_for_view(
|
||||
public_view, serialized_row
|
||||
)
|
||||
view_page_type.broadcast(
|
||||
RealtimeRowMessages.row_created(
|
||||
table_id=PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
serialized_row=restricted_serialized_row,
|
||||
metadata={},
|
||||
before=before,
|
||||
),
|
||||
slug=public_view.slug,
|
||||
)
|
||||
|
||||
|
||||
def _send_row_deleted_event_to_views(
|
||||
serialized_deleted_row: Dict[Any, Any], public_views: Iterable[View]
|
||||
):
|
||||
view_page_type = page_registry.get("view")
|
||||
handler = ViewHandler()
|
||||
for public_view in public_views:
|
||||
restricted_serialized_deleted_row = handler.restrict_row_for_view(
|
||||
public_view, serialized_deleted_row
|
||||
)
|
||||
view_page_type.broadcast(
|
||||
RealtimeRowMessages.row_deleted(
|
||||
table_id=PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
serialized_row=restricted_serialized_deleted_row,
|
||||
),
|
||||
slug=public_view.slug,
|
||||
)
|
||||
|
||||
|
||||
@receiver(row_signals.row_created)
|
||||
def public_row_created(sender, row, before, user, table, model, **kwargs):
|
||||
row_checker = ViewHandler().get_public_views_row_checker(table, model)
|
||||
transaction.on_commit(
|
||||
lambda: _send_row_created_event_to_views(
|
||||
_serialize_row(model, row),
|
||||
before,
|
||||
row_checker.get_public_views_where_row_is_visible(row),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@receiver(row_signals.before_row_delete)
|
||||
def public_before_row_delete(sender, row, user, table, model, **kwargs):
|
||||
row_checker = ViewHandler().get_public_views_row_checker(table, model)
|
||||
return {
|
||||
"deleted_row_public_views": (
|
||||
row_checker.get_public_views_where_row_is_visible(row)
|
||||
),
|
||||
"deleted_row": _serialize_row(model, row),
|
||||
}
|
||||
|
||||
|
||||
@receiver(row_signals.row_deleted)
|
||||
def public_row_deleted(
|
||||
sender, row_id, row, user, table, model, before_return, **kwargs
|
||||
):
|
||||
public_views = dict(before_return)[public_before_row_delete][
|
||||
"deleted_row_public_views"
|
||||
]
|
||||
serialized_deleted_row = dict(before_return)[public_before_row_delete][
|
||||
"deleted_row"
|
||||
]
|
||||
transaction.on_commit(
|
||||
lambda: _send_row_deleted_event_to_views(serialized_deleted_row, public_views)
|
||||
)
|
||||
|
||||
|
||||
@receiver(row_signals.before_row_update)
|
||||
def public_before_row_update(
|
||||
sender, row, user, table, model, updated_field_ids, **kwargs
|
||||
):
|
||||
# Generate a serialized version of the row before it is updated. The
|
||||
# `row_updated` receiver needs this serialized version because it can't serialize
|
||||
# the old row after it has been updated.
|
||||
row_checker = ViewHandler().get_public_views_row_checker(
|
||||
table, model, updated_field_ids=updated_field_ids
|
||||
)
|
||||
return {
|
||||
"old_row": _serialize_row(model, row),
|
||||
"old_row_public_views": row_checker.get_public_views_where_row_is_visible(row),
|
||||
"caching_row_checker": row_checker,
|
||||
}
|
||||
|
||||
|
||||
@receiver(row_signals.row_updated)
|
||||
def public_row_updated(
|
||||
sender, row, user, table, model, before_return, updated_field_ids, **kwargs
|
||||
):
|
||||
before_return_dict = dict(before_return)[public_before_row_update]
|
||||
serialized_old_row = before_return_dict["old_row"]
|
||||
serialized_updated_row = _serialize_row(model, row)
|
||||
|
||||
old_row_public_views = before_return_dict["old_row_public_views"]
|
||||
existing_checker = before_return_dict["caching_row_checker"]
|
||||
views = existing_checker.get_public_views_where_row_is_visible(row)
|
||||
updated_row_public_views = {view.slug: view for view in views}
|
||||
|
||||
# When a row is updated from the point of view of a public view it might not always
|
||||
# result in a `row_updated` event. For example if the row was previously not visible
|
||||
# in the public view due to its filters, but the row update makes it now match
|
||||
# the filters we want to send a `row_created` event to that views page as the
|
||||
# clients won't know anything about the row and hence a `row_updated` event makes
|
||||
# no sense for them.
|
||||
|
||||
public_views_where_row_was_deleted = []
|
||||
public_views_where_row_was_updated = []
|
||||
for old_row_view in old_row_public_views:
|
||||
updated_row_view = updated_row_public_views.pop(old_row_view.slug, None)
|
||||
if updated_row_view is None:
|
||||
# The updated row is no longer visible in `old_row_view` hence we should
|
||||
# send that view a deleted event.
|
||||
public_views_where_row_was_deleted.append(old_row_view)
|
||||
else:
|
||||
# The updated row is still visible so here we want a normal updated event.
|
||||
public_views_where_row_was_updated.append(old_row_view)
|
||||
# Any remaining views in the updated_row_public_views dict are views which
|
||||
# previously didn't show the old row, but now show the new row, so we want created.
|
||||
public_views_where_row_was_created = updated_row_public_views.values()
|
||||
|
||||
def _send_created_updated_deleted_row_signals_to_views():
|
||||
_send_row_deleted_event_to_views(
|
||||
serialized_old_row, public_views_where_row_was_deleted
|
||||
)
|
||||
_send_row_created_event_to_views(
|
||||
serialized_updated_row,
|
||||
before=None,
|
||||
public_views=public_views_where_row_was_created,
|
||||
)
|
||||
|
||||
view_page_type = page_registry.get("view")
|
||||
handler = ViewHandler()
|
||||
for public_view in public_views_where_row_was_updated:
|
||||
(
|
||||
visible_fields_only_updated_row,
|
||||
visible_fields_only_old_row,
|
||||
) = handler.restrict_rows_for_view(
|
||||
public_view, [serialized_updated_row, serialized_old_row]
|
||||
)
|
||||
view_page_type.broadcast(
|
||||
RealtimeRowMessages.row_updated(
|
||||
table_id=PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
serialized_row_before_update=visible_fields_only_old_row,
|
||||
serialized_row=visible_fields_only_updated_row,
|
||||
metadata={},
|
||||
),
|
||||
slug=public_view.slug,
|
||||
)
|
||||
|
||||
transaction.on_commit(_send_created_updated_deleted_row_signals_to_views)
|
|
@ -0,0 +1,58 @@
|
|||
from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
|
||||
from baserow.contrib.database.api.views.grid.serializers import PublicFieldSerializer
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.contrib.database.views import signals as view_signals
|
||||
from baserow.contrib.database.views.registries import view_type_registry
|
||||
from baserow.ws.registries import page_registry
|
||||
|
||||
|
||||
def _send_force_rows_refresh_if_view_public(view):
|
||||
view_page_type = page_registry.get("view")
|
||||
if view.public:
|
||||
transaction.on_commit(
|
||||
lambda: view_page_type.broadcast(
|
||||
{"type": "force_view_rows_refresh", "view_id": view.slug},
|
||||
None,
|
||||
slug=view.slug,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(view_signals.view_updated)
|
||||
def public_view_updated(sender, view, user, **kwargs):
|
||||
_send_force_rows_refresh_if_view_public(view)
|
||||
|
||||
|
||||
@receiver(view_signals.view_filter_created)
|
||||
def public_view_filter_created(sender, view_filter, user, **kwargs):
|
||||
_send_force_rows_refresh_if_view_public(view_filter.view)
|
||||
|
||||
|
||||
@receiver(view_signals.view_filter_updated)
|
||||
def public_view_filter_updated(sender, view_filter, user, **kwargs):
|
||||
_send_force_rows_refresh_if_view_public(view_filter.view)
|
||||
|
||||
|
||||
@receiver(view_signals.view_filter_deleted)
|
||||
def public_view_filter_deleted(sender, view_filter_id, view_filter, user, **kwargs):
|
||||
_send_force_rows_refresh_if_view_public(view_filter.view)
|
||||
|
||||
|
||||
@receiver(view_signals.view_field_options_updated)
|
||||
def public_view_field_options_updated(sender, view, user, **kwargs):
|
||||
if view.public:
|
||||
view = view.specific
|
||||
view_page_type = page_registry.get("view")
|
||||
view_type = view_type_registry.get_by_model(view)
|
||||
field_options = view_type.get_visible_field_options_in_order(view)
|
||||
fields = [
|
||||
field_type_registry.get_serializer(o.field, PublicFieldSerializer).data
|
||||
for o in field_options.select_related("field")
|
||||
]
|
||||
view_page_type.broadcast(
|
||||
{"type": "force_view_refresh", "view_id": view.slug, "fields": fields},
|
||||
None,
|
||||
slug=view.slug,
|
||||
)
|
|
@ -1,14 +1,16 @@
|
|||
from django.dispatch import receiver
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
|
||||
from baserow.contrib.database.rows.registries import row_metadata_registry
|
||||
from baserow.ws.registries import page_registry
|
||||
|
||||
from baserow.contrib.database.rows import signals as row_signals
|
||||
from baserow.contrib.database.api.rows.serializers import (
|
||||
get_row_serializer_class,
|
||||
RowSerializer,
|
||||
)
|
||||
from baserow.contrib.database.rows import signals as row_signals
|
||||
from baserow.contrib.database.rows.registries import row_metadata_registry
|
||||
from baserow.contrib.database.table.models import GeneratedTableModel
|
||||
from baserow.ws.registries import page_registry
|
||||
|
||||
|
||||
@receiver(row_signals.row_created)
|
||||
|
@ -16,17 +18,16 @@ def row_created(sender, row, before, user, table, model, **kwargs):
|
|||
table_page_type = page_registry.get("table")
|
||||
transaction.on_commit(
|
||||
lambda: table_page_type.broadcast(
|
||||
{
|
||||
"type": "row_created",
|
||||
"table_id": table.id,
|
||||
"row": get_row_serializer_class(model, RowSerializer, is_response=True)(
|
||||
row
|
||||
).data,
|
||||
"metadata": row_metadata_registry.generate_and_merge_metadata_for_row(
|
||||
RealtimeRowMessages.row_created(
|
||||
table_id=table.id,
|
||||
serialized_row=get_row_serializer_class(
|
||||
model, RowSerializer, is_response=True
|
||||
)(row).data,
|
||||
metadata=row_metadata_registry.generate_and_merge_metadata_for_row(
|
||||
table, row.id
|
||||
),
|
||||
"before_row_id": before.id if before else None,
|
||||
},
|
||||
before=before,
|
||||
),
|
||||
getattr(user, "web_socket_id", None),
|
||||
table_id=table.id,
|
||||
)
|
||||
|
@ -34,7 +35,7 @@ def row_created(sender, row, before, user, table, model, **kwargs):
|
|||
|
||||
|
||||
@receiver(row_signals.before_row_update)
|
||||
def before_row_update(sender, row, user, table, model, **kwargs):
|
||||
def before_row_update(sender, row, user, table, model, updated_field_ids, **kwargs):
|
||||
# Generate a serialized version of the row before it is updated. The
|
||||
# `row_updated` receiver needs this serialized version because it can't serialize
|
||||
# the old row after it has been updated.
|
||||
|
@ -42,24 +43,22 @@ def before_row_update(sender, row, user, table, model, **kwargs):
|
|||
|
||||
|
||||
@receiver(row_signals.row_updated)
|
||||
def row_updated(sender, row, user, table, model, before_return, **kwargs):
|
||||
def row_updated(
|
||||
sender, row, user, table, model, before_return, updated_field_ids, **kwargs
|
||||
):
|
||||
table_page_type = page_registry.get("table")
|
||||
transaction.on_commit(
|
||||
lambda: table_page_type.broadcast(
|
||||
{
|
||||
"type": "row_updated",
|
||||
"table_id": table.id,
|
||||
# The web-frontend expects a serialized version of the row before it
|
||||
# was updated in order the estimate what position the row had in the
|
||||
# view.
|
||||
"row_before_update": dict(before_return)[before_row_update],
|
||||
"row": get_row_serializer_class(model, RowSerializer, is_response=True)(
|
||||
row
|
||||
).data,
|
||||
"metadata": row_metadata_registry.generate_and_merge_metadata_for_row(
|
||||
RealtimeRowMessages.row_updated(
|
||||
table_id=table.id,
|
||||
serialized_row_before_update=dict(before_return)[before_row_update],
|
||||
serialized_row=get_row_serializer_class(
|
||||
model, RowSerializer, is_response=True
|
||||
)(row).data,
|
||||
metadata=row_metadata_registry.generate_and_merge_metadata_for_row(
|
||||
table, row.id
|
||||
),
|
||||
},
|
||||
),
|
||||
getattr(user, "web_socket_id", None),
|
||||
table_id=table.id,
|
||||
)
|
||||
|
@ -79,15 +78,62 @@ def row_deleted(sender, row_id, row, user, table, model, before_return, **kwargs
|
|||
table_page_type = page_registry.get("table")
|
||||
transaction.on_commit(
|
||||
lambda: table_page_type.broadcast(
|
||||
{
|
||||
"type": "row_deleted",
|
||||
"table_id": table.id,
|
||||
"row_id": row_id,
|
||||
# The web-frontend expects a serialized version of the row that is
|
||||
# deleted in order the estimate what position the row had in the view.
|
||||
"row": dict(before_return)[before_row_delete],
|
||||
},
|
||||
RealtimeRowMessages.row_deleted(
|
||||
table_id=table.id, serialized_row=dict(before_return)[before_row_delete]
|
||||
),
|
||||
getattr(user, "web_socket_id", None),
|
||||
table_id=table.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RealtimeRowMessages:
|
||||
"""
|
||||
A collection of functions which construct the payloads for the realtime
|
||||
websocket messages related to rows.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def row_deleted(table_id: int, serialized_row: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "row_deleted",
|
||||
"table_id": table_id,
|
||||
"row_id": serialized_row["id"],
|
||||
# The web-frontend expects a serialized version of the row that is
|
||||
# deleted in order the estimate what position the row had in the view,
|
||||
# or find which kanban column the row was in etc.
|
||||
"row": serialized_row,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def row_created(
|
||||
table_id: int,
|
||||
serialized_row: Dict[str, Any],
|
||||
metadata: Dict[str, Any],
|
||||
before: Optional[GeneratedTableModel],
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "row_created",
|
||||
"table_id": table_id,
|
||||
"row": serialized_row,
|
||||
"metadata": metadata,
|
||||
"before_row_id": before.id if before else None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def row_updated(
|
||||
table_id: int,
|
||||
serialized_row_before_update: Dict[str, Any],
|
||||
serialized_row: Dict[str, Any],
|
||||
metadata: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "row_updated",
|
||||
"table_id": table_id,
|
||||
# The web-frontend expects a serialized version of the row before it
|
||||
# was updated in order the estimate what position the row had in the
|
||||
# view.
|
||||
"row_before_update": serialized_row_before_update,
|
||||
"row": serialized_row,
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
|
|
@ -1,8 +1,51 @@
|
|||
from django.conf import settings
|
||||
|
||||
from .table.signals import table_created, table_updated, table_deleted
|
||||
from .views.signals import view_created, views_reordered, view_updated, view_deleted
|
||||
from .rows.signals import row_created, row_updated, row_deleted
|
||||
from .fields.signals import field_created, field_updated, field_deleted
|
||||
|
||||
if settings.DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS:
|
||||
PUBLIC_SIGNALS = []
|
||||
else:
|
||||
# noinspection PyUnresolvedReferences
|
||||
from .public.rows.signals import ( # noqa: F401
|
||||
public_row_created,
|
||||
public_row_deleted,
|
||||
public_row_updated,
|
||||
)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from .public.views.signals import ( # noqa: F401
|
||||
public_view_filter_created,
|
||||
public_view_filter_deleted,
|
||||
public_view_filter_updated,
|
||||
public_view_updated,
|
||||
public_view_field_options_updated,
|
||||
)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from .public.fields.signals import ( # noqa: F401
|
||||
public_field_created,
|
||||
public_field_deleted,
|
||||
public_field_updated,
|
||||
public_field_restored,
|
||||
)
|
||||
|
||||
PUBLIC_SIGNALS = [
|
||||
"public_row_created",
|
||||
"public_row_deleted",
|
||||
"public_row_updated",
|
||||
"public_view_filter_updated",
|
||||
"public_view_filter_deleted",
|
||||
"public_view_filter_created",
|
||||
"public_view_updated",
|
||||
"public_view_field_options_updated",
|
||||
"public_field_created",
|
||||
"public_field_deleted",
|
||||
"public_field_updated",
|
||||
"public_field_restored",
|
||||
]
|
||||
|
||||
__all__ = [
|
||||
"table_created",
|
||||
|
@ -18,4 +61,5 @@ __all__ = [
|
|||
"field_created",
|
||||
"field_updated",
|
||||
"field_deleted",
|
||||
*PUBLIC_SIGNALS,
|
||||
]
|
||||
|
|
|
@ -4,12 +4,15 @@ from urllib.parse import parse_qs
|
|||
import jwt
|
||||
from channels.db import database_sync_to_async
|
||||
from channels.middleware import BaseMiddleware
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework_jwt.settings import api_settings
|
||||
|
||||
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
|
||||
jwt_decode_token = api_settings.JWT_DECODE_HANDLER
|
||||
|
||||
ANONYMOUS_USER_TOKEN = "anonymous"
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user(token):
|
||||
|
@ -23,26 +26,35 @@ def get_user(token):
|
|||
:rtype: User or None
|
||||
"""
|
||||
|
||||
try:
|
||||
payload = jwt_decode_token(token)
|
||||
except jwt.InvalidTokenError:
|
||||
return
|
||||
anonymous = token == ANONYMOUS_USER_TOKEN
|
||||
if anonymous:
|
||||
if settings.DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS:
|
||||
return
|
||||
else:
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
User = get_user_model()
|
||||
username = jwt_get_username_from_payload(payload)
|
||||
return AnonymousUser()
|
||||
else:
|
||||
try:
|
||||
payload = jwt_decode_token(token)
|
||||
except jwt.InvalidTokenError:
|
||||
return
|
||||
|
||||
if not username:
|
||||
return
|
||||
User = get_user_model()
|
||||
username = jwt_get_username_from_payload(payload)
|
||||
|
||||
try:
|
||||
user = User.objects.get_by_natural_key(username)
|
||||
except User.DoesNotExist:
|
||||
return
|
||||
if not username:
|
||||
return
|
||||
|
||||
if not user.is_active:
|
||||
return
|
||||
try:
|
||||
user = User.objects.get_by_natural_key(username)
|
||||
except User.DoesNotExist:
|
||||
return
|
||||
|
||||
return user
|
||||
if not user.is_active:
|
||||
return
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class JWTTokenAuthMiddleware(BaseMiddleware):
|
||||
|
|
|
@ -70,7 +70,7 @@ class PageType(Instance):
|
|||
:type payload: dict
|
||||
:param ignore_web_socket_id: If provided then the payload will not be broad
|
||||
casted to that web socket id. This is often the sender.
|
||||
:type ignore_web_socket_id: str
|
||||
:type ignore_web_socket_id: Optional[str]
|
||||
:param kwargs: The additional parameters including their provided values.
|
||||
:type kwargs: dict
|
||||
"""
|
||||
|
|
|
@ -4,8 +4,6 @@ import pytest
|
|||
from django.db import connection
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
|
||||
|
||||
from baserow.test_utils.fixtures.row import RowFixture
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
|
||||
|
||||
|
@ -24,9 +22,6 @@ def test_forwards_migration(data_fixture, transactional_db, migrate_to_latest_at
|
|||
migrate_to = [("database", "0050_remove_multiselect_missing_options")]
|
||||
|
||||
field_handler = FieldHandler()
|
||||
row_fixture = RowFixture()
|
||||
|
||||
migrate(migrate_from)
|
||||
|
||||
# The models used by the data_fixture below are not touched by this migration so
|
||||
# it is safe to use the latest version in the test.
|
||||
|
@ -44,7 +39,7 @@ def test_forwards_migration(data_fixture, transactional_db, migrate_to_latest_at
|
|||
option_b = data_fixture.create_select_option(field=field, value="B", color="blue")
|
||||
option_c = data_fixture.create_select_option(field=field, value="C", color="green")
|
||||
|
||||
row_fixture.create_row_for_many_to_many_field(
|
||||
data_fixture.create_row_for_many_to_many_field(
|
||||
table=table,
|
||||
field=field,
|
||||
values=[option_a.id, option_b.id, option_c.id],
|
||||
|
@ -53,6 +48,8 @@ def test_forwards_migration(data_fixture, transactional_db, migrate_to_latest_at
|
|||
|
||||
option_c.delete()
|
||||
|
||||
migrate(migrate_from)
|
||||
|
||||
# We check that we still have a link with the deleted option
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from freezegun import freeze_time
|
||||
from rest_framework.status import HTTP_200_OK
|
||||
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.fields.models import Field, TextField, LinkRowField
|
||||
from baserow.contrib.database.fields.models import (
|
||||
Field,
|
||||
TextField,
|
||||
LinkRowField,
|
||||
LookupField,
|
||||
FormulaField,
|
||||
)
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.table.models import Table
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
|
@ -1116,3 +1127,82 @@ def test_can_delete_tables_and_rows_in_the_same_perm_delete_batch(
|
|||
|
||||
assert TrashEntry.objects.count() == 0
|
||||
assert table.get_database_table_name() not in connection.introspection.table_names()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_perm_delete_lookup_row_field(data_fixture, api_client):
|
||||
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)
|
||||
customers_table = data_fixture.create_database_table(
|
||||
name="Customers", database=database
|
||||
)
|
||||
cars_table = data_fixture.create_database_table(name="Cars", database=database)
|
||||
data_fixture.create_database_table(name="Unrelated")
|
||||
|
||||
field_handler = FieldHandler()
|
||||
row_handler = RowHandler()
|
||||
|
||||
# Create a primary field and some example data for the customers table.
|
||||
customers_primary_field = field_handler.create_field(
|
||||
user=user, table=customers_table, type_name="text", name="Name", primary=True
|
||||
)
|
||||
row_handler.create_row(
|
||||
user=user,
|
||||
table=customers_table,
|
||||
values={f"field_{customers_primary_field.id}": "John"},
|
||||
)
|
||||
row_handler.create_row(
|
||||
user=user,
|
||||
table=customers_table,
|
||||
values={f"field_{customers_primary_field.id}": "Jane"},
|
||||
)
|
||||
|
||||
# Create a primary field and some example data for the cars table.
|
||||
cars_primary_field = field_handler.create_field(
|
||||
user=user, table=cars_table, type_name="text", name="Name", primary=True
|
||||
)
|
||||
row_handler.create_row(
|
||||
user=user, table=cars_table, values={f"field_{cars_primary_field.id}": "BMW"}
|
||||
)
|
||||
row_handler.create_row(
|
||||
user=user, table=cars_table, values={f"field_{cars_primary_field.id}": "Audi"}
|
||||
)
|
||||
|
||||
link_field_1 = field_handler.create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="link_row",
|
||||
name="Customer",
|
||||
link_row_table=customers_table,
|
||||
)
|
||||
lookup_field = field_handler.create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
type_name="lookup",
|
||||
name="Lookup",
|
||||
through_field_id=link_field_1.id,
|
||||
target_field_id=customers_primary_field.id,
|
||||
)
|
||||
trashed_at = timezone.now()
|
||||
plus_one_hour_over = timezone.timedelta(
|
||||
hours=settings.HOURS_UNTIL_TRASH_PERMANENTLY_DELETED + 1
|
||||
)
|
||||
with freeze_time(trashed_at):
|
||||
url = reverse(
|
||||
"api:database:fields:item",
|
||||
kwargs={"field_id": link_field_1.link_row_related_field.id},
|
||||
)
|
||||
response = api_client.delete(url, HTTP_AUTHORIZATION=f"JWT {token}")
|
||||
assert response.status_code == HTTP_200_OK
|
||||
url = reverse("api:database:fields:item", kwargs={"field_id": lookup_field.id})
|
||||
response = api_client.delete(url, HTTP_AUTHORIZATION=f"JWT {token}")
|
||||
assert response.status_code == HTTP_200_OK
|
||||
datetime_when_trash_item_old_enough_to_be_deleted = trashed_at + plus_one_hour_over
|
||||
with freeze_time(datetime_when_trash_item_old_enough_to_be_deleted):
|
||||
TrashHandler.mark_old_trash_for_permanent_deletion()
|
||||
TrashHandler.permanently_delete_marked_trash()
|
||||
|
||||
assert LinkRowField.objects_and_trash.all().count() == 0
|
||||
assert LookupField.objects_and_trash.all().count() == 0
|
||||
assert FormulaField.objects_and_trash.all().count() == 0
|
||||
|
|
|
@ -4,6 +4,7 @@ from decimal import Decimal
|
|||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.views.view_types import GridViewType
|
||||
from baserow.core.exceptions import UserNotInGroup
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
|
@ -1384,3 +1385,331 @@ def test_submit_form_view(send_mock, data_fixture):
|
|||
assert getattr(all[1], f"field_{text_field.id}") == "Another value"
|
||||
assert getattr(all[1], f"field_{number_field.id}") == 10
|
||||
assert not getattr(all[1], f"field_{boolean_field.id}")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_public_views_which_include_row(data_fixture, django_assert_num_queries):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
public_view1 = data_fixture.create_grid_view(
|
||||
user,
|
||||
create_options=False,
|
||||
table=table,
|
||||
public=True,
|
||||
order=0,
|
||||
)
|
||||
public_view2 = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=1
|
||||
)
|
||||
public_view3 = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=2
|
||||
)
|
||||
data_fixture.create_grid_view(user, table=table)
|
||||
data_fixture.create_grid_view_field_option(public_view1, hidden_field, hidden=True)
|
||||
data_fixture.create_grid_view_field_option(public_view2, hidden_field, hidden=True)
|
||||
|
||||
# Public View 1 has filters which match row 1
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view1, field=visible_field, type="equal", value="Visible"
|
||||
)
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view1, field=hidden_field, type="equal", value="Hidden"
|
||||
)
|
||||
|
||||
# Public View 2 has filters which match row 2
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view2, field=visible_field, type="equal", value="Visible"
|
||||
)
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view2, field=hidden_field, type="equal", value="Not Match"
|
||||
)
|
||||
|
||||
# Public View 3 has filters which match both rows
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view2, field=visible_field, type="equal", value="Visible"
|
||||
)
|
||||
|
||||
# Private View 1 has no filters so matches both rows
|
||||
|
||||
row = RowHandler().create_row(
|
||||
user=user,
|
||||
table=table,
|
||||
values={
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "Hidden",
|
||||
},
|
||||
)
|
||||
row2 = RowHandler().create_row(
|
||||
user=user,
|
||||
table=table,
|
||||
values={
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "Not Match",
|
||||
},
|
||||
)
|
||||
|
||||
model = table.get_model()
|
||||
checker = ViewHandler().get_public_views_row_checker(
|
||||
table,
|
||||
model,
|
||||
)
|
||||
assert checker.get_public_views_where_row_is_visible(row) == [
|
||||
public_view1.view_ptr,
|
||||
public_view3.view_ptr,
|
||||
]
|
||||
assert checker.get_public_views_where_row_is_visible(row2) == [
|
||||
public_view2.view_ptr,
|
||||
public_view3.view_ptr,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_public_view_row_checker_caches_when_only_unfiltered_fields_updated(
|
||||
data_fixture, django_assert_num_queries
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
filtered_field = data_fixture.create_text_field(table=table)
|
||||
unfiltered_field = data_fixture.create_text_field(table=table)
|
||||
public_grid_view = data_fixture.create_grid_view(
|
||||
user,
|
||||
table=table,
|
||||
public=True,
|
||||
)
|
||||
|
||||
data_fixture.create_view_filter(
|
||||
view=public_grid_view, field=filtered_field, type="equal", value="FilterValue"
|
||||
)
|
||||
model = table.get_model()
|
||||
visible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{filtered_field.id}": "FilterValue",
|
||||
f"field_{unfiltered_field.id}": "any",
|
||||
}
|
||||
)
|
||||
invisible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{filtered_field.id}": "NotFilterValue",
|
||||
f"field_{unfiltered_field.id}": "any",
|
||||
}
|
||||
)
|
||||
row_checker = ViewHandler().get_public_views_row_checker(
|
||||
table, model, updated_field_ids=[unfiltered_field.id]
|
||||
)
|
||||
|
||||
assert row_checker.get_public_views_where_row_is_visible(visible_row) == [
|
||||
public_grid_view.view_ptr
|
||||
]
|
||||
assert row_checker.get_public_views_where_row_is_visible(invisible_row) == []
|
||||
|
||||
# Because we've already checked these rows and we've told the checker we'll only
|
||||
# be changing unfiltered_field it knows it can cache the results
|
||||
with django_assert_num_queries(0):
|
||||
assert row_checker.get_public_views_where_row_is_visible(visible_row) == [
|
||||
public_grid_view.view_ptr
|
||||
]
|
||||
assert row_checker.get_public_views_where_row_is_visible(invisible_row) == []
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_public_view_row_checker_includes_public_views_with_no_filters_with_no_queries(
|
||||
data_fixture, django_assert_num_queries
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
filtered_field = data_fixture.create_text_field(table=table)
|
||||
unfiltered_field = data_fixture.create_text_field(table=table)
|
||||
public_grid_view = data_fixture.create_grid_view(
|
||||
user,
|
||||
table=table,
|
||||
public=True,
|
||||
)
|
||||
|
||||
model = table.get_model()
|
||||
visible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{filtered_field.id}": "any",
|
||||
f"field_{unfiltered_field.id}": "any",
|
||||
}
|
||||
)
|
||||
other_row = model.objects.create(
|
||||
**{
|
||||
f"field_{filtered_field.id}": "any",
|
||||
f"field_{unfiltered_field.id}": "any",
|
||||
}
|
||||
)
|
||||
row_checker = ViewHandler().get_public_views_row_checker(
|
||||
table, model, updated_field_ids=[unfiltered_field.id]
|
||||
)
|
||||
|
||||
# It should precalculate that this view is always visible.
|
||||
with django_assert_num_queries(0):
|
||||
assert row_checker.get_public_views_where_row_is_visible(visible_row) == [
|
||||
public_grid_view.view_ptr
|
||||
]
|
||||
assert row_checker.get_public_views_where_row_is_visible(other_row) == [
|
||||
public_grid_view.view_ptr
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_public_view_row_checker_does_not_cache_when_any_filtered_fields_updated(
|
||||
data_fixture, django_assert_num_queries
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
filtered_field = data_fixture.create_text_field(table=table)
|
||||
unfiltered_field = data_fixture.create_text_field(table=table)
|
||||
public_grid_view = data_fixture.create_grid_view(
|
||||
user,
|
||||
table=table,
|
||||
public=True,
|
||||
)
|
||||
|
||||
data_fixture.create_view_filter(
|
||||
view=public_grid_view, field=filtered_field, type="equal", value="FilterValue"
|
||||
)
|
||||
model = table.get_model()
|
||||
visible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{filtered_field.id}": "FilterValue",
|
||||
f"field_{unfiltered_field.id}": "any",
|
||||
}
|
||||
)
|
||||
invisible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{filtered_field.id}": "NotFilterValue",
|
||||
f"field_{unfiltered_field.id}": "any",
|
||||
}
|
||||
)
|
||||
row_checker = ViewHandler().get_public_views_row_checker(
|
||||
table, model, updated_field_ids=[filtered_field.id, unfiltered_field.id]
|
||||
)
|
||||
|
||||
assert row_checker.get_public_views_where_row_is_visible(visible_row) == [
|
||||
public_grid_view.view_ptr
|
||||
]
|
||||
assert row_checker.get_public_views_where_row_is_visible(invisible_row) == []
|
||||
|
||||
# Now update the rows so they swap and the invisible one becomes visible and vice
|
||||
# versa
|
||||
setattr(invisible_row, f"field_{filtered_field.id}", "FilterValue")
|
||||
invisible_row.save()
|
||||
setattr(visible_row, f"field_{filtered_field.id}", "NotFilterValue")
|
||||
visible_row.save()
|
||||
|
||||
assert row_checker.get_public_views_where_row_is_visible(invisible_row) == [
|
||||
public_grid_view.view_ptr
|
||||
]
|
||||
assert row_checker.get_public_views_where_row_is_visible(visible_row) == []
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_public_view_row_checker_runs_expected_queries_on_init(
|
||||
data_fixture, django_assert_num_queries
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
filtered_field = data_fixture.create_text_field(table=table)
|
||||
unfiltered_field = data_fixture.create_text_field(table=table)
|
||||
public_grid_view = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=0
|
||||
)
|
||||
|
||||
data_fixture.create_view_filter(
|
||||
view=public_grid_view, field=filtered_field, type="equal", value="FilterValue"
|
||||
)
|
||||
model = table.get_model()
|
||||
with django_assert_num_queries(2):
|
||||
# First query to get the public views, second query to get their filters.
|
||||
ViewHandler().get_public_views_row_checker(
|
||||
table, model, updated_field_ids=[filtered_field.id, unfiltered_field.id]
|
||||
)
|
||||
|
||||
another_public_grid_view = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=1
|
||||
)
|
||||
# Public View 1 has filters which match row 1
|
||||
data_fixture.create_view_filter(
|
||||
view=another_public_grid_view,
|
||||
field=filtered_field,
|
||||
type="equal",
|
||||
value="FilterValue",
|
||||
)
|
||||
|
||||
# Adding another view shouldn't result in more queries
|
||||
with django_assert_num_queries(2):
|
||||
# First query to get the public views, second query to get their filters.
|
||||
ViewHandler().get_public_views_row_checker(
|
||||
table, model, updated_field_ids=[filtered_field.id, unfiltered_field.id]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_public_view_row_checker_runs_expected_queries_when_checking_rows(
|
||||
data_fixture, django_assert_num_queries
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
filtered_field = data_fixture.create_text_field(table=table)
|
||||
unfiltered_field = data_fixture.create_text_field(table=table)
|
||||
public_grid_view = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=0
|
||||
)
|
||||
|
||||
# Public View 1 has filters which match row 1
|
||||
data_fixture.create_view_filter(
|
||||
view=public_grid_view, field=filtered_field, type="equal", value="FilterValue"
|
||||
)
|
||||
model = table.get_model()
|
||||
visible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{filtered_field.id}": "FilterValue",
|
||||
f"field_{unfiltered_field.id}": "any",
|
||||
}
|
||||
)
|
||||
invisible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{filtered_field.id}": "NotFilterValue",
|
||||
f"field_{unfiltered_field.id}": "any",
|
||||
}
|
||||
)
|
||||
row_checker = ViewHandler().get_public_views_row_checker(
|
||||
table, model, updated_field_ids=[filtered_field.id, unfiltered_field.id]
|
||||
)
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
# Only should run a single exists query to check if the row is in the single
|
||||
# public view
|
||||
assert row_checker.get_public_views_where_row_is_visible(visible_row) == [
|
||||
public_grid_view.view_ptr
|
||||
]
|
||||
with django_assert_num_queries(1):
|
||||
# Only should run a single exists query to check if the row is in the single
|
||||
# public view
|
||||
assert row_checker.get_public_views_where_row_is_visible(invisible_row) == []
|
||||
|
||||
another_public_grid_view = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=1
|
||||
)
|
||||
data_fixture.create_view_filter(
|
||||
view=another_public_grid_view,
|
||||
field=filtered_field,
|
||||
type="equal",
|
||||
value="FilterValue",
|
||||
)
|
||||
|
||||
row_checker = ViewHandler().get_public_views_row_checker(
|
||||
table, model, updated_field_ids=[filtered_field.id, unfiltered_field.id]
|
||||
)
|
||||
with django_assert_num_queries(2):
|
||||
# Now should run two queries, one per public view
|
||||
assert row_checker.get_public_views_where_row_is_visible(visible_row) == [
|
||||
public_grid_view.view_ptr,
|
||||
another_public_grid_view.view_ptr,
|
||||
]
|
||||
with django_assert_num_queries(2):
|
||||
# Now should run two queries, one per public view
|
||||
assert row_checker.get_public_views_where_row_is_visible(invisible_row) == []
|
||||
|
|
|
@ -25,19 +25,19 @@ def test_view_get_field_options(data_fixture):
|
|||
field_options = grid_view.get_field_options()
|
||||
assert len(field_options) == 2
|
||||
|
||||
field_options = grid_view.get_field_options(create_if_not_exists=True)
|
||||
field_options = grid_view.get_field_options(create_if_missing=True)
|
||||
assert len(field_options) == 3
|
||||
assert field_options[0].field_id == field_1.id
|
||||
assert field_options[1].field_id == field_2.id
|
||||
assert field_options[2].field_id == field_3.id
|
||||
|
||||
field_options = grid_view.get_field_options(create_if_not_exists=False)
|
||||
field_options = grid_view.get_field_options(create_if_missing=False)
|
||||
assert len(field_options) == 3
|
||||
assert field_options[0].field_id == field_1.id
|
||||
assert field_options[1].field_id == field_2.id
|
||||
assert field_options[2].field_id == field_3.id
|
||||
|
||||
field_options = form_view.get_field_options(create_if_not_exists=False)
|
||||
field_options = form_view.get_field_options(create_if_missing=False)
|
||||
assert len(field_options) == 2
|
||||
assert field_options[0].field_id == field_1.id
|
||||
assert field_options[0].name == ""
|
||||
|
@ -45,7 +45,7 @@ def test_view_get_field_options(data_fixture):
|
|||
assert field_options[0].field_id == field_1.id
|
||||
assert field_options[1].field_id == field_2.id
|
||||
|
||||
field_options = form_view.get_field_options(create_if_not_exists=True)
|
||||
field_options = form_view.get_field_options(create_if_missing=True)
|
||||
assert len(field_options) == 3
|
||||
assert field_options[0].field_id == field_1.id
|
||||
assert field_options[0].field_id == field_1.id
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
import pytest
|
||||
|
||||
from unittest.mock import patch, call, ANY
|
||||
|
||||
from baserow.contrib.database.api.constants import PUBLIC_PLACEHOLDER_ENTITY_ID
|
||||
from baserow.contrib.database.fields.dependencies.handler import FieldDependencyHandler
|
||||
from baserow.contrib.database.fields.field_cache import FieldCache
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.core.trash.handler import TrashHandler
|
||||
|
||||
|
||||
class MatchDictSubSet(object):
|
||||
def __init__(self, sub_set):
|
||||
self.sub_set = sub_set
|
||||
|
||||
def __eq__(self, other):
|
||||
return all(
|
||||
self.sub_set[k] == other[k] for k in self.sub_set.keys() & other.keys()
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"MatchDictSubSet({self.sub_set})"
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_field_created_public_views_are_sent_field_created_with_restricted_related(
|
||||
mock_broadcast_to_channel_group, data_fixture, django_assert_num_queries
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
data_fixture.create_text_field(table=table, order=0)
|
||||
hidden_broken_field = data_fixture.create_formula_field(
|
||||
table=table, formula="field('a')", name="hidden_broken"
|
||||
)
|
||||
visible_broken_field = data_fixture.create_formula_field(
|
||||
table=table, formula="field('a')", name="visible_broken"
|
||||
)
|
||||
field_cache = FieldCache()
|
||||
FieldDependencyHandler().rebuild_dependencies(hidden_broken_field, field_cache)
|
||||
FieldDependencyHandler().rebuild_dependencies(visible_broken_field, field_cache)
|
||||
public_view = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view, hidden_broken_field, hidden=True, order=0
|
||||
)
|
||||
new_visible_field = FieldHandler().create_field(
|
||||
user, table, "text", name="a", order=1
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view.slug}",
|
||||
{
|
||||
"type": "field_created",
|
||||
"field": MatchDictSubSet(
|
||||
{
|
||||
"id": new_visible_field.id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"name": new_visible_field.name,
|
||||
}
|
||||
),
|
||||
"related_fields": [
|
||||
MatchDictSubSet(
|
||||
{
|
||||
"id": visible_broken_field.id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"name": visible_broken_field.name,
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_field_deleted_public_views_are_field_deleted_with_restricted_related(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table, order=0, name="visible")
|
||||
hidden_broken_field = data_fixture.create_formula_field(
|
||||
table=table, formula="field('visible')", name="hidden_broken"
|
||||
)
|
||||
visible_broken_field = data_fixture.create_formula_field(
|
||||
table=table, formula="field('visible')", name="visible_broken"
|
||||
)
|
||||
field_cache = FieldCache()
|
||||
FieldDependencyHandler().rebuild_dependencies(hidden_broken_field, field_cache)
|
||||
FieldDependencyHandler().rebuild_dependencies(visible_broken_field, field_cache)
|
||||
public_view = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view, hidden_broken_field, hidden=True, order=0
|
||||
)
|
||||
deleted_field_id = visible_field.id
|
||||
FieldHandler().delete_field(user, visible_field)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view.slug}",
|
||||
{
|
||||
"type": "field_deleted",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"field_id": deleted_field_id,
|
||||
"related_fields": [
|
||||
MatchDictSubSet(
|
||||
{
|
||||
"id": visible_broken_field.id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"name": visible_broken_field.name,
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_field_restored_public_views_sent_event_with_restricted_related_fields(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table, order=0, name="visible")
|
||||
hidden_broken_field = data_fixture.create_formula_field(
|
||||
table=table, formula="field('visible')", name="hidden_broken"
|
||||
)
|
||||
visible_broken_field = data_fixture.create_formula_field(
|
||||
table=table, formula="field('visible')", name="visible_broken"
|
||||
)
|
||||
field_cache = FieldCache()
|
||||
FieldDependencyHandler().rebuild_dependencies(hidden_broken_field, field_cache)
|
||||
FieldDependencyHandler().rebuild_dependencies(visible_broken_field, field_cache)
|
||||
public_view = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view, hidden_broken_field, hidden=True, order=0
|
||||
)
|
||||
deleted_field_id = visible_field.id
|
||||
FieldHandler().delete_field(user, visible_field)
|
||||
TrashHandler().restore_item(user, "field", visible_field.id)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(f"view-{public_view.slug}", ANY, ANY),
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view.slug}",
|
||||
{
|
||||
"type": "field_restored",
|
||||
"field": MatchDictSubSet(
|
||||
{
|
||||
"id": deleted_field_id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"name": visible_field.name,
|
||||
}
|
||||
),
|
||||
"related_fields": [
|
||||
MatchDictSubSet(
|
||||
{
|
||||
"id": visible_broken_field.id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"name": visible_broken_field.name,
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_field_updated_public_views_are_sent_event_with_restricted_related(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table, order=0, name="b")
|
||||
hidden_broken_field = data_fixture.create_formula_field(
|
||||
table=table, formula="field('a')", name="hidden_broken"
|
||||
)
|
||||
visible_broken_field = data_fixture.create_formula_field(
|
||||
table=table, formula="field('a')", name="visible_broken"
|
||||
)
|
||||
field_cache = FieldCache()
|
||||
FieldDependencyHandler().rebuild_dependencies(hidden_broken_field, field_cache)
|
||||
FieldDependencyHandler().rebuild_dependencies(visible_broken_field, field_cache)
|
||||
public_view = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view, hidden_broken_field, hidden=True, order=0
|
||||
)
|
||||
updated_field = FieldHandler().update_field(user, visible_field, name="a")
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view.slug}",
|
||||
{
|
||||
"type": "field_updated",
|
||||
"field_id": updated_field.id,
|
||||
"field": MatchDictSubSet(
|
||||
{
|
||||
"id": updated_field.id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"name": updated_field.name,
|
||||
}
|
||||
),
|
||||
"related_fields": [
|
||||
MatchDictSubSet(
|
||||
{
|
||||
"id": visible_broken_field.id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"name": visible_broken_field.name,
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,822 @@
|
|||
from unittest.mock import patch, call, ANY
|
||||
|
||||
import pytest
|
||||
from django.db import transaction
|
||||
|
||||
from baserow.contrib.database.api.constants import PUBLIC_PLACEHOLDER_ENTITY_ID
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
from baserow.core.trash.handler import TrashHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_row_created_public_views_receive_restricted_row_created_ws_event(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
public_view_only_showing_one_field = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
public_view_showing_all_fields = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=1
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_only_showing_one_field, hidden_field, hidden=True
|
||||
)
|
||||
row = RowHandler().create_row(
|
||||
user=user,
|
||||
table=table,
|
||||
values={
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "Hidden",
|
||||
},
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view_only_showing_one_field.slug}",
|
||||
{
|
||||
"type": "row_created",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row": {
|
||||
"id": row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
"metadata": {},
|
||||
"before_row_id": None,
|
||||
},
|
||||
None,
|
||||
),
|
||||
call(
|
||||
f"view-{public_view_showing_all_fields.slug}",
|
||||
{
|
||||
"type": "row_created",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row": {
|
||||
"id": row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
# This field is not hidden for this public view and so should be
|
||||
# included
|
||||
f"field_{hidden_field.id}": "Hidden",
|
||||
},
|
||||
"metadata": {},
|
||||
"before_row_id": None,
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_row_created_public_views_receive_row_created_only_when_filters_match(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
public_view_showing_row = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
public_view_hiding_row = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=1
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_showing_row, hidden_field, hidden=True
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_hiding_row, hidden_field, hidden=True
|
||||
)
|
||||
|
||||
# Match the visible field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_hiding_row, field=visible_field, type="equal", value="Visible"
|
||||
)
|
||||
# But filter out based on the hidden field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_hiding_row, field=hidden_field, type="equal", value="Not Match"
|
||||
)
|
||||
|
||||
# Match
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_showing_row, field=visible_field, type="equal", value="Visible"
|
||||
)
|
||||
# Match
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_showing_row, field=hidden_field, type="equal", value="Hidden"
|
||||
)
|
||||
|
||||
row = RowHandler().create_row(
|
||||
user=user,
|
||||
table=table,
|
||||
values={
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "Hidden",
|
||||
},
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view_showing_row.slug}",
|
||||
{
|
||||
"type": "row_created",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row": {
|
||||
"id": row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
"metadata": {},
|
||||
"before_row_id": None,
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_row_deleted_public_views_receive_restricted_row_deleted_ws_event(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
public_view_only_showing_one_field = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
public_view_showing_all_fields = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=1
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_only_showing_one_field, hidden_field, hidden=True
|
||||
)
|
||||
model = table.get_model()
|
||||
row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "Hidden",
|
||||
},
|
||||
)
|
||||
RowHandler().delete_row(user, table, row.id, model)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view_only_showing_one_field.slug}",
|
||||
{
|
||||
"type": "row_deleted",
|
||||
"row_id": row.id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row": {
|
||||
"id": row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
},
|
||||
None,
|
||||
),
|
||||
call(
|
||||
f"view-{public_view_showing_all_fields.slug}",
|
||||
{
|
||||
"type": "row_deleted",
|
||||
"row_id": row.id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row": {
|
||||
"id": row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
# This field is not hidden for this public view and so should be
|
||||
# included
|
||||
f"field_{hidden_field.id}": "Hidden",
|
||||
},
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_row_deleted_public_views_receive_row_deleted_only_when_filters_match(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
public_view_showing_row = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
public_view_hiding_row = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=1
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_showing_row, hidden_field, hidden=True
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_hiding_row, hidden_field, hidden=True
|
||||
)
|
||||
|
||||
# Match the visible field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_hiding_row, field=visible_field, type="equal", value="Visible"
|
||||
)
|
||||
# But filter out based on the hidden field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_hiding_row, field=hidden_field, type="equal", value="Not Match"
|
||||
)
|
||||
|
||||
# Match
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_showing_row, field=visible_field, type="equal", value="Visible"
|
||||
)
|
||||
# Match
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_showing_row, field=hidden_field, type="equal", value="Hidden"
|
||||
)
|
||||
|
||||
model = table.get_model()
|
||||
row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "Hidden",
|
||||
},
|
||||
)
|
||||
RowHandler().delete_row(user, table, row.id, model)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view_showing_row.slug}",
|
||||
{
|
||||
"type": "row_deleted",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row_id": row.id,
|
||||
"row": {
|
||||
"id": row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_given_row_not_visible_in_public_view_when_updated_to_be_visible_event_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
|
||||
public_view_with_filters_initially_hiding_all_rows = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_with_filters_initially_hiding_all_rows, hidden_field, hidden=True
|
||||
)
|
||||
|
||||
# Match the visible field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_with_filters_initially_hiding_all_rows,
|
||||
field=visible_field,
|
||||
type="equal",
|
||||
value="Visible",
|
||||
)
|
||||
# But filter out based on the hidden field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_with_filters_initially_hiding_all_rows,
|
||||
field=hidden_field,
|
||||
type="equal",
|
||||
value="ValueWhichMatchesFilter",
|
||||
)
|
||||
|
||||
model = table.get_model()
|
||||
initially_hidden_row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "ValueWhichDoesntMatchFilter",
|
||||
},
|
||||
)
|
||||
|
||||
# Double check the row isn't visible in any views to begin with
|
||||
row_checker = ViewHandler().get_public_views_row_checker(table, model)
|
||||
assert row_checker.get_public_views_where_row_is_visible(initially_hidden_row) == []
|
||||
|
||||
RowHandler().update_row(
|
||||
user,
|
||||
table,
|
||||
initially_hidden_row.id,
|
||||
values={f"field_{hidden_field.id}": "ValueWhichMatchesFilter"},
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view_with_filters_initially_hiding_all_rows.slug}",
|
||||
{
|
||||
# The row should appear as a created event as for the public view
|
||||
# it effectively has been created as it didn't exist before.
|
||||
"type": "row_created",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row": {
|
||||
"id": initially_hidden_row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
"metadata": {},
|
||||
"before_row_id": None,
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_given_row_visible_in_public_view_when_updated_to_be_not_visible_event_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
|
||||
public_view_with_row_showing = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_with_row_showing, hidden_field, hidden=True
|
||||
)
|
||||
|
||||
# Match the visible field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_with_row_showing,
|
||||
field=visible_field,
|
||||
type="contains",
|
||||
value="Visible",
|
||||
)
|
||||
# But filter out based on the hidden field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_with_row_showing,
|
||||
field=hidden_field,
|
||||
type="equal",
|
||||
value="ValueWhichMatchesFilter",
|
||||
)
|
||||
|
||||
model = table.get_model()
|
||||
initially_visible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "ValueWhichMatchesFilter",
|
||||
},
|
||||
)
|
||||
|
||||
# Double check the row is visible in the view to start with
|
||||
row_checker = ViewHandler().get_public_views_row_checker(table, model)
|
||||
assert row_checker.get_public_views_where_row_is_visible(initially_visible_row) == [
|
||||
public_view_with_row_showing.view_ptr
|
||||
]
|
||||
|
||||
# Update the row so it is no longer visible
|
||||
RowHandler().update_row(
|
||||
user,
|
||||
table,
|
||||
initially_visible_row.id,
|
||||
values={
|
||||
f"field_{hidden_field.id}": "ValueWhichDoesNotMatchFilter",
|
||||
f"field_{visible_field.id}": "StillVisibleButNew",
|
||||
},
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view_with_row_showing.slug}",
|
||||
{
|
||||
# The row should appear as a deleted event as for the public view
|
||||
# it effectively has been.
|
||||
"type": "row_deleted",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row_id": initially_visible_row.id,
|
||||
"row": {
|
||||
"id": initially_visible_row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent in its state before it
|
||||
# was updated
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_given_row_visible_in_public_view_when_updated_to_still_be_visible_event_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
|
||||
public_view_with_row_showing = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_with_row_showing, hidden_field, hidden=True
|
||||
)
|
||||
|
||||
# Match the visible field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_with_row_showing,
|
||||
field=visible_field,
|
||||
type="contains",
|
||||
value="Visible",
|
||||
)
|
||||
# But filter out based on the hidden field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_with_row_showing,
|
||||
field=hidden_field,
|
||||
type="contains",
|
||||
value="e",
|
||||
)
|
||||
|
||||
model = table.get_model()
|
||||
initially_visible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "e",
|
||||
},
|
||||
)
|
||||
|
||||
# Double check the row is visible in the view to start with
|
||||
row_checker = ViewHandler().get_public_views_row_checker(table, model)
|
||||
assert row_checker.get_public_views_where_row_is_visible(initially_visible_row) == [
|
||||
public_view_with_row_showing.view_ptr
|
||||
]
|
||||
|
||||
# Update the row so it is still visible but changed
|
||||
RowHandler().update_row(
|
||||
user,
|
||||
table,
|
||||
initially_visible_row.id,
|
||||
values={
|
||||
f"field_{hidden_field.id}": "eee",
|
||||
f"field_{visible_field.id}": "StillVisibleButUpdated",
|
||||
},
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view_with_row_showing.slug}",
|
||||
{
|
||||
"type": "row_updated",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row_before_update": {
|
||||
"id": initially_visible_row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
"row": {
|
||||
"id": initially_visible_row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "StillVisibleButUpdated",
|
||||
},
|
||||
"metadata": {},
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_row_restored_public_views_receive_restricted_row_created_ws_event(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
public_view_only_showing_one_field = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
public_view_showing_all_fields = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=1
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_only_showing_one_field, hidden_field, hidden=True
|
||||
)
|
||||
model = table.get_model()
|
||||
row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "Hidden",
|
||||
},
|
||||
)
|
||||
TrashHandler.trash(
|
||||
user, table.database.group, table.database, row, parent_id=table.id
|
||||
)
|
||||
TrashHandler.restore_item(user, "row", row.id, parent_trash_item_id=table.id)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view_only_showing_one_field.slug}",
|
||||
{
|
||||
"type": "row_created",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row": {
|
||||
"id": row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
"metadata": {},
|
||||
"before_row_id": None,
|
||||
},
|
||||
None,
|
||||
),
|
||||
call(
|
||||
f"view-{public_view_showing_all_fields.slug}",
|
||||
{
|
||||
"type": "row_created",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row": {
|
||||
"id": row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
# This field is not hidden for this public view and so should be
|
||||
# included
|
||||
f"field_{hidden_field.id}": "Hidden",
|
||||
},
|
||||
"metadata": {},
|
||||
"before_row_id": None,
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_row_restored_public_views_receive_row_created_only_when_filters_match(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
public_view_showing_row = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
public_view_hiding_row = data_fixture.create_grid_view(
|
||||
user, table=table, public=True, order=1
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_showing_row, hidden_field, hidden=True
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
public_view_hiding_row, hidden_field, hidden=True
|
||||
)
|
||||
|
||||
# Match the visible field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_hiding_row, field=visible_field, type="equal", value="Visible"
|
||||
)
|
||||
# But filter out based on the hidden field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_hiding_row, field=hidden_field, type="equal", value="Not Match"
|
||||
)
|
||||
|
||||
# Match
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_showing_row, field=visible_field, type="equal", value="Visible"
|
||||
)
|
||||
# Match
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view_showing_row, field=hidden_field, type="equal", value="Hidden"
|
||||
)
|
||||
|
||||
model = table.get_model()
|
||||
row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "Hidden",
|
||||
},
|
||||
)
|
||||
TrashHandler.trash(
|
||||
user, table.database.group, table.database, row, parent_id=table.id
|
||||
)
|
||||
TrashHandler.restore_item(user, "row", row.id, parent_trash_item_id=table.id)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view_showing_row.slug}",
|
||||
{
|
||||
"type": "row_created",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row": {
|
||||
"id": row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
"metadata": {},
|
||||
"before_row_id": None,
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_given_row_visible_in_public_view_when_moved_row_updated_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
|
||||
public_view = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(public_view, hidden_field, hidden=True)
|
||||
|
||||
# Match the visible field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view,
|
||||
field=visible_field,
|
||||
type="contains",
|
||||
value="Visible",
|
||||
)
|
||||
# But filter out based on the hidden field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view,
|
||||
field=hidden_field,
|
||||
type="equal",
|
||||
value="ValueWhichMatchesFilter",
|
||||
)
|
||||
|
||||
model = table.get_model()
|
||||
visible_moving_row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "ValueWhichMatchesFilter",
|
||||
},
|
||||
)
|
||||
invisible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "ValueWhichDoesNotMatchesFilter",
|
||||
},
|
||||
)
|
||||
|
||||
# Double check the row is visible in the view to start with
|
||||
row_checker = ViewHandler().get_public_views_row_checker(table, model)
|
||||
assert row_checker.get_public_views_where_row_is_visible(visible_moving_row) == [
|
||||
public_view.view_ptr
|
||||
]
|
||||
|
||||
# Move the visible row behind the invisible one
|
||||
with transaction.atomic():
|
||||
RowHandler().move_row(
|
||||
user, table, visible_moving_row.id, before=invisible_row, model=model
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view.slug}",
|
||||
{
|
||||
# The row should appear as a deleted event as for the public view
|
||||
# it effectively has been.
|
||||
"type": "row_updated",
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"row_before_update": {
|
||||
"id": visible_moving_row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
"row": {
|
||||
"id": visible_moving_row.id,
|
||||
"order": "0.99999999999999999999",
|
||||
# Only the visible field should be sent
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
},
|
||||
"metadata": {},
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_given_row_invisible_in_public_view_when_moved_no_update_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
|
||||
public_view = data_fixture.create_grid_view(
|
||||
user, create_options=False, table=table, public=True, order=0
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(public_view, hidden_field, hidden=True)
|
||||
|
||||
# Match the visible field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view,
|
||||
field=visible_field,
|
||||
type="contains",
|
||||
value="Visible",
|
||||
)
|
||||
# But filter out based on the hidden field
|
||||
data_fixture.create_view_filter(
|
||||
view=public_view,
|
||||
field=hidden_field,
|
||||
type="equal",
|
||||
value="ValueWhichMatchesFilter",
|
||||
)
|
||||
|
||||
model = table.get_model()
|
||||
visible_row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "ValueWhichMatchesFilter",
|
||||
},
|
||||
)
|
||||
invisible_moving_row = model.objects.create(
|
||||
**{
|
||||
f"field_{visible_field.id}": "Visible",
|
||||
f"field_{hidden_field.id}": "ValueWhichDoesNotMatchesFilter",
|
||||
},
|
||||
)
|
||||
|
||||
# Double check the row is visible in the view to start with
|
||||
row_checker = ViewHandler().get_public_views_row_checker(table, model)
|
||||
assert row_checker.get_public_views_where_row_is_visible(invisible_moving_row) == []
|
||||
|
||||
# Move the invisible row
|
||||
with transaction.atomic():
|
||||
RowHandler().move_row(
|
||||
user, table, invisible_moving_row.id, before=visible_row, model=model
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,216 @@
|
|||
from unittest.mock import patch, call, ANY
|
||||
|
||||
import pytest
|
||||
|
||||
from baserow.contrib.database.api.constants import PUBLIC_PLACEHOLDER_ENTITY_ID
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_view_filter_created_for_public_view_force_refresh_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
field = data_fixture.create_text_field(table=table)
|
||||
public_view = data_fixture.create_grid_view(user=user, table=table, public=True)
|
||||
ViewHandler().create_filter(
|
||||
user=user, view=public_view, type_name="equal", value="test", field=field
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view.slug}",
|
||||
{"type": "force_view_rows_refresh", "view_id": public_view.slug},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_view_filter_updated_for_public_view_force_refresh_event_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
field = data_fixture.create_text_field(table=table)
|
||||
public_view = data_fixture.create_grid_view(user=user, table=table, public=True)
|
||||
view_filter = data_fixture.create_view_filter(
|
||||
user=user, view=public_view, field=field
|
||||
)
|
||||
ViewHandler().update_filter(user=user, view_filter=view_filter, value="test2")
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view.slug}",
|
||||
{"type": "force_view_rows_refresh", "view_id": public_view.slug},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_view_filter_deleted_for_public_view_force_refresh_event_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
field = data_fixture.create_text_field(table=table)
|
||||
public_view = data_fixture.create_grid_view(user=user, table=table, public=True)
|
||||
view_filter = data_fixture.create_view_filter(
|
||||
user=user, view=public_view, field=field
|
||||
)
|
||||
ViewHandler().delete_filter(user=user, view_filter=view_filter)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_view.slug}",
|
||||
{"type": "force_view_rows_refresh", "view_id": public_view.slug},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_field_hidden_in_public_view_field_force_refresh_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
text_field = data_fixture.create_text_field(table=table)
|
||||
public_grid_view = data_fixture.create_grid_view(table=table, public=True)
|
||||
|
||||
ViewHandler().update_field_options(
|
||||
user=user,
|
||||
view=public_grid_view,
|
||||
field_options={str(text_field.id): {"hidden": True}},
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_grid_view.slug}",
|
||||
{
|
||||
"type": "force_view_refresh",
|
||||
"view_id": public_grid_view.slug,
|
||||
"fields": [],
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_field_unhidden_in_public_view_force_refresh_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
text_field = data_fixture.create_text_field(table=table)
|
||||
public_grid_view = data_fixture.create_grid_view(
|
||||
table=table, public=True, create_options=False
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
grid_view=public_grid_view, field=text_field, hidden=True
|
||||
)
|
||||
|
||||
ViewHandler().update_field_options(
|
||||
user=user,
|
||||
view=public_grid_view,
|
||||
field_options={str(text_field.id): {"hidden": False}},
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_grid_view.slug}",
|
||||
{
|
||||
"type": "force_view_refresh",
|
||||
"view_id": public_grid_view.slug,
|
||||
"fields": [
|
||||
{
|
||||
"id": text_field.id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"name": text_field.name,
|
||||
"order": 0,
|
||||
"type": "text",
|
||||
"primary": False,
|
||||
"text_default": "",
|
||||
}
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_when_only_field_options_updated_in_public_grid_view_force_refresh_sent(
|
||||
mock_broadcast_to_channel_group, data_fixture
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
visible_field = data_fixture.create_text_field(table=table)
|
||||
hidden_field = data_fixture.create_text_field(table=table)
|
||||
public_grid_view = data_fixture.create_grid_view(
|
||||
table=table, public=True, create_options=False
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
grid_view=public_grid_view, field=visible_field, hidden=False
|
||||
)
|
||||
data_fixture.create_grid_view_field_option(
|
||||
grid_view=public_grid_view, field=hidden_field, hidden=True
|
||||
)
|
||||
|
||||
ViewHandler().update_field_options(
|
||||
user=user,
|
||||
view=public_grid_view,
|
||||
field_options={
|
||||
str(visible_field.id): {"width": 100},
|
||||
str(hidden_field.id): {"width": 100},
|
||||
},
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_has_calls(
|
||||
[
|
||||
call(f"table-{table.id}", ANY, ANY),
|
||||
call(
|
||||
f"view-{public_grid_view.slug}",
|
||||
{
|
||||
"type": "force_view_refresh",
|
||||
"view_id": public_grid_view.slug,
|
||||
"fields": [
|
||||
{
|
||||
"id": visible_field.id,
|
||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"name": visible_field.name,
|
||||
"order": 0,
|
||||
"type": "text",
|
||||
"primary": False,
|
||||
"text_default": "",
|
||||
}
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,177 @@
|
|||
import pytest
|
||||
from pyinstrument import Profiler
|
||||
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.slow
|
||||
# You must add --runslow -s to pytest to run this test, you can do this in intellij by
|
||||
# editing the run config for this test and adding --runslow -s to additional args.
|
||||
def test_creating_many_rows_in_public_filtered_views(
|
||||
data_fixture, django_assert_num_queries
|
||||
):
|
||||
|
||||
user = data_fixture.create_user()
|
||||
table, fields, rows = data_fixture.build_table(
|
||||
columns=[("number", "number")], rows=[["0"]], user=user
|
||||
)
|
||||
|
||||
num_public_views = 10
|
||||
views = []
|
||||
for i in range(num_public_views):
|
||||
views.append(data_fixture.create_grid_view(user=user, table=table, public=True))
|
||||
|
||||
num_fields = 20
|
||||
for i in range(num_fields):
|
||||
last_field = FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
name=f"field{i}",
|
||||
type_name="text",
|
||||
)
|
||||
for view in views:
|
||||
data_fixture.create_view_filter(
|
||||
view=view, field=last_field, type="equal", value=i
|
||||
)
|
||||
handler = RowHandler()
|
||||
num_rows = 1000
|
||||
rows = []
|
||||
for i in range(num_rows):
|
||||
row = {}
|
||||
for j in range(num_fields):
|
||||
row[f"field_{j}"] = (i + j) % num_fields
|
||||
rows.append(row)
|
||||
model = table.get_model()
|
||||
profiler = Profiler()
|
||||
profiler.start()
|
||||
for i in range(num_rows):
|
||||
handler.create_row(user, table, rows[i], model=model)
|
||||
profiler.stop()
|
||||
print(profiler.output_text(unicode=True, color=True))
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.slow
|
||||
# You must add --runslow -s to pytest to run this test, you can do this in intellij by
|
||||
# editing the run config for this test and adding --runslow -s to additional args.
|
||||
def test_updating_many_rows_in_public_filtered_views(
|
||||
data_fixture, django_assert_num_queries
|
||||
):
|
||||
|
||||
user = data_fixture.create_user()
|
||||
table, fields, rows = data_fixture.build_table(
|
||||
columns=[("number", "number")], rows=[["0"]], user=user
|
||||
)
|
||||
|
||||
num_public_views = 10
|
||||
num_fields = 100
|
||||
num_rows = 1000
|
||||
num_fields_to_filter = 3
|
||||
num_views_to_filter = 6
|
||||
num_row_updates_to_profile = 1
|
||||
|
||||
views = []
|
||||
for i in range(num_public_views):
|
||||
views.append(data_fixture.create_grid_view(user=user, table=table, public=True))
|
||||
|
||||
for i in range(num_fields):
|
||||
last_field = FieldHandler().create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
name=f"field{i}",
|
||||
type_name="text",
|
||||
)
|
||||
if i < num_fields_to_filter:
|
||||
for view in views[:num_views_to_filter]:
|
||||
data_fixture.create_view_filter(
|
||||
view=view, field=last_field, type="equal", value=i
|
||||
)
|
||||
handler = RowHandler()
|
||||
rows = []
|
||||
for i in range(num_rows):
|
||||
row = {}
|
||||
for j in range(num_fields):
|
||||
row[f"field_{j}"] = (i + j) % num_fields
|
||||
rows.append(row)
|
||||
model = table.get_model()
|
||||
for i in range(num_rows):
|
||||
rows[i] = handler.create_row(user, table, rows[i], model=model)
|
||||
profiler = Profiler()
|
||||
profiler.start()
|
||||
run_row_updates = 0
|
||||
for i in range(num_rows):
|
||||
for k in range(num_fields):
|
||||
handler.update_row(user, table, rows[i].id, {f"field_{k}": 0}, model=model)
|
||||
run_row_updates += 1
|
||||
if run_row_updates >= num_row_updates_to_profile:
|
||||
break
|
||||
if run_row_updates >= num_row_updates_to_profile:
|
||||
break
|
||||
profiler.stop()
|
||||
print(profiler.output_text(unicode=True, color=True))
|
||||
|
||||
"""
|
||||
Profiling result on 11/01/2021:
|
||||
- Dev: Nigel
|
||||
- Machine: 32gb RAM, Ubuntu 21.04, AMD 5900X
|
||||
- Variables:
|
||||
num_public_views = 10
|
||||
num_fields = 100
|
||||
num_rows = 1000
|
||||
num_fields_to_filter = 3
|
||||
num_views_to_filter = 6
|
||||
num_row_updates_to_profile = 1
|
||||
|
||||
|
||||
|
||||
0.018 test_updating_many_rows_in_public_filtered_views test_public_sharing_perform
|
||||
└─ 0.018 update_row baserow/contrib/database/rows/handler.py:426
|
||||
├─ 0.015 send django/dispatch/dispatcher.py:159
|
||||
│ [2 frames hidden] django
|
||||
│ 0.015 <listcomp> django/dispatch/dispatcher.py:180
|
||||
│ ├─ 0.009 public_before_row_update baserow/contrib/database/ws/public/rows
|
||||
│ │ ├─ 0.004 get_public_views_row_checker baserow/contrib/database/views/han
|
||||
│ │ │ └─ 0.004 __init__ baserow/contrib/database/views/handler.py:985
|
||||
│ │ │ ├─ 0.002 apply_filters baserow/contrib/database/views/handler.py:2
|
||||
│ │ │ │ └─ 0.002 apply_to_queryset baserow/contrib/database/fields/fiel
|
||||
│ │ │ │ └─ 0.002 filter django/db/models/query.py:935
|
||||
│ │ │ │ [14 frames hidden] django
|
||||
│ │ │ └─ 0.002 __iter__ django/db/models/query.py:265
|
||||
│ │ │ [16 frames hidden] django
|
||||
│ │ │ 0.001 get_prefetch_queryset django/db/models/fields/relat
|
||||
│ │ │ └─ 0.001 get_queryset baserow/core/managers.py:29
|
||||
│ │ │ └─ 0.001 filter django/db/models/query.py:935
|
||||
│ │ │ [9 frames hidden] django
|
||||
│ │ ├─ 0.003 _serialize_row baserow/contrib/database/ws/public/rows/signals
|
||||
│ │ │ ├─ 0.002 data rest_framework/serializers.py:546
|
||||
│ │ │ │ [18 frames hidden] rest_framework, django, copy, <built-in>
|
||||
│ │ │ └─ 0.001 get_row_serializer_class baserow/contrib/database/api/rows/
|
||||
│ │ │ └─ 0.001 get_response_serializer_field baserow/contrib/database/fi
|
||||
│ │ └─ 0.002 get_public_views_where_row_is_visible baserow/contrib/database/
|
||||
│ │ └─ 0.002 _check_row_visible baserow/contrib/database/views/handler.py
|
||||
│ │ └─ 0.002 exists django/db/models/query.py:806
|
||||
│ │ [19 frames hidden] django, copy
|
||||
│ ├─ 0.003 public_row_updated baserow/contrib/database/ws/public/rows/signals
|
||||
│ │ ├─ 0.002 _serialize_row baserow/contrib/database/ws/public/rows/signals.
|
||||
│ │ │ ├─ 0.001 get_row_serializer_class baserow/contrib/database/api/rows/s
|
||||
│ │ │ │ └─ 0.001 get_response_serializer_field baserow/contrib/database/fi
|
||||
│ │ │ │ └─ 0.001 get_serializer_field baserow/contrib/database/fields/f
|
||||
│ │ │ │ └─ 0.001 __init__ rest_framework/fields.py:773
|
||||
│ │ │ │ [3 frames hidden] rest_framework
|
||||
│ │ │ └─ 0.001 data rest_framework/serializers.py:546
|
||||
│ │ │ [15 frames hidden] rest_framework, django, copy, <built-in>
|
||||
│ │ └─ 0.001 on_commit django/db/transaction.py:123
|
||||
│ │ [7 frames hidden] django, asgiref
|
||||
│ └─ 0.003 before_row_update baserow/contrib/database/ws/rows/signals.py:37
|
||||
│ ├─ 0.002 data rest_framework/serializers.py:546
|
||||
│ │ [13 frames hidden] rest_framework, django, copy
|
||||
│ └─ 0.001 get_row_serializer_class baserow/contrib/database/api/rows/seri
|
||||
│ └─ 0.001 get_serializer_class baserow/api/utils.py:254
|
||||
├─ 0.002 get django/db/models/query.py:414
|
||||
│ [18 frames hidden] django
|
||||
└─ 0.001 save django/db/models/base.py:672
|
||||
[11 frames hidden] django
|
||||
|
||||
"""
|
|
@ -2,7 +2,7 @@ import pytest
|
|||
from channels.testing import WebsocketCommunicator
|
||||
|
||||
from baserow.config.asgi import application
|
||||
from baserow.ws.auth import get_user
|
||||
from baserow.ws.auth import get_user, ANONYMOUS_USER_TOKEN
|
||||
|
||||
|
||||
@pytest.mark.run(order=1)
|
||||
|
@ -16,11 +16,14 @@ async def test_get_user(data_fixture):
|
|||
u = await get_user(token)
|
||||
assert user.id == u.id
|
||||
|
||||
anonymous_token_user = await get_user(ANONYMOUS_USER_TOKEN)
|
||||
assert anonymous_token_user.is_anonymous
|
||||
|
||||
|
||||
@pytest.mark.run(order=2)
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
async def test_token_auth_middleware(data_fixture):
|
||||
async def test_token_auth_middleware(data_fixture, settings):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
|
||||
communicator = WebsocketCommunicator(application, f"ws/core/")
|
||||
|
@ -57,3 +60,27 @@ async def test_token_auth_middleware(data_fixture):
|
|||
assert json["type"] == "authentication"
|
||||
assert json["web_socket_id"] is not None
|
||||
await communicator.disconnect()
|
||||
|
||||
# Test anonymous connections
|
||||
communicator = WebsocketCommunicator(
|
||||
application, f"ws/core/?jwt_token={ANONYMOUS_USER_TOKEN}"
|
||||
)
|
||||
connected, subprotocol = await communicator.connect()
|
||||
assert connected
|
||||
json = await communicator.receive_json_from()
|
||||
assert json["type"] == "authentication"
|
||||
assert json["success"]
|
||||
assert json["web_socket_id"] is not None
|
||||
await communicator.disconnect()
|
||||
|
||||
# Test cant connect as anonymous user if feature disabled.
|
||||
settings.DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS = True
|
||||
communicator = WebsocketCommunicator(
|
||||
application, f"ws/core/?jwt_token={ANONYMOUS_USER_TOKEN}"
|
||||
)
|
||||
connected, subprotocol = await communicator.connect()
|
||||
assert connected
|
||||
json = await communicator.receive_json_from()
|
||||
assert json["type"] == "authentication"
|
||||
assert not json["success"]
|
||||
await communicator.disconnect()
|
||||
|
|
|
@ -3,6 +3,7 @@ import pytest
|
|||
from channels.testing import WebsocketCommunicator
|
||||
|
||||
from baserow.config.asgi import application
|
||||
from baserow.ws.auth import ANONYMOUS_USER_TOKEN
|
||||
|
||||
|
||||
@pytest.mark.run(order=3)
|
||||
|
@ -39,3 +40,61 @@ async def test_join_page(data_fixture):
|
|||
await communicator_1.send_json_to({"page": "NOT_EXISTING_PAGE"})
|
||||
assert communicator_1.output_queue.qsize() == 0
|
||||
await communicator_1.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.run(order=4)
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
async def test_join_page_as_anonymous_user(data_fixture):
|
||||
user_1, token_1 = data_fixture.create_user_and_token()
|
||||
table_1 = data_fixture.create_database_table(user=user_1)
|
||||
public_grid_view = data_fixture.create_grid_view(user_1, table=table_1, public=True)
|
||||
non_public_grid_view = data_fixture.create_grid_view(
|
||||
user_1, table=table_1, public=False
|
||||
)
|
||||
|
||||
communicator_1 = WebsocketCommunicator(
|
||||
application,
|
||||
f"ws/core/?jwt_token={ANONYMOUS_USER_TOKEN}",
|
||||
headers=[(b"origin", b"http://localhost")],
|
||||
)
|
||||
await communicator_1.connect()
|
||||
await communicator_1.receive_json_from()
|
||||
|
||||
# Join the public view page.
|
||||
await communicator_1.send_json_to({"page": "view", "slug": public_grid_view.slug})
|
||||
response = await communicator_1.receive_json_from(0.1)
|
||||
assert response["type"] == "page_add"
|
||||
assert response["page"] == "view"
|
||||
assert response["parameters"]["slug"] == public_grid_view.slug
|
||||
|
||||
# Cant join a table page.
|
||||
# When switching to a page where the user cannot join we expect to be discarded from
|
||||
# the current page.
|
||||
await communicator_1.send_json_to({"page": "table", "table_id": table_1.id})
|
||||
response = await communicator_1.receive_json_from(0.1)
|
||||
assert response["type"] == "page_discard"
|
||||
assert response["page"] == "view"
|
||||
assert response["parameters"]["slug"] == public_grid_view.slug
|
||||
|
||||
# Can't join a non public grid view page
|
||||
await communicator_1.send_json_to(
|
||||
{"page": "view", "slug": non_public_grid_view.slug}
|
||||
)
|
||||
assert communicator_1.output_queue.qsize() == 0
|
||||
await communicator_1.disconnect()
|
||||
|
||||
# Can't join an invalid view page
|
||||
await communicator_1.send_json_to({"page": "view", "slug": "invalid slug"})
|
||||
assert communicator_1.output_queue.qsize() == 0
|
||||
await communicator_1.disconnect()
|
||||
|
||||
# Can't join a view page without a slug
|
||||
await communicator_1.send_json_to({"page": "view"})
|
||||
assert communicator_1.output_queue.qsize() == 0
|
||||
await communicator_1.disconnect()
|
||||
|
||||
# Can't join an invalid page
|
||||
await communicator_1.send_json_to({"page": ""})
|
||||
assert communicator_1.output_queue.qsize() == 0
|
||||
await communicator_1.disconnect()
|
||||
|
|
|
@ -48,6 +48,7 @@ services:
|
|||
- EMAIL_SMTP_USER
|
||||
- EMAIL_SMTP_PASSWORD
|
||||
- FROM_EMAIL
|
||||
- DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS
|
||||
ports:
|
||||
- "${HOST_PUBLISH_IP:-127.0.0.1}:${BACKEND_PORT:-8000}:8000"
|
||||
depends_on:
|
||||
|
@ -71,6 +72,7 @@ services:
|
|||
- EMAIL_SMTP_USER
|
||||
- EMAIL_SMTP_PASSWORD
|
||||
- FROM_EMAIL
|
||||
- DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS
|
||||
build:
|
||||
dockerfile: ./backend/Dockerfile
|
||||
context: .
|
||||
|
@ -99,6 +101,7 @@ services:
|
|||
- EMAIL_SMTP_USER
|
||||
- EMAIL_SMTP_PASSWORD
|
||||
- FROM_EMAIL
|
||||
- DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS
|
||||
volumes:
|
||||
- media:/baserow/media
|
||||
networks:
|
||||
|
@ -121,6 +124,7 @@ services:
|
|||
- EMAIL_SMTP_USER
|
||||
- EMAIL_SMTP_PASSWORD
|
||||
- FROM_EMAIL
|
||||
- DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS
|
||||
volumes:
|
||||
- media:/baserow/media
|
||||
networks:
|
||||
|
@ -135,6 +139,7 @@ services:
|
|||
- PUBLIC_BACKEND_URL=${PUBLIC_BACKEND_URL:-http://localhost:8000}
|
||||
- PUBLIC_WEB_FRONTEND_URL=${PUBLIC_WEB_FRONTEND_URL:-http://localhost:3000}
|
||||
- ADDITIONAL_MODULES
|
||||
- DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS
|
||||
ports:
|
||||
- "${HOST_PUBLISH_IP:-127.0.0.1}:${WEB_FRONTEND_PORT:-3000}:3000"
|
||||
depends_on:
|
||||
|
|
|
@ -135,3 +135,5 @@ are accepted.
|
|||
* `EMAIL_SMTP_PASSWORD` (default ``): The password of the SMTP server.
|
||||
* `HOURS_UNTIL_TRASH_PERMANENTLY_DELETED` (default 72): The number of hours to keep
|
||||
trashed items until they are permanently deleted.
|
||||
* `DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS` (default ``): If set to 'true' will
|
||||
disable realtime events being sent to publicly shared views.
|
||||
|
|
|
@ -140,6 +140,7 @@ for what these variables do.
|
|||
- `EMAIL_SMTP_USER`
|
||||
- `EMAIL_SMTP_PASSWORD`
|
||||
- `FROM_EMAIL`
|
||||
- `DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS`
|
||||
|
||||
### Dev Only Variables
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ 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).order_by(
|
||||
field_options = view.get_field_options(create_if_missing=True).order_by(
|
||||
"field__id"
|
||||
)
|
||||
ids_to_update = [f.id for f in field_options[0:3]]
|
||||
|
|
|
@ -61,6 +61,10 @@ export default function CoreModule(options) {
|
|||
key: 'HOURS_UNTIL_TRASH_PERMANENTLY_DELETED',
|
||||
default: 24 * 3,
|
||||
},
|
||||
{
|
||||
key: 'DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
|
|
@ -6,6 +6,7 @@ export class RealTimeHandler {
|
|||
this.socket = null
|
||||
this.connected = false
|
||||
this.reconnect = false
|
||||
this.anonymous = false
|
||||
this.reconnectTimeout = null
|
||||
this.attempts = 0
|
||||
this.events = {}
|
||||
|
@ -21,10 +22,13 @@ export class RealTimeHandler {
|
|||
* Creates a new connection with to the web socket so that real time updates can be
|
||||
* received.
|
||||
*/
|
||||
connect(reconnect = true) {
|
||||
connect(reconnect = true, anonymous = false) {
|
||||
this.reconnect = reconnect
|
||||
this.anonymous = anonymous
|
||||
|
||||
const token = this.context.store.getters['auth/token']
|
||||
const token = anonymous
|
||||
? 'anonymous'
|
||||
: this.context.store.getters['auth/token']
|
||||
|
||||
// If the user is already connected to the web socket, we don't have to do
|
||||
// anything.
|
||||
|
@ -115,7 +119,7 @@ export class RealTimeHandler {
|
|||
|
||||
this.reconnectTimeout = setTimeout(
|
||||
() => {
|
||||
this.connect(true)
|
||||
this.connect(true, this.anonymous)
|
||||
},
|
||||
// After the first try, we want to try again every 5 seconds.
|
||||
this.attempts > 1 ? 5000 : 0
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
<Table
|
||||
:database="database"
|
||||
:table="table"
|
||||
:fields="fields"
|
||||
:primary="primary"
|
||||
:fields="fields || startingFields"
|
||||
:primary="primary || startingPrimary"
|
||||
:views="[view]"
|
||||
:view="view"
|
||||
:read-only="true"
|
||||
|
@ -18,6 +18,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Notifications from '@baserow/modules/core/components/notifications/Notifications'
|
||||
import Table from '@baserow/modules/database/components/table/Table'
|
||||
import GridService from '@baserow/modules/database/services/view/grid'
|
||||
|
@ -67,8 +68,8 @@ export default {
|
|||
database,
|
||||
table,
|
||||
view,
|
||||
primary,
|
||||
fields,
|
||||
startingFields: fields,
|
||||
startingPrimary: primary,
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.response && e.response.status === 404) {
|
||||
|
@ -78,5 +79,23 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
primary: 'field/getPrimary',
|
||||
fields: 'field/getAll',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$env.DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS) {
|
||||
this.$realtime.connect(true, true)
|
||||
this.$realtime.subscribe('view', { slug: this.$route.params.slug })
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (!this.$env.DISABLE_ANONYMOUS_PUBLIC_VIEW_WS_CONNECTIONS) {
|
||||
this.$realtime.subscribe(null)
|
||||
this.$realtime.disconnect()
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -227,6 +227,34 @@ export const registerRealtimeEvents = (realtime) => {
|
|||
}
|
||||
})
|
||||
|
||||
realtime.registerEvent('force_view_rows_refresh', ({ store, app }, data) => {
|
||||
const view = store.getters['view/get'](data.view_id)
|
||||
if (view !== undefined) {
|
||||
if (store.getters['view/getSelectedId'] === view.id) {
|
||||
app.$bus.$emit('table-refresh', {
|
||||
tableId: store.getters['table/getSelectedId'],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
realtime.registerEvent('force_view_refresh', ({ store, app }, data) => {
|
||||
const view = store.getters['view/get'](data.view_id)
|
||||
if (view !== undefined) {
|
||||
if (store.getters['view/getSelectedId'] === view.id) {
|
||||
app.$bus.$emit('table-refresh', {
|
||||
tableId: store.getters['table/getSelectedId'],
|
||||
includeFieldOptions: true,
|
||||
async callback() {
|
||||
await store.dispatch('field/forceSetFields', {
|
||||
fields: data.fields,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
realtime.registerEvent('view_filter_created', ({ store, app }, data) => {
|
||||
const view = store.getters['view/get'](data.view_filter.view)
|
||||
if (view !== undefined) {
|
||||
|
|
|
@ -347,17 +347,23 @@ export class GridViewType extends ViewType {
|
|||
}
|
||||
|
||||
async fieldRestored(
|
||||
{ dispatch },
|
||||
{ dispatch, rootGetters },
|
||||
table,
|
||||
selectedView,
|
||||
field,
|
||||
fieldType,
|
||||
storePrefix = ''
|
||||
) {
|
||||
// There might be new filters and sorts associated with the restored field,
|
||||
// ensure we fetch them. For now we have to fetch all filters/sorts however in the
|
||||
// future we should instead just fetch them for this particular restored field.
|
||||
await dispatch('view/refreshView', { view: selectedView }, { root: true })
|
||||
// Dont refresh if we are public view as filters wont be exposed and sorts are
|
||||
// treated as defaults from the server so not a big deal if we don't know what
|
||||
// it is for a restored field.
|
||||
const isPublic = rootGetters[storePrefix + 'view/grid/isPublic']
|
||||
if (!isPublic) {
|
||||
// There might be new filters and sorts associated with the restored field,
|
||||
// ensure we fetch them. For now we have to fetch all filters/sorts however in the
|
||||
// future we should instead just fetch them for this particular restored field.
|
||||
await dispatch('view/refreshView', { view: selectedView }, { root: true })
|
||||
}
|
||||
}
|
||||
|
||||
async fieldCreated({ dispatch }, table, field, fieldType, storePrefix = '') {
|
||||
|
|
|
@ -66,6 +66,8 @@ export class TestApp {
|
|||
this._realtime = {
|
||||
registerEvent(e, f) {},
|
||||
subscribe(e, f) {},
|
||||
connect(a, b) {},
|
||||
disconnect() {},
|
||||
}
|
||||
// Various stub and mock attributes which will be injected into components
|
||||
// mounted using TestApp.
|
||||
|
@ -81,6 +83,9 @@ export class TestApp {
|
|||
t: (key) => key,
|
||||
tc: (key) => key,
|
||||
},
|
||||
$route: {
|
||||
params: {},
|
||||
},
|
||||
}
|
||||
this._vueContext = bootstrapVueContext()
|
||||
this.store = _createBaserowStoreAndRegistry(this._app, this._vueContext)
|
||||
|
|
Loading…
Add table
Reference in a new issue