1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-15 01:28:30 +00:00

Resolve "Make Views Trashable"

This commit is contained in:
Alexander Haller 2022-04-29 10:37:09 +00:00
parent 9c5775656a
commit a88b57238c
26 changed files with 635 additions and 98 deletions

View file

@ -475,6 +475,7 @@ class ViewView(APIView):
"""Deletes an existing view if the user belongs to the group."""
view = ViewHandler().get_view(view_id)
ViewHandler().delete_view(request.user, view)
return Response(status=204)

View file

@ -270,11 +270,13 @@ class DatabaseConfig(AppConfig):
TableTrashableItemType,
RowTrashableItemType,
FieldTrashableItemType,
ViewTrashableItemType,
)
trash_item_type_registry.register(TableTrashableItemType())
trash_item_type_registry.register(FieldTrashableItemType())
trash_item_type_registry.register(RowTrashableItemType())
trash_item_type_registry.register(ViewTrashableItemType())
from .formula.ast.function_defs import register_formula_functions

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-04-29 10:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("database", "0068_view_public_view_password"),
]
operations = [
migrations.AddField(
model_name="view",
name="trashed",
field=models.BooleanField(db_index=True, default=False),
),
]

View file

@ -1,4 +1,3 @@
from baserow.core.mixins import make_trashable_mixin
ParentFieldTrashableModelMixin = make_trashable_mixin("field")
ParentTableTrashableModelMixin = make_trashable_mixin("table")

View file

@ -1,4 +1,4 @@
from typing import Optional, Any
from typing import Optional, Any, Dict
from django.contrib.auth import get_user_model
from django.db import connection
@ -14,6 +14,9 @@ from baserow.contrib.database.rows.signals import row_created
from baserow.contrib.database.table.models import Table, GeneratedTableModel
from baserow.contrib.database.table.signals import table_created
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.models import View
from baserow.contrib.database.views.registries import view_type_registry
from baserow.contrib.database.views.signals import view_created
from baserow.core.exceptions import TrashItemDoesNotExist
from baserow.core.models import TrashEntry
from baserow.core.trash.exceptions import RelatedTableTrashedException
@ -307,3 +310,34 @@ class RowTrashableItemType(TrashableItemType):
return primary_value
return "unknown row"
class ViewTrashableItemType(TrashableItemType):
type = "view"
model_class = View
@property
def requires_parent_id(self) -> bool:
return False
def permanently_delete_item(
self, trashed_item: View, trash_item_lookup_cache: Dict[str, View] = None
):
trashed_item.delete()
def get_parent(self, trashed_item: View, parent_id: int) -> Optional[View]:
return trashed_item.table
def restore(self, trashed_item: View, trash_entry):
super().restore(trashed_item, trash_entry)
type_name = view_type_registry.get_by_model(trashed_item.specific_class).type
view_created.send(
self,
user=trash_entry.user_who_trashed,
view=trashed_item,
type_name=type_name,
)
def get_name(self, trashed_item: View) -> str:
return trashed_item.name

View file

@ -254,7 +254,8 @@ class ViewHandler:
group.has_user(user, raise_error=True)
view_id = view.id
view.delete()
TrashHandler().trash(user, group, view.table.database, view)
view_deleted.send(self, view_id=view_id, view=view, user=user)
@ -263,6 +264,14 @@ class ViewHandler:
Updates the field options with the provided values if the field id exists in
the table related to the view.
This will also update views which are trashed. It is up to the caller to
ensure that the view is not trashed if they would like to exclude it from
the update.
It is necesarry to do so, because aggregations have to be removed
from trashed views as well if the field options change. Otherwise,
you might restore a view and the aggregation is invalid on that view.
:param view: The view for which the field options need to be updated.
:type view: View
:param field_options: A dict with the field ids as the key and a dict
@ -314,7 +323,7 @@ class ViewHandler:
raise UnrelatedFieldError(
f"The field id {field_id} is not related to the view."
)
model.objects.update_or_create(
model.objects_and_trash.update_or_create(
field_id=field_id, defaults=options, **{field_name: view}
)
@ -470,7 +479,7 @@ class ViewHandler:
)
if TrashHandler.item_has_a_trashed_parent(
view_filter.view.table, check_item_also=True
view_filter.view, check_item_also=True
):
raise ViewFilterDoesNotExist(
f"The view filter with id {view_filter_id} does not exist."
@ -650,6 +659,7 @@ class ViewHandler:
:type queryset: QuerySet
:raises ValueError: When the queryset's model is not a table model or if the
table model does not contain the one of the fields.
:raises ViewSortDoesNotExist: When the view is trashed
:param restrict_to_field_ids: Only field ids in this iterable will have their
view sorts applied in the resulting queryset.
:type restrict_to_field_ids: Optional[Iterable[int]]
@ -664,6 +674,9 @@ class ViewHandler:
if not hasattr(model, "_field_objects"):
raise ValueError("A queryset of the table model is required.")
if view.trashed:
raise ViewSortDoesNotExist(f"The view {view.id} is trashed.")
order_by = []
qs = view.viewsort_set
@ -737,9 +750,7 @@ class ViewHandler:
f"The view sort with id {view_sort_id} does not exist."
)
if TrashHandler.item_has_a_trashed_parent(
view_sort.view.table, check_item_also=True
):
if TrashHandler.item_has_a_trashed_parent(view_sort.view, check_item_also=True):
raise ViewSortDoesNotExist(
f"The view sort with id {view_sort_id} does not exist."
)
@ -823,12 +834,16 @@ class ViewHandler:
:param view_sort: The view sort that needs to be updated.
:param field: The field that must be sorted on.
:param order: Indicates the sort order direction.
:raises ViewSortDoesNotExist: When the view used by the filter is trashed.
:raises ViewSortFieldNotSupported: When the field does not support sorting.
:raises FieldNotInTable: When the provided field does not belong to the
provided view's table.
:return: The updated view sort instance.
"""
if view_sort.view.trashed:
raise ViewSortDoesNotExist(f"The view {view_sort.view.id} is trashed.")
group = view_sort.view.table.database.group
group.has_user(user, raise_error=True)

View file

@ -3,6 +3,7 @@ import secrets
from django.contrib.auth.hashers import check_password, make_password
from django.contrib.contenttypes.models import ContentType
from django.db import models, transaction
from django.db.models import Q
from baserow.core.utils import get_model_reference_field_name
from baserow.core.models import UserFile
@ -10,6 +11,7 @@ from baserow.core.mixins import (
OrderableMixin,
PolymorphicContentTypeMixin,
CreatedAndUpdatedOnMixin,
TrashableModelMixin,
)
from baserow.contrib.database.fields.field_filters import (
FILTER_TYPE_AND,
@ -20,10 +22,6 @@ from baserow.contrib.database.views.registries import (
view_type_registry,
view_filter_type_registry,
)
from baserow.contrib.database.mixins import (
ParentTableTrashableModelMixin,
ParentFieldTrashableModelMixin,
)
FILTER_TYPES = ((FILTER_TYPE_AND, "And"), (FILTER_TYPE_OR, "Or"))
@ -45,7 +43,7 @@ def get_default_view_content_type():
class View(
ParentTableTrashableModelMixin,
TrashableModelMixin,
CreatedAndUpdatedOnMixin,
OrderableMixin,
PolymorphicContentTypeMixin,
@ -228,7 +226,21 @@ class View(
return field_options
class ViewFilter(ParentFieldTrashableModelMixin, models.Model):
class ViewFilterManager(models.Manager):
"""
Manager for the ViewFilter model.
The View can be trashed and the filters are not deleted, therefore
we need to filter out the trashed views.
"""
def get_queryset(self):
trashed_Q = Q(view__trashed=True) | Q(field__trashed=True)
return super().get_queryset().filter(~trashed_Q)
class ViewFilter(models.Model):
objects = ViewFilterManager()
view = models.ForeignKey(
View,
on_delete=models.CASCADE,
@ -302,7 +314,21 @@ class ViewDecoration(OrderableMixin, models.Model):
ordering = ("order", "id")
class ViewSort(ParentFieldTrashableModelMixin, models.Model):
class ViewSortManager(models.Manager):
"""
Manager for the ViewSort model.
The View can be trashed and the sorts are not deleted, therefore
we need to filter out the trashed views.
"""
def get_queryset(self):
trashed_Q = Q(view__trashed=True) | Q(field__trashed=True)
return super().get_queryset().filter(~trashed_Q)
class ViewSort(models.Model):
objects = ViewSortManager()
view = models.ForeignKey(
View,
on_delete=models.CASCADE,
@ -330,7 +356,22 @@ class GridView(View):
field_options = models.ManyToManyField(Field, through="GridViewFieldOptions")
class GridViewFieldOptions(ParentFieldTrashableModelMixin, models.Model):
class GridViewFieldOptionsManager(models.Manager):
"""
Manager for the GridViewFieldOptions model.
The View can be trashed and the field options are not deleted, therefore
we need to filter out the trashed views.
"""
def get_queryset(self):
trashed_Q = Q(grid_view__trashed=True) | Q(field__trashed=True)
return super().get_queryset().filter(~trashed_Q)
class GridViewFieldOptions(models.Model):
objects = GridViewFieldOptionsManager()
objects_and_trash = models.Manager()
grid_view = models.ForeignKey(GridView, on_delete=models.CASCADE)
field = models.ForeignKey(Field, on_delete=models.CASCADE)
# The defaults should match the ones in `afterFieldCreated` of the `GridViewType`
@ -398,7 +439,21 @@ class GalleryView(View):
)
class GalleryViewFieldOptions(ParentFieldTrashableModelMixin, models.Model):
class GalleryViewFieldOptionsManager(models.Manager):
"""
The View can be trashed and the field options are not deleted, therefore
we need to filter out the trashed views.
"""
def get_queryset(self):
trashed_Q = Q(gallery_view__trashed=True) | Q(field__trashed=True)
return super().get_queryset().filter(~trashed_Q)
class GalleryViewFieldOptions(models.Model):
objects = GalleryViewFieldOptionsManager()
objects_and_trash = models.Manager()
gallery_view = models.ForeignKey(GalleryView, on_delete=models.CASCADE)
field = models.ForeignKey(Field, on_delete=models.CASCADE)
hidden = models.BooleanField(
@ -477,7 +532,21 @@ class FormView(View):
)
class FormViewFieldOptions(ParentFieldTrashableModelMixin, models.Model):
class FormViewFieldOptionsManager(models.Manager):
"""
The View can be trashed and the field options are not deleted, therefore
we need to filter out the trashed views.
"""
def get_queryset(self):
trashed_Q = Q(form_view__trashed=True) | Q(field__trashed=True)
return super().get_queryset().filter(~trashed_Q)
class FormViewFieldOptions(models.Model):
objects = FormViewFieldOptionsManager()
objects_and_trash = models.Manager()
form_view = models.ForeignKey(FormView, on_delete=models.CASCADE)
field = models.ForeignKey(Field, on_delete=models.CASCADE)
name = models.CharField(

View file

@ -171,7 +171,7 @@ class GridViewType(ViewType):
"""
field_options = (
GridViewFieldOptions.objects.filter(field=field)
GridViewFieldOptions.objects_and_trash.filter(field=field)
.exclude(aggregation_raw_type="")
.select_related("grid_view")
)

View file

@ -9,6 +9,7 @@ from rest_framework.status import (
from django.shortcuts import reverse
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.models import ViewFilter
from baserow.contrib.database.views.registries import (
view_type_registry,
@ -522,3 +523,40 @@ def test_list_views_including_filters(api_client, data_fixture):
assert response_json[0]["filters"][1]["id"] == filter_2.id
assert len(response_json[1]["filters"]) == 1
assert response_json[1]["filters"][0]["id"] == filter_3.id
@pytest.mark.django_db
def test_cant_update_view_filter_when_view_trashed(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
grid_view = data_fixture.create_grid_view(user=user)
view_filter = data_fixture.create_view_filter(user=user, view=grid_view)
ViewHandler().delete_view(user, grid_view)
response = api_client.patch(
reverse(
"api:database:views:filter_item", kwargs={"view_filter_id": view_filter.id}
),
data={"value": "new value"},
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
@pytest.mark.django_db
def test_cant_delete_view_filter_when_view_trashed(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
grid_view = data_fixture.create_grid_view(user=user)
view_filter = data_fixture.create_view_filter(user=user, view=grid_view)
ViewHandler().delete_view(user, grid_view)
response = api_client.delete(
reverse(
"api:database:views:filter_item", kwargs={"view_filter_id": view_filter.id}
),
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_404_NOT_FOUND

View file

@ -9,6 +9,7 @@ from rest_framework.status import (
from django.shortcuts import reverse
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.models import ViewSort
from baserow.contrib.database.views.registries import (
view_type_registry,
@ -454,3 +455,61 @@ def test_list_views_including_sortings(api_client, data_fixture):
assert response_json[0]["sortings"][1]["id"] == sort_2.id
assert len(response_json[1]["sortings"]) == 1
assert response_json[1]["sortings"][0]["id"] == sort_3.id
@pytest.mark.django_db
def test_cant_get_view_sort_when_view_trashed(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
view = data_fixture.create_form_view(table=table)
ViewHandler().delete_view(user, view)
url = reverse("api:database:views:sort_item", kwargs={"view_sort_id": view.id})
response = api_client.get(url, format="json", HTTP_AUTHORIZATION=f"JWT {token}")
assert response.status_code == HTTP_404_NOT_FOUND
@pytest.mark.django_db
def test_cant_update_view_sort_when_view_trashed(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
view = data_fixture.create_grid_view(table=table)
field = data_fixture.create_number_field(user, table=table)
view_sort = ViewHandler().create_sort(user, view, field, "asc")
ViewHandler().delete_view(user, view)
url = reverse(
"api:database:views:sort_item",
kwargs={"view_sort_id": view_sort.id},
)
response = api_client.patch(
url,
{"order": "ASC"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
@pytest.mark.django_db
def test_cant_delete_view_sort_when_view_trashed(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
view = data_fixture.create_grid_view(table=table)
field = data_fixture.create_number_field(user, table=table)
view_sort = ViewHandler().create_sort(user, view, field, "asc")
ViewHandler().delete_view(user, view)
url = reverse(
"api:database:views:sort_item",
kwargs={"view_sort_id": view_sort.id},
)
response = api_client.delete(url, format="json", HTTP_AUTHORIZATION=f"JWT {token}")
assert response.status_code == HTTP_404_NOT_FOUND

View file

@ -1409,3 +1409,23 @@ def test_trashing_two_linked_tables_after_one_perm_deleted_can_restore(
TrashHandler.permanently_delete_marked_trash()
assert not TrashEntry.objects.exists()
@pytest.mark.django_db
def test_trash_restore_view(data_fixture):
user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user, name="Placeholder")
table = data_fixture.create_database_table(name="Table 1", database=database)
view = data_fixture.create_grid_view(name="View 1", table=table)
assert view.trashed is False
TrashHandler.trash(user, database.group, database, view)
view.refresh_from_db()
assert view.trashed is True
TrashHandler.restore_item(user, "view", view.id)
view.refresh_from_db()
assert view.trashed is False

View file

@ -2,10 +2,12 @@ import pytest
import random
from decimal import Decimal
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.views.registries import view_aggregation_type_registry
from baserow.contrib.database.views.exceptions import FieldAggregationNotSupported
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.fields.exceptions import FieldNotInTable
from baserow.core.trash.handler import TrashHandler
from baserow.test_utils.helpers import setup_interesting_test_table
@ -353,3 +355,79 @@ def test_view_aggregation_errors(data_fixture):
),
],
)
@pytest.mark.django_db
def test_aggregation_is_updated_when_view_is_trashed(data_fixture):
"""
Test that aggregation is updated when view is trashed
The following scenario is tested:
- Create two views
- Creat a field in both views
- Create and aggregation in both views
- Trash that view
- Change the field type to a different type that is
incompatible with the aggregation
- Restore the view
- Check that the aggregation is updated
"""
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
grid_view_one = data_fixture.create_grid_view(table=table)
grid_view_two = data_fixture.create_grid_view(table=table)
field = data_fixture.create_number_field(user=user, table=table)
application = table.database
view_handler = ViewHandler()
model = table.get_model()
model.objects.create(**{field.db_column: 1})
model.objects.create(**{field.db_column: 2})
view_handler.update_field_options(
view=grid_view_one,
field_options={
field.id: {
"aggregation_type": "sum",
"aggregation_raw_type": "sum",
}
},
)
view_handler.update_field_options(
view=grid_view_two,
field_options={
field.id: {
"aggregation_type": "sum",
"aggregation_raw_type": "sum",
}
},
)
# Verify both views have an aggregation
aggregations_view_one = view_handler.get_view_field_aggregations(grid_view_one)
aggregations_view_two = view_handler.get_view_field_aggregations(grid_view_two)
assert field.db_column in aggregations_view_one
assert field.db_column in aggregations_view_two
# Trash the view and verify that the aggregation is not retreivable anymore
TrashHandler().trash(user, application.group, application, trash_item=grid_view_one)
aggregations = view_handler.get_view_field_aggregations(grid_view_one)
assert field.db_column not in aggregations
# Update the field and verify that the aggregation is removed from the
# not trashed view
FieldHandler().update_field(user, field, new_type_name="text")
aggregations_not_trashed_view = view_handler.get_view_field_aggregations(
grid_view_two
)
assert field.db_column not in aggregations_not_trashed_view
# Restore the view and verify that the aggregation
# is also removed from the restored view
TrashHandler().restore_item(user, "view", grid_view_one.id)
aggregations_restored_view = view_handler.get_view_field_aggregations(grid_view_one)
assert field.db_column not in aggregations_restored_view

View file

@ -1776,3 +1776,55 @@ def test_public_view_row_checker_runs_expected_queries_when_checking_rows(
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) == []
@pytest.mark.django_db
def test_cant_get_view_filter_when_view_trashed(data_fixture):
user = data_fixture.create_user()
grid_view = data_fixture.create_grid_view(user=user)
view_filter = data_fixture.create_view_filter(user=user, view=grid_view)
ViewHandler().delete_view(user, grid_view)
with pytest.raises(ViewFilterDoesNotExist):
ViewHandler().get_filter(user, view_filter.id)
@pytest.mark.django_db
def test_cant_apply_sorting_when_view_trashed(data_fixture):
user = data_fixture.create_user()
grid_view = data_fixture.create_grid_view(user=user)
ViewHandler().delete_view(user, grid_view)
with pytest.raises(ViewSortDoesNotExist):
ViewHandler().apply_sorting(
grid_view,
grid_view.table.get_model().objects.all(),
)
@pytest.mark.django_db
def test_cant_get_sort_when_view_trashed(data_fixture):
user = data_fixture.create_user()
grid_view = data_fixture.create_grid_view(user=user)
field = data_fixture.create_number_field(user, table=grid_view.table)
view_sort = ViewHandler().create_sort(user, grid_view, field, "asc")
ViewHandler().delete_view(user, grid_view)
with pytest.raises(ViewSortDoesNotExist):
ViewHandler().get_sort(user, view_sort.id)
@pytest.mark.django_db
def test_cant_update_sort_when_view_trashed(data_fixture):
user = data_fixture.create_user()
grid_view = data_fixture.create_grid_view(user=user)
field = data_fixture.create_number_field(user, table=grid_view.table)
view_sort = ViewHandler().create_sort(user, grid_view, field, "asc")
ViewHandler().delete_view(user, grid_view)
with pytest.raises(ViewSortDoesNotExist):
ViewHandler().update_sort(user, view_sort, field)

View file

@ -1,5 +1,13 @@
import pytest
from baserow.contrib.database.views.models import (
ViewFilter,
ViewSort,
GridViewFieldOptions,
GalleryViewFieldOptions,
FormViewFieldOptions,
)
@pytest.mark.django_db
def test_view_get_field_options(data_fixture):
@ -59,6 +67,144 @@ def test_rotate_view_slug(data_fixture):
assert str(form_view.slug) != old_slug
@pytest.mark.django_db
def test_view_filter_manager_view_trashed(data_fixture):
grid_view = data_fixture.create_grid_view()
data_fixture.create_view_filter(view=grid_view)
assert ViewFilter.objects.count() == 1
grid_view.trashed = True
grid_view.save()
assert ViewFilter.objects.count() == 0
@pytest.mark.django_db
def test_view_filter_manager_field_trashed(data_fixture):
grid_view = data_fixture.create_grid_view()
field = data_fixture.create_text_field(table=grid_view.table)
data_fixture.create_view_filter(view=grid_view, field=field)
assert ViewFilter.objects.count() == 1
field.trashed = True
field.save()
assert ViewFilter.objects.count() == 0
@pytest.mark.django_db
def test_view_sort_manager_view_trashed(data_fixture):
grid_view = data_fixture.create_grid_view()
data_fixture.create_view_sort(view=grid_view)
assert ViewSort.objects.count() == 1
grid_view.trashed = True
grid_view.save()
assert ViewSort.objects.count() == 0
@pytest.mark.django_db
def test_view_sort_manager_field_trashed(data_fixture):
grid_view = data_fixture.create_grid_view()
field = data_fixture.create_text_field(table=grid_view.table)
data_fixture.create_view_sort(view=grid_view, field=field)
assert ViewSort.objects.count() == 1
field.trashed = True
field.save()
assert ViewSort.objects.count() == 0
@pytest.mark.django_db
def test_grid_view_field_options_manager_view_trashed(data_fixture):
grid_view = data_fixture.create_grid_view()
field = data_fixture.create_text_field(table=grid_view.table)
data_fixture.create_grid_view_field_option(grid_view, field)
assert GridViewFieldOptions.objects.count() == 1
grid_view.trashed = True
grid_view.save()
assert GridViewFieldOptions.objects.count() == 0
@pytest.mark.django_db
def test_grid_view_field_options_manager_field_trashed(data_fixture):
grid_view = data_fixture.create_grid_view()
field = data_fixture.create_text_field(table=grid_view.table)
data_fixture.create_grid_view_field_option(grid_view, field)
assert GridViewFieldOptions.objects.count() == 1
field.trashed = True
field.save()
assert GridViewFieldOptions.objects.count() == 0
@pytest.mark.django_db
def test_gallery_view_field_options_manager_view_trashed(data_fixture):
gallery_view = data_fixture.create_gallery_view()
field = data_fixture.create_text_field(table=gallery_view.table)
data_fixture.create_gallery_view_field_option(gallery_view, field)
assert GalleryViewFieldOptions.objects.count() == 1
gallery_view.trashed = True
gallery_view.save()
assert GalleryViewFieldOptions.objects.count() == 0
@pytest.mark.django_db
def test_gallery_view_field_options_manager_field_trashed(data_fixture):
gallery_view = data_fixture.create_gallery_view()
field = data_fixture.create_text_field(table=gallery_view.table)
data_fixture.create_gallery_view_field_option(gallery_view, field)
assert GalleryViewFieldOptions.objects.count() == 1
field.trashed = True
field.save()
assert GalleryViewFieldOptions.objects.count() == 0
@pytest.mark.django_db
def test_form_view_field_options_manager_view_trashed(data_fixture):
form_view = data_fixture.create_form_view()
field = data_fixture.create_text_field(table=form_view.table)
data_fixture.create_form_view_field_option(form_view, field)
assert FormViewFieldOptions.objects.count() == 1
form_view.trashed = True
form_view.save()
assert FormViewFieldOptions.objects.count() == 0
@pytest.mark.django_db
def test_form_view_field_options_manager_field_trashed(data_fixture):
form_view = data_fixture.create_form_view()
field = data_fixture.create_text_field(table=form_view.table)
data_fixture.create_form_view_field_option(form_view, field)
assert FormViewFieldOptions.objects.count() == 1
field.trashed = True
field.save()
assert FormViewFieldOptions.objects.count() == 0
@pytest.mark.django_db
def test_public_view_password(data_fixture):
form_view = data_fixture.create_form_view()

View file

@ -41,6 +41,7 @@
* Added Spanish and Italian languages.
* Added undo/redo.
* Added password protection for publicly shared grids and forms.
* Made views trashable.
## Released (2022-03-03 1.9.1)

View file

@ -1,8 +1,8 @@
from django.db import models
from django.db.models import Q
from baserow.contrib.database.fields.models import Field, FileField, SingleSelectField
from baserow.contrib.database.views.models import View
from baserow.contrib.database.mixins import ParentFieldTrashableModelMixin
class KanbanView(View):
@ -30,7 +30,21 @@ class KanbanView(View):
db_table = "database_kanbanview"
class KanbanViewFieldOptions(ParentFieldTrashableModelMixin, models.Model):
class KanbanViewFieldOptionsManager(models.Manager):
"""
The View can be trashed and the field options are not deleted, therefore
we need to filter out the trashed views.
"""
def get_queryset(self):
trashed_Q = Q(kanban_view__trashed=True) | Q(field__trashed=True)
return super().get_queryset().filter(~trashed_Q)
class KanbanViewFieldOptions(models.Model):
objects = KanbanViewFieldOptionsManager()
objects_and_trash = models.Manager()
kanban_view = models.ForeignKey(KanbanView, on_delete=models.CASCADE)
field = models.ForeignKey(Field, on_delete=models.CASCADE)
hidden = models.BooleanField(

View file

@ -0,0 +1,41 @@
import pytest
from baserow_premium.views.models import KanbanViewFieldOptions
@pytest.mark.django_db
def test_kanban_view_field_options_manager_view_trashed(premium_data_fixture):
kanban_view = premium_data_fixture.create_kanban_view()
# create_kanban_view already creates a view field option
# which we don't want to test here
KanbanViewFieldOptions.objects.all().delete()
field = premium_data_fixture.create_text_field(table=kanban_view.table)
premium_data_fixture.create_kanban_view_field_option(kanban_view, field)
assert KanbanViewFieldOptions.objects.count() == 1
kanban_view.trashed = True
kanban_view.save()
assert KanbanViewFieldOptions.objects.count() == 0
@pytest.mark.django_db
def test_kanban_view_field_options_manager_field_trashed(premium_data_fixture):
kanban_view = premium_data_fixture.create_kanban_view()
# create_kanban_view already creates a view field option
# which we don't want to test here
KanbanViewFieldOptions.objects.all().delete()
field = premium_data_fixture.create_text_field(table=kanban_view.table)
premium_data_fixture.create_kanban_view_field_option(kanban_view, field)
assert KanbanViewFieldOptions.objects.count() == 1
field.trashed = True
field.save()
assert KanbanViewFieldOptions.objects.count() == 0

View file

@ -154,7 +154,8 @@
"application": "",
"table": "",
"field": "",
"row": ""
"row": "",
"view": ""
},
"webhook": {
"request": "",

View file

@ -154,7 +154,8 @@
"application": "Applikation",
"table": "Tabelle",
"field": "Feld",
"row": "Zeile"
"row": "Zeile",
"view": "Ansicht"
},
"webhook": {
"request": "Anfrage",

View file

@ -155,7 +155,8 @@
"application": "application",
"table": "table",
"field": "field",
"row": "row"
"row": "row",
"view": "view"
},
"webhook": {
"request": "Request",

View file

@ -154,7 +154,8 @@
"application": "aplicación",
"table": "tabla",
"field": "campo",
"row": "fila"
"row": "fila",
"view": "vista"
},
"webhook": {
"request": "Requerimiento",

View file

@ -154,7 +154,8 @@
"application": "application",
"table": "table",
"field": "champ",
"row": "ligne"
"row": "ligne",
"view": "vue"
},
"webhook": {
"request": "Requête",

View file

@ -154,7 +154,8 @@
"application": "applicazione",
"table": "tabella",
"field": "campo",
"row": "riga"
"row": "riga",
"view": "vista"
},
"webhook": {
"request": "Richiesta",

View file

@ -154,7 +154,8 @@
"application": "applicatie",
"table": "tabel",
"field": "veld",
"row": "rij"
"row": "rij",
"view": "weergave"
},
"webhook": {
"request": "Verzoek",

View file

@ -1,63 +0,0 @@
<template>
<Modal>
<h2 class="box__title">
{{ $t('deleteViewModal.title', { name: view.name }) }}
</h2>
<Error :error="error"></Error>
<div>
<i18n path="deleteViewModal.description" tag="p">
<template #name>
<strong>{{ view.name }}</strong>
</template>
</i18n>
<div class="actions">
<div class="align-right">
<button
class="button button--large button--error"
:class="{ 'button--loading': loading }"
:disabled="loading"
@click="deleteView()"
>
{{ $t('deleteViewModal.delete') }}
</button>
</div>
</div>
</div>
</Modal>
</template>
<script>
import modal from '@baserow/modules/core/mixins/modal'
import error from '@baserow/modules/core/mixins/error'
export default {
name: 'DeleteViewModal',
mixins: [modal, error],
props: {
view: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
}
},
methods: {
async deleteView() {
this.hideError()
this.loading = true
try {
await this.$store.dispatch('view/delete', this.view)
this.hide()
} catch (error) {
this.handleError(error, 'view')
}
this.loading = false
},
},
}
</script>

View file

@ -26,7 +26,6 @@
</a>
</li>
</ul>
<DeleteViewModal ref="deleteViewModal" :view="view" />
<ExportTableModal ref="exportViewModal" :table="table" :view="view" />
<WebhookModal ref="webhookModal" :table="table" />
</Context>
@ -34,16 +33,16 @@
<script>
import context from '@baserow/modules/core/mixins/context'
import error from '@baserow/modules/core/mixins/error'
import viewTypeHasExporterTypes from '@baserow/modules/database/utils/viewTypeHasExporterTypes'
import ExportTableModal from '@baserow/modules/database/components/export/ExportTableModal'
import DeleteViewModal from './DeleteViewModal'
import WebhookModal from '@baserow/modules/database/components/webhook/WebhookModal.vue'
export default {
name: 'ViewContext',
components: { DeleteViewModal, ExportTableModal, WebhookModal },
mixins: [context],
components: { ExportTableModal, WebhookModal },
mixins: [context, error],
props: {
view: {
type: Object,
@ -67,9 +66,16 @@ export default {
this.$refs.context.hide()
this.$emit('enable-rename')
},
deleteView() {
this.$refs.context.hide()
this.$refs.deleteViewModal.show()
async deleteView() {
this.setLoading(this.view, true)
try {
await this.$store.dispatch('view/delete', this.view)
} catch (error) {
this.handleError(error, 'view')
}
this.setLoading(this.view, false)
},
exportView() {
this.$refs.context.hide()