mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-04 13:15:24 +00:00
Merge branch '710-sharegridview-migrate-slug-public-from-form-view-to-all-views' into 'develop'
Resolve "📨 ShareGridView: Migrate slug+public from form view to all views" Closes #710 See merge request bramw/baserow!459
This commit is contained in:
commit
3e9fb65840
22 changed files with 585 additions and 192 deletions
backend
src/baserow/contrib/database
api/views
migrations
views
tests/baserow/contrib/database
web-frontend/modules/database
|
@ -61,3 +61,8 @@ ERROR_VIEW_DOES_NOT_SUPPORT_FIELD_OPTIONS = (
|
|||
HTTP_400_BAD_REQUEST,
|
||||
"This view model does not support field options.",
|
||||
)
|
||||
ERROR_CANNOT_SHARE_VIEW_TYPE = (
|
||||
"ERROR_CANNOT_SHARE_VIEW_TYPE",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"This view type does not support sharing.",
|
||||
)
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
from django.urls import re_path
|
||||
|
||||
from .views import (
|
||||
RotateFormViewSlugView,
|
||||
SubmitFormViewView,
|
||||
FormViewLinkRowFieldLookupView,
|
||||
)
|
||||
|
||||
|
||||
app_name = "baserow.contrib.database.api.views.form"
|
||||
|
||||
urlpatterns = [
|
||||
re_path(
|
||||
r"(?P<view_id>[0-9]+)/rotate-slug/$",
|
||||
RotateFormViewSlugView.as_view(),
|
||||
name="rotate_slug",
|
||||
),
|
||||
re_path(
|
||||
r"(?P<slug>[-\w]+)/submit/$",
|
||||
SubmitFormViewView.as_view(),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.fields import empty
|
||||
|
@ -9,12 +9,10 @@ from django.contrib.contenttypes.models import ContentType
|
|||
from django.conf import settings
|
||||
|
||||
from baserow.api.decorators import map_exceptions
|
||||
from baserow.api.errors import ERROR_USER_NOT_IN_GROUP
|
||||
from baserow.api.schemas import get_error_schema
|
||||
from baserow.api.utils import validate_data
|
||||
from baserow.api.pagination import PageNumberPagination
|
||||
from baserow.api.serializers import get_example_pagination_serializer_class
|
||||
from baserow.contrib.database.api.views.errors import ERROR_VIEW_DOES_NOT_EXIST
|
||||
from baserow.contrib.database.api.rows.serializers import (
|
||||
get_row_serializer_class,
|
||||
get_example_row_serializer_class,
|
||||
|
@ -25,60 +23,12 @@ from baserow.contrib.database.fields.models import LinkRowField
|
|||
from baserow.contrib.database.fields.exceptions import FieldDoesNotExist
|
||||
from baserow.contrib.database.views.exceptions import ViewDoesNotExist
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
from baserow.contrib.database.views.models import FormView, FormViewFieldOptions
|
||||
from baserow.contrib.database.views.registries import view_type_registry
|
||||
from baserow.contrib.database.views.models import FormViewFieldOptions, FormView
|
||||
from baserow.contrib.database.views.validators import required_validator
|
||||
from baserow.core.exceptions import UserNotInGroup
|
||||
|
||||
from .errors import ERROR_FORM_DOES_NOT_EXIST
|
||||
from .serializers import PublicFormViewSerializer, FormViewSubmittedSerializer
|
||||
|
||||
form_view_serializer_class = view_type_registry.get("form").get_serializer_class()
|
||||
|
||||
|
||||
class RotateFormViewSlugView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="view_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
required=True,
|
||||
description="Rotates the slug of the form view related to the provided "
|
||||
"value.",
|
||||
)
|
||||
],
|
||||
tags=["Database table form view"],
|
||||
operation_id="rotate_database_table_form_view_slug",
|
||||
description=(
|
||||
"Rotates the unique slug of the form view by replacing it with a new "
|
||||
"value. This would mean that the publicly shared URL of the form will "
|
||||
"change. Everyone that knew the URL won't have access to the form anymore."
|
||||
),
|
||||
request=None,
|
||||
responses={
|
||||
200: form_view_serializer_class(many=True),
|
||||
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
|
||||
404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]),
|
||||
},
|
||||
)
|
||||
@map_exceptions(
|
||||
{
|
||||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST,
|
||||
}
|
||||
)
|
||||
@transaction.atomic
|
||||
def post(self, request, view_id):
|
||||
"""Rotates the slug of a form view."""
|
||||
|
||||
handler = ViewHandler()
|
||||
form = ViewHandler().get_view(view_id, FormView)
|
||||
form = handler.rotate_form_view_slug(request.user, form)
|
||||
return Response(form_view_serializer_class(form).data)
|
||||
|
||||
|
||||
class SubmitFormViewView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
@ -111,7 +61,9 @@ class SubmitFormViewView(APIView):
|
|||
}
|
||||
)
|
||||
def get(self, request, slug):
|
||||
form = ViewHandler().get_public_form_view_by_slug(request.user, slug)
|
||||
form = ViewHandler().get_public_view_by_slug(
|
||||
request.user, slug, view_model=FormView
|
||||
)
|
||||
serializer = PublicFormViewSerializer(form)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
@ -147,7 +99,7 @@ class SubmitFormViewView(APIView):
|
|||
@transaction.atomic
|
||||
def post(self, request, slug):
|
||||
handler = ViewHandler()
|
||||
form = handler.get_public_form_view_by_slug(request.user, slug)
|
||||
form = handler.get_public_view_by_slug(request.user, slug, view_model=FormView)
|
||||
model = form.table.get_model()
|
||||
|
||||
options = form.active_field_options
|
||||
|
@ -215,7 +167,7 @@ class FormViewLinkRowFieldLookupView(APIView):
|
|||
)
|
||||
def get(self, request, slug, field_id):
|
||||
handler = ViewHandler()
|
||||
form = handler.get_public_form_view_by_slug(request.user, slug)
|
||||
form = handler.get_public_view_by_slug(request.user, slug, view_model=FormView)
|
||||
link_row_field_content_type = ContentType.objects.get_for_model(LinkRowField)
|
||||
|
||||
try:
|
||||
|
|
|
@ -171,7 +171,10 @@ class ViewSerializer(serializers.ModelSerializer):
|
|||
"sortings",
|
||||
"filters_disabled",
|
||||
)
|
||||
extra_kwargs = {"id": {"read_only": True}, "table_id": {"read_only": True}}
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"table_id": {"read_only": True},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
context = kwargs.setdefault("context", {})
|
||||
|
|
|
@ -11,6 +11,7 @@ from .views import (
|
|||
ViewSortingsView,
|
||||
ViewSortView,
|
||||
ViewFieldOptionsView,
|
||||
RotateViewSlugView,
|
||||
)
|
||||
|
||||
|
||||
|
@ -43,4 +44,9 @@ urlpatterns = view_type_registry.api_urls + [
|
|||
ViewFieldOptionsView.as_view(),
|
||||
name="field_options",
|
||||
),
|
||||
re_path(
|
||||
r"(?P<view_id>[0-9]+)/rotate-slug/$",
|
||||
RotateViewSlugView.as_view(),
|
||||
name="rotate_slug",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -46,6 +46,7 @@ from baserow.contrib.database.views.exceptions import (
|
|||
ViewSortFieldNotSupported,
|
||||
UnrelatedFieldError,
|
||||
ViewDoesNotSupportFieldOptions,
|
||||
CannotShareViewTypeError,
|
||||
)
|
||||
|
||||
from .serializers import (
|
||||
|
@ -72,6 +73,7 @@ from .errors import (
|
|||
ERROR_VIEW_SORT_FIELD_NOT_SUPPORTED,
|
||||
ERROR_UNRELATED_FIELD,
|
||||
ERROR_VIEW_DOES_NOT_SUPPORT_FIELD_OPTIONS,
|
||||
ERROR_CANNOT_SHARE_VIEW_TYPE,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1040,3 +1042,55 @@ class ViewFieldOptionsView(APIView):
|
|||
|
||||
serializer = serializer_class(view)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class RotateViewSlugView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="view_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
required=True,
|
||||
description="Rotates the slug of the view related to the provided "
|
||||
"value.",
|
||||
)
|
||||
],
|
||||
tags=["Database table views"],
|
||||
operation_id="rotate_database_view_slug",
|
||||
description=(
|
||||
"Rotates the unique slug of the view by replacing it with a new "
|
||||
"value. This would mean that the publicly shared URL of the view will "
|
||||
"change. Anyone with the old URL won't be able to access the view"
|
||||
"anymore. Only view types which are sharable can have their slugs rotated."
|
||||
),
|
||||
request=None,
|
||||
responses={
|
||||
200: DiscriminatorCustomFieldsMappingSerializer(
|
||||
view_type_registry,
|
||||
ViewSerializer,
|
||||
),
|
||||
400: get_error_schema(
|
||||
["ERROR_USER_NOT_IN_GROUP", "ERROR_CANNOT_SHARE_VIEW_TYPE"]
|
||||
),
|
||||
404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]),
|
||||
},
|
||||
)
|
||||
@map_exceptions(
|
||||
{
|
||||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST,
|
||||
CannotShareViewTypeError: ERROR_CANNOT_SHARE_VIEW_TYPE,
|
||||
}
|
||||
)
|
||||
@transaction.atomic
|
||||
def post(self, request, view_id):
|
||||
"""Rotates the slug of a view."""
|
||||
|
||||
handler = ViewHandler()
|
||||
view = ViewHandler().get_view(view_id)
|
||||
view = handler.rotate_view_slug(request.user, view)
|
||||
serializer = view_type_registry.get_serializer(view, ViewSerializer)
|
||||
return Response(serializer.data)
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
# Generated by Django 3.2.6 on 2021-12-14 09:16
|
||||
import math
|
||||
import secrets
|
||||
|
||||
from django.db import migrations, models
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def update_batch_size():
|
||||
# Exists as a function purely so tests can mock the return_value
|
||||
return 1000
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
FormView = apps.get_model("database", "FormView")
|
||||
|
||||
print("Migrating view public and slug properties back to form view...")
|
||||
for f in FormView.objects.all():
|
||||
view = f.view_ptr
|
||||
f.public = view.public_temp
|
||||
f.slug = view.slug_temp
|
||||
f.save()
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
def forward(apps, schema_editor):
|
||||
FormView = apps.get_model("database", "FormView")
|
||||
View = apps.get_model("database", "View")
|
||||
|
||||
_copy_slug_and_public_from_form_to_view(FormView, View)
|
||||
_generate_slugs_for_views(View)
|
||||
|
||||
|
||||
def _copy_slug_and_public_from_form_to_view(FormView, View):
|
||||
print("Migrating form view public and slug properties to View...")
|
||||
updated_form_views = []
|
||||
for f in FormView.objects.all():
|
||||
view = f.view_ptr
|
||||
view.public_temp = f.public
|
||||
view.slug_temp = f.slug
|
||||
updated_form_views.append(view)
|
||||
View.objects.bulk_update(updated_form_views, fields=["public_temp", "slug_temp"])
|
||||
print("Done with form view")
|
||||
|
||||
|
||||
def _generate_slugs_for_views(View):
|
||||
views_to_generate_slugs_for = View.objects.filter(slug_temp__isnull=True)
|
||||
view_count = views_to_generate_slugs_for.count()
|
||||
batch_size = update_batch_size()
|
||||
updated_views = []
|
||||
|
||||
# Use tqdm in manual mode as it doesn't work nicely wrapping generators like
|
||||
# .iterator()
|
||||
with tqdm(
|
||||
total=math.ceil(view_count / batch_size),
|
||||
desc=f"Generating slugs for {view_count} views in batches of {batch_size}",
|
||||
) as pbar:
|
||||
for view in views_to_generate_slugs_for.iterator():
|
||||
view.slug_temp = secrets.token_urlsafe()
|
||||
updated_views.append(view)
|
||||
if len(updated_views) >= batch_size:
|
||||
View.objects.bulk_update(updated_views, fields=["slug_temp"])
|
||||
updated_views.clear()
|
||||
pbar.update(1)
|
||||
View.objects.bulk_update(updated_views, fields=["slug_temp"])
|
||||
pbar.update(1)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("database", "0052_table_order_and_id_index"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="view",
|
||||
name="public_temp",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Indicates whether the view is publicly accessible to "
|
||||
"visitors.",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="view",
|
||||
name="slug_temp",
|
||||
field=models.SlugField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="The unique slug where the view can be accessed publicly on.",
|
||||
unique=True,
|
||||
db_index=True,
|
||||
),
|
||||
),
|
||||
# Ensure that the slug is nullable on the formview so when migrating backwards
|
||||
# so that the field is created filled with nulls which we then copy
|
||||
# view.slug into. The reverse of this AlterField will set null=False,
|
||||
# blank=False, default=secrets.token_urlsafe.
|
||||
migrations.AlterField(
|
||||
model_name="formview",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="The unique slug where the form can be accessed "
|
||||
"publicly on.",
|
||||
null=True,
|
||||
blank=True,
|
||||
unique=True,
|
||||
db_index=True,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(forward, reverse),
|
||||
migrations.AlterField(
|
||||
model_name="view",
|
||||
name="slug_temp",
|
||||
field=models.SlugField(
|
||||
help_text="The unique slug where the view can be accessed publicly on.",
|
||||
default=secrets.token_urlsafe,
|
||||
unique=True,
|
||||
db_index=True,
|
||||
),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="formview",
|
||||
name="public",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="formview",
|
||||
name="slug",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="view",
|
||||
old_name="public_temp",
|
||||
new_name="public",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="view",
|
||||
old_name="slug_temp",
|
||||
new_name="slug",
|
||||
),
|
||||
]
|
|
@ -8,6 +8,10 @@ class ViewDoesNotExist(Exception):
|
|||
"""Raised when trying to get a view that doesn't exist."""
|
||||
|
||||
|
||||
class CannotShareViewTypeError(Exception):
|
||||
"""Raised when trying to a share a view that cannot be shared"""
|
||||
|
||||
|
||||
class ViewNotInTable(Exception):
|
||||
"""Raised when a provided view does not belong to a table."""
|
||||
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
from django.db.models import F
|
||||
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||
from django.db.models import F
|
||||
|
||||
from baserow.contrib.database.fields.exceptions import FieldNotInTable
|
||||
from baserow.contrib.database.fields.field_filters import FilterBuilder
|
||||
from baserow.contrib.database.fields.field_sortings import AnnotatedOrder
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.rows.signals import row_created
|
||||
from baserow.core.trash.handler import TrashHandler
|
||||
from baserow.core.utils import (
|
||||
extract_allowed,
|
||||
set_allowed_attrs,
|
||||
get_model_reference_field_name,
|
||||
)
|
||||
from baserow.contrib.database.fields.exceptions import FieldNotInTable
|
||||
from baserow.contrib.database.fields.field_filters import FilterBuilder
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.contrib.database.fields.field_sortings import AnnotatedOrder
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.contrib.database.rows.signals import row_created
|
||||
from .exceptions import (
|
||||
ViewDoesNotExist,
|
||||
ViewNotInTable,
|
||||
|
@ -26,9 +26,9 @@ from .exceptions import (
|
|||
ViewSortFieldAlreadyExist,
|
||||
ViewSortFieldNotSupported,
|
||||
ViewDoesNotSupportFieldOptions,
|
||||
CannotShareViewTypeError,
|
||||
)
|
||||
from .validators import EMPTY_VALUES
|
||||
from .models import View, ViewFilter, ViewSort, FormView
|
||||
from .models import View, ViewFilter, ViewSort
|
||||
from .registries import view_type_registry, view_filter_type_registry
|
||||
from .signals import (
|
||||
view_created,
|
||||
|
@ -43,6 +43,7 @@ from .signals import (
|
|||
view_sort_deleted,
|
||||
view_field_options_updated,
|
||||
)
|
||||
from .validators import EMPTY_VALUES
|
||||
|
||||
|
||||
class ViewHandler:
|
||||
|
@ -782,55 +783,65 @@ class ViewHandler:
|
|||
queryset = queryset.search_all_fields(search)
|
||||
return queryset
|
||||
|
||||
def rotate_form_view_slug(self, user, form):
|
||||
def rotate_view_slug(self, user, view):
|
||||
"""
|
||||
Rotates the slug of the provided form view.
|
||||
Rotates the slug of the provided view.
|
||||
|
||||
:param user: The user on whose behalf the form view is updated.
|
||||
:param user: The user on whose behalf the view is updated.
|
||||
:type user: User
|
||||
:param form: The form view instance that needs to be updated.
|
||||
:type form: View
|
||||
:param view: The form view instance that needs to be updated.
|
||||
:type view: View
|
||||
:return: The updated view instance.
|
||||
:rtype: View
|
||||
:raises CannotShareViewTypeError: Raised if called for a view which does not
|
||||
support sharing.
|
||||
"""
|
||||
|
||||
view_type = view_type_registry.get_by_model(view.specific_class)
|
||||
if not view_type.can_share:
|
||||
raise CannotShareViewTypeError()
|
||||
|
||||
group = view.table.database.group
|
||||
group.has_user(user, raise_error=True)
|
||||
|
||||
view.rotate_slug()
|
||||
view.save()
|
||||
|
||||
view_updated.send(self, view=view, user=user)
|
||||
|
||||
return view
|
||||
|
||||
def get_public_view_by_slug(self, user, slug, view_model=None):
|
||||
"""
|
||||
Returns the view with the provided slug if it is public or if the user has
|
||||
access to the views group.
|
||||
|
||||
:param user: The user on whose behalf the view is requested.
|
||||
:type user: User
|
||||
:param slug: The slug of the view.
|
||||
:type slug: str
|
||||
:param view_model: If provided that models objects are used to select the
|
||||
view. This can for example be useful when you want to select a GridView or
|
||||
other child of the View model.
|
||||
:type view_model: Type[View]
|
||||
:return: The requested view with matching slug.
|
||||
:rtype: View
|
||||
"""
|
||||
|
||||
if not isinstance(form, FormView):
|
||||
raise ValueError("The provided form is not an instance of FormView.")
|
||||
|
||||
group = form.table.database.group
|
||||
group.has_user(user, raise_error=True)
|
||||
|
||||
form.rotate_slug()
|
||||
form.save()
|
||||
|
||||
view_updated.send(self, view=form, user=user)
|
||||
|
||||
return form
|
||||
|
||||
def get_public_form_view_by_slug(self, user, slug):
|
||||
"""
|
||||
Returns the form view related to the provided slug if the form related to the
|
||||
slug is public or if the user has access to the related group.
|
||||
|
||||
:param user: The user on whose behalf the form is requested.
|
||||
:type user: User
|
||||
:param slug: The slug of the form view.
|
||||
:type slug: str
|
||||
:return: The requested form view that belongs to the form with the slug.
|
||||
:rtype: FormView
|
||||
"""
|
||||
if not view_model:
|
||||
view_model = View
|
||||
|
||||
try:
|
||||
form = FormView.objects.get(slug=slug)
|
||||
except (FormView.DoesNotExist, ValidationError):
|
||||
raise ViewDoesNotExist("The form does not exist.")
|
||||
view = view_model.objects.get(slug=slug)
|
||||
except (view_model.DoesNotExist, ValidationError):
|
||||
raise ViewDoesNotExist("The view does not exist.")
|
||||
|
||||
if not form.public and (
|
||||
not user or not form.table.database.group.has_user(user)
|
||||
if not view.public and (
|
||||
not user or not view.table.database.group.has_user(user)
|
||||
):
|
||||
raise ViewDoesNotExist("The form does not exist.")
|
||||
raise ViewDoesNotExist("The view does not exist.")
|
||||
|
||||
return form
|
||||
return view
|
||||
|
||||
def submit_form_view(self, form, values, model=None, enabled_field_options=None):
|
||||
"""
|
||||
|
|
|
@ -70,6 +70,19 @@ class View(
|
|||
help_text="Allows users to see results unfiltered while still keeping "
|
||||
"the filters saved for the view.",
|
||||
)
|
||||
slug = models.SlugField(
|
||||
default=secrets.token_urlsafe,
|
||||
help_text="The unique slug where the view can be accessed publicly on.",
|
||||
unique=True,
|
||||
db_index=True,
|
||||
)
|
||||
public = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Indicates whether the view is publicly accessible to visitors.",
|
||||
)
|
||||
|
||||
def rotate_slug(self):
|
||||
self.slug = secrets.token_urlsafe()
|
||||
|
||||
class Meta:
|
||||
ordering = ("order",)
|
||||
|
@ -247,17 +260,6 @@ class GalleryViewFieldOptions(ParentFieldTrashableModelMixin, models.Model):
|
|||
|
||||
class FormView(View):
|
||||
field_options = models.ManyToManyField(Field, through="FormViewFieldOptions")
|
||||
slug = models.SlugField(
|
||||
default=secrets.token_urlsafe,
|
||||
help_text="The unique slug where the form can be accessed publicly on.",
|
||||
unique=True,
|
||||
db_index=True,
|
||||
)
|
||||
public = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Indicates whether the form is publicly accessible to visitors and "
|
||||
"if they can fill it out.",
|
||||
)
|
||||
title = models.TextField(
|
||||
blank=True,
|
||||
help_text="The title that is displayed at the beginning of the form.",
|
||||
|
@ -301,9 +303,6 @@ class FormView(View):
|
|||
f"form.",
|
||||
)
|
||||
|
||||
def rotate_slug(self):
|
||||
self.slug = secrets.token_urlsafe()
|
||||
|
||||
@property
|
||||
def active_field_options(self):
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Callable, Union, List
|
||||
|
||||
from django.contrib.auth.models import User as DjangoUser
|
||||
from rest_framework.fields import CharField
|
||||
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
|
@ -81,6 +82,11 @@ class ViewType(
|
|||
sort to the view.
|
||||
"""
|
||||
|
||||
can_share = False
|
||||
"""
|
||||
Indicates if the view supports being shared via a public link.
|
||||
"""
|
||||
|
||||
field_options_model_class = None
|
||||
"""
|
||||
The model class of the through table that contains the field options. The model
|
||||
|
@ -94,6 +100,23 @@ class ViewType(
|
|||
option changes.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.can_share:
|
||||
self.allowed_fields = self.allowed_fields + ["public"]
|
||||
self.serializer_field_names = self.serializer_field_names + [
|
||||
"public",
|
||||
"slug",
|
||||
]
|
||||
self.serializer_field_overrides = {
|
||||
**self.serializer_field_overrides,
|
||||
"slug": CharField(
|
||||
read_only=True,
|
||||
help_text="The unique slug that can be used to construct a public "
|
||||
"URL.",
|
||||
),
|
||||
}
|
||||
|
||||
def export_serialized(self, view, files_zip, storage):
|
||||
"""
|
||||
Exports the view to a serialized dict that can be imported by the
|
||||
|
@ -138,6 +161,9 @@ class ViewType(
|
|||
for sort in view.viewsort_set.all()
|
||||
]
|
||||
|
||||
if self.can_share:
|
||||
serialized["public"] = view.public
|
||||
|
||||
return serialized
|
||||
|
||||
def import_serialized(
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
from django.urls import path, include
|
||||
|
||||
from rest_framework.serializers import CharField
|
||||
|
||||
from baserow.api.user_files.serializers import UserFileField
|
||||
from baserow.core.user_files.handler import UserFileHandler
|
||||
from baserow.contrib.database.api.views.form.errors import (
|
||||
ERROR_FORM_VIEW_FIELD_TYPE_IS_NOT_SUPPORTED,
|
||||
)
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.contrib.database.api.views.grid.serializers import (
|
||||
GridViewFieldOptionsSerializer,
|
||||
)
|
||||
from baserow.contrib.database.api.views.gallery.serializers import (
|
||||
GalleryViewFieldOptionsSerializer,
|
||||
)
|
||||
from baserow.contrib.database.api.views.form.serializers import (
|
||||
FormViewFieldOptionsSerializer,
|
||||
)
|
||||
|
||||
from baserow.contrib.database.api.views.gallery.serializers import (
|
||||
GalleryViewFieldOptionsSerializer,
|
||||
)
|
||||
from baserow.contrib.database.api.views.grid.serializers import (
|
||||
GridViewFieldOptionsSerializer,
|
||||
)
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.core.user_files.handler import UserFileHandler
|
||||
from .exceptions import FormViewFieldTypeIsNotSupported
|
||||
from .handler import ViewHandler
|
||||
from .models import (
|
||||
GridView,
|
||||
|
@ -28,7 +26,6 @@ from .models import (
|
|||
FormViewFieldOptions,
|
||||
)
|
||||
from .registries import ViewType
|
||||
from .exceptions import FormViewFieldTypeIsNotSupported
|
||||
|
||||
|
||||
class GridViewType(ViewType):
|
||||
|
@ -207,10 +204,10 @@ class FormViewType(ViewType):
|
|||
model_class = FormView
|
||||
can_filter = False
|
||||
can_sort = False
|
||||
can_share = True
|
||||
field_options_model_class = FormViewFieldOptions
|
||||
field_options_serializer_class = FormViewFieldOptionsSerializer
|
||||
allowed_fields = [
|
||||
"public",
|
||||
"title",
|
||||
"description",
|
||||
"cover_image",
|
||||
|
@ -220,8 +217,6 @@ class FormViewType(ViewType):
|
|||
"submit_action_redirect_url",
|
||||
]
|
||||
serializer_field_names = [
|
||||
"slug",
|
||||
"public",
|
||||
"title",
|
||||
"description",
|
||||
"cover_image",
|
||||
|
@ -231,10 +226,6 @@ class FormViewType(ViewType):
|
|||
"submit_action_redirect_url",
|
||||
]
|
||||
serializer_field_overrides = {
|
||||
"slug": CharField(
|
||||
read_only=True,
|
||||
help_text="The unique slug that can be used to construct a public URL.",
|
||||
),
|
||||
"cover_image": UserFileField(
|
||||
required=False,
|
||||
help_text="The cover image that must be displayed at the top of the form.",
|
||||
|
@ -291,7 +282,6 @@ class FormViewType(ViewType):
|
|||
|
||||
return {"name": name, "original_name": user_file.original_name}
|
||||
|
||||
serialized["public"] = form.public
|
||||
serialized["title"] = form.title
|
||||
serialized["description"] = form.description
|
||||
serialized["cover_image"] = add_user_file(form.cover_image)
|
||||
|
|
|
@ -192,35 +192,6 @@ def test_update_form_view(api_client, data_fixture):
|
|||
assert response_json["logo_image"]["name"] == user_file_2.name
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rotate_slug(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)
|
||||
view_2 = data_fixture.create_form_view()
|
||||
old_slug = str(view.slug)
|
||||
|
||||
url = reverse("api:database:views:form:rotate_slug", kwargs={"view_id": view_2.id})
|
||||
response = api_client.post(url, format="json", HTTP_AUTHORIZATION=f"JWT {token}")
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||
|
||||
url = reverse("api:database:views:form:rotate_slug", kwargs={"view_id": 99999})
|
||||
response = api_client.post(url, format="json", HTTP_AUTHORIZATION=f"JWT {token}")
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
|
||||
url = reverse("api:database:views:form:rotate_slug", kwargs={"view_id": view.id})
|
||||
response = api_client.post(
|
||||
url,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json["slug"] != old_slug
|
||||
assert len(response_json["slug"]) == 43
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_meta_submit_form_view(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
|
|
|
@ -646,6 +646,18 @@ def test_create_grid_view(api_client, data_fixture):
|
|||
assert "filters" not in response_json
|
||||
assert "sortings" not in response_json
|
||||
|
||||
# Can't create a public non sharable view.
|
||||
response = api_client.post(
|
||||
reverse("api:database:views:list", kwargs={"table_id": table.id}),
|
||||
{"name": "Test 1", "type": "grid", "public": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert "public" not in response_json
|
||||
assert "slug" not in response_json
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_grid_view(api_client, data_fixture):
|
||||
|
@ -733,3 +745,15 @@ def test_update_grid_view(api_client, data_fixture):
|
|||
assert response_json["filters_disabled"] is True
|
||||
assert response_json["filters"][0]["id"] == filter_1.id
|
||||
assert response_json["sortings"] == []
|
||||
|
||||
# Can't make a non sharable view public.
|
||||
response = api_client.patch(
|
||||
reverse("api:database:views:item", kwargs={"view_id": view.id}),
|
||||
{"public": True},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert "public" not in response_json
|
||||
assert "slug" not in response_json
|
||||
|
|
|
@ -1313,3 +1313,38 @@ def test_patch_view_field_options(api_client, data_fixture):
|
|||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response_json["error"] == "ERROR_VIEW_DOES_NOT_SUPPORT_FIELD_OPTIONS"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rotate_slug(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)
|
||||
view_2 = data_fixture.create_form_view(public=True)
|
||||
grid_view = data_fixture.create_grid_view(user=user, table=table)
|
||||
old_slug = str(view.slug)
|
||||
|
||||
url = reverse("api:database:views:rotate_slug", kwargs={"view_id": view_2.id})
|
||||
response = api_client.post(url, format="json", HTTP_AUTHORIZATION=f"JWT {token}")
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||
|
||||
url = reverse("api:database:views:rotate_slug", kwargs={"view_id": grid_view.id})
|
||||
response = api_client.post(url, format="json", HTTP_AUTHORIZATION=f"JWT {token}")
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_CANNOT_SHARE_VIEW_TYPE"
|
||||
|
||||
url = reverse("api:database:views:rotate_slug", kwargs={"view_id": 99999})
|
||||
response = api_client.post(url, format="json", HTTP_AUTHORIZATION=f"JWT {token}")
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
|
||||
url = reverse("api:database:views:rotate_slug", kwargs={"view_id": view.id})
|
||||
response = api_client.post(
|
||||
url,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json["slug"] != old_slug
|
||||
assert len(response_json["slug"]) == 43
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
import secrets
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
from django.db import connection
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_forwards_migration(data_fixture, transactional_db):
|
||||
migrate_from = [("database", "0052_table_order_and_id_index")]
|
||||
migrate_to = [("database", "0053_add_and_move_public_flags")]
|
||||
|
||||
old_state = 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.
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
FormView = old_state.apps.get_model("database", "FormView")
|
||||
GridView = old_state.apps.get_model("database", "GridView")
|
||||
ContentType = old_state.apps.get_model("contenttypes", "ContentType")
|
||||
form_content_type_id = ContentType.objects.get_for_model(FormView).id
|
||||
grid_content_type_id = ContentType.objects.get_for_model(GridView).id
|
||||
form_view = FormView.objects.create(
|
||||
table_id=table.id,
|
||||
name="a",
|
||||
order=1,
|
||||
public=True,
|
||||
slug="slug",
|
||||
content_type_id=form_content_type_id,
|
||||
)
|
||||
form_view2 = FormView.objects.create(
|
||||
table_id=table.id,
|
||||
name="b",
|
||||
order=1,
|
||||
public=False,
|
||||
slug="slug2",
|
||||
content_type_id=form_content_type_id,
|
||||
)
|
||||
grid_view = GridView.objects.create(
|
||||
table_id=table.id,
|
||||
name="c",
|
||||
order=1,
|
||||
content_type_id=grid_content_type_id,
|
||||
)
|
||||
grid_view2 = GridView.objects.create(
|
||||
table_id=table.id,
|
||||
name="d",
|
||||
order=2,
|
||||
content_type_id=grid_content_type_id,
|
||||
)
|
||||
new_state = migrate(migrate_to)
|
||||
NewFormView = new_state.apps.get_model("database", "FormView")
|
||||
NewGridView = new_state.apps.get_model("database", "GridView")
|
||||
|
||||
new_form_view = NewFormView.objects.get(id=form_view.id)
|
||||
assert new_form_view.view_ptr.public
|
||||
assert new_form_view.view_ptr.slug == form_view.slug
|
||||
|
||||
new_form_view2 = NewFormView.objects.get(id=form_view2.id)
|
||||
assert not new_form_view2.view_ptr.public
|
||||
assert new_form_view2.view_ptr.slug == form_view2.slug
|
||||
|
||||
new_grid_view = NewGridView.objects.get(id=grid_view.id)
|
||||
assert not new_grid_view.view_ptr.public
|
||||
assert new_grid_view.view_ptr.slug is not None
|
||||
assert len(new_grid_view.view_ptr.slug) == len(secrets.token_urlsafe())
|
||||
|
||||
new_grid_view2 = NewGridView.objects.get(id=grid_view2.id)
|
||||
assert not new_grid_view2.view_ptr.public
|
||||
assert new_grid_view2.view_ptr.slug is not None
|
||||
assert len(new_grid_view2.view_ptr.slug) == len(secrets.token_urlsafe())
|
||||
assert new_grid_view.view_ptr.slug != new_grid_view2.view_ptr.slug
|
||||
|
||||
# We need to apply the latest migration otherwise other tests might fail.
|
||||
call_command("migrate", verbosity=0, database=DEFAULT_DB_ALIAS)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch(
|
||||
"baserow.contrib.database.migrations.0053_add_and_move_public_flags"
|
||||
".update_batch_size"
|
||||
)
|
||||
def test_multi_batch_forwards_migration(
|
||||
patched_update_size, data_fixture, transactional_db
|
||||
):
|
||||
migrate_from = [("database", "0052_table_order_and_id_index")]
|
||||
migrate_to = [("database", "0053_add_and_move_public_flags")]
|
||||
|
||||
old_state = 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.
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
GridView = old_state.apps.get_model("database", "GridView")
|
||||
ContentType = old_state.apps.get_model("contenttypes", "ContentType")
|
||||
grid_content_type_id = ContentType.objects.get_for_model(GridView).id
|
||||
size = 3
|
||||
patched_update_size.return_value = size
|
||||
views_to_make = size * 2 + int(size / 2)
|
||||
for i in range(views_to_make):
|
||||
GridView.objects.create(
|
||||
table_id=table.id,
|
||||
name=str(i),
|
||||
order=1,
|
||||
content_type_id=grid_content_type_id,
|
||||
)
|
||||
new_state = migrate(migrate_to)
|
||||
NewGridView = new_state.apps.get_model("database", "GridView")
|
||||
|
||||
assert not NewGridView.objects.filter(slug__isnull=True).exists()
|
||||
assert not NewGridView.objects.filter(public=True).exists()
|
||||
# We need to apply the latest migration otherwise other tests might fail.
|
||||
call_command("migrate", verbosity=0, database=DEFAULT_DB_ALIAS)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_backwards_migration(data_fixture, transactional_db):
|
||||
migrate_from = [("database", "0053_add_and_move_public_flags")]
|
||||
migrate_to = [("database", "0052_table_order_and_id_index")]
|
||||
|
||||
old_state = 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.
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
FormView = old_state.apps.get_model("database", "FormView")
|
||||
ContentType = old_state.apps.get_model("contenttypes", "ContentType")
|
||||
content_type_id = ContentType.objects.get_for_model(FormView).id
|
||||
form_view = FormView.objects.create(
|
||||
table_id=table.id,
|
||||
name="a",
|
||||
order=1,
|
||||
public=True,
|
||||
slug="slug",
|
||||
content_type_id=content_type_id,
|
||||
)
|
||||
form_view2 = FormView.objects.create(
|
||||
table_id=table.id,
|
||||
name="b",
|
||||
order=2,
|
||||
public=False,
|
||||
slug="slug2",
|
||||
content_type_id=content_type_id,
|
||||
)
|
||||
new_state = migrate(migrate_to)
|
||||
NewFormView = new_state.apps.get_model("database", "FormView")
|
||||
new_form_view = NewFormView.objects.get(id=form_view.id)
|
||||
new_form_view2 = NewFormView.objects.get(id=form_view2.id)
|
||||
assert new_form_view.public
|
||||
assert new_form_view.slug == form_view.slug
|
||||
assert not new_form_view2.public
|
||||
assert new_form_view2.slug == form_view2.slug
|
||||
|
||||
# We need to apply the latest migration otherwise other tests might fail.
|
||||
call_command("migrate", verbosity=0, database=DEFAULT_DB_ALIAS)
|
||||
|
||||
|
||||
def migrate(target):
|
||||
executor = MigrationExecutor(connection)
|
||||
executor.loader.build_graph() # reload.
|
||||
executor.migrate(target)
|
||||
new_state = executor.loader.project_state(target)
|
||||
return new_state
|
|
@ -32,6 +32,7 @@ from baserow.contrib.database.views.exceptions import (
|
|||
ViewSortFieldNotSupported,
|
||||
ViewDoesNotSupportFieldOptions,
|
||||
FormViewFieldTypeIsNotSupported,
|
||||
CannotShareViewTypeError,
|
||||
)
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
|
@ -1263,22 +1264,23 @@ def test_delete_sort(send_mock, data_fixture):
|
|||
|
||||
@pytest.mark.django_db
|
||||
@patch("baserow.contrib.database.views.signals.view_updated.send")
|
||||
def test_rotate_form_view_slug(send_mock, data_fixture):
|
||||
def test_rotate_view_slug(send_mock, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
form = data_fixture.create_form_view(table=table)
|
||||
grid = data_fixture.create_grid_view(table=table)
|
||||
old_slug = str(form.slug)
|
||||
|
||||
handler = ViewHandler()
|
||||
|
||||
with pytest.raises(UserNotInGroup):
|
||||
handler.rotate_form_view_slug(user=user_2, form=form)
|
||||
handler.rotate_view_slug(user=user_2, view=form)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
handler.rotate_form_view_slug(user=user, form=object())
|
||||
with pytest.raises(CannotShareViewTypeError):
|
||||
handler.rotate_view_slug(user=user, view=grid)
|
||||
|
||||
handler.rotate_form_view_slug(user=user, form=form)
|
||||
handler.rotate_view_slug(user=user, view=form)
|
||||
|
||||
send_mock.assert_called_once()
|
||||
assert send_mock.call_args[1]["view"].id == form.id
|
||||
|
@ -1290,7 +1292,7 @@ def test_rotate_form_view_slug(send_mock, data_fixture):
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_public_form_view_by_slug(data_fixture):
|
||||
def test_get_public_view_by_slug(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
form = data_fixture.create_form_view(user=user)
|
||||
|
@ -1298,25 +1300,27 @@ def test_get_public_form_view_by_slug(data_fixture):
|
|||
handler = ViewHandler()
|
||||
|
||||
with pytest.raises(ViewDoesNotExist):
|
||||
handler.get_public_form_view_by_slug(user_2, "not_existing")
|
||||
handler.get_public_view_by_slug(user_2, "not_existing")
|
||||
|
||||
with pytest.raises(ViewDoesNotExist):
|
||||
handler.get_public_form_view_by_slug(
|
||||
user_2, "a3f1493a-9229-4889-8531-6a65e745602e"
|
||||
)
|
||||
handler.get_public_view_by_slug(user_2, "a3f1493a-9229-4889-8531-6a65e745602e")
|
||||
|
||||
with pytest.raises(ViewDoesNotExist):
|
||||
handler.get_public_form_view_by_slug(user_2, form.slug)
|
||||
handler.get_public_view_by_slug(user_2, form.slug)
|
||||
|
||||
form2 = handler.get_public_form_view_by_slug(user, form.slug)
|
||||
form2 = handler.get_public_view_by_slug(user, form.slug)
|
||||
assert form.id == form2.id
|
||||
|
||||
form.public = True
|
||||
form.save()
|
||||
|
||||
form2 = handler.get_public_form_view_by_slug(user_2, form.slug)
|
||||
form2 = handler.get_public_view_by_slug(user_2, form.slug)
|
||||
assert form.id == form2.id
|
||||
|
||||
form3 = handler.get_public_view_by_slug(user_2, form.slug, view_model=FormView)
|
||||
assert form.id == form3.id
|
||||
assert isinstance(form3, FormView)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("baserow.contrib.database.rows.signals.row_created.send")
|
||||
|
|
|
@ -54,7 +54,7 @@ def test_view_get_field_options(data_fixture):
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rotate_form_view_slug(data_fixture):
|
||||
def test_rotate_view_slug(data_fixture):
|
||||
form_view = data_fixture.create_form_view()
|
||||
old_slug = str(form_view.slug)
|
||||
form_view.rotate_slug()
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
* **dev.sh users** Fixed bug in dev.sh where UID/GID were not being set correctly,
|
||||
please rebuild any dev images you are using.
|
||||
* Replaced the table `order` index with an `order, id` index to improve performance.
|
||||
* **breaking change** The API endpoint to rotate a form views slug has been moved to
|
||||
`/database/views/${viewId}/rotate-slug/`.
|
||||
|
||||
## Released (2021-11-25)
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
import modal from '@baserow/modules/core/mixins/modal'
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import formViewHelpers from '@baserow/modules/database/mixins/formViewHelpers'
|
||||
import FormService from '@baserow/modules/database/services/view/form'
|
||||
import ViewService from '@baserow/modules/database/services/view'
|
||||
|
||||
export default {
|
||||
name: 'FormViewRotateSlugModal',
|
||||
|
@ -51,7 +51,7 @@ export default {
|
|||
this.loading = true
|
||||
|
||||
try {
|
||||
const { data } = await FormService(this.$client).rotateSlug(
|
||||
const { data } = await ViewService(this.$client).rotateSlug(
|
||||
this.view.id
|
||||
)
|
||||
await this.$store.dispatch('view/forceUpdate', {
|
||||
|
|
|
@ -58,5 +58,8 @@ export default (client) => {
|
|||
updateFieldOptions({ viewId, values }) {
|
||||
return client.patch(`/database/views/${viewId}/field-options/`, values)
|
||||
},
|
||||
rotateSlug(viewId) {
|
||||
return client.post(`/database/views/${viewId}/rotate-slug/`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
export default (client) => {
|
||||
return {
|
||||
rotateSlug(formId) {
|
||||
return client.post(`/database/views/form/${formId}/rotate-slug/`)
|
||||
},
|
||||
getMetaInformation(slug) {
|
||||
return client.get(`/database/views/form/${slug}/submit/`)
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue