1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-26 13:44:41 +00:00

Resolve "Allow to create sub domains"

This commit is contained in:
Alexander Haller 2023-09-18 08:29:09 +00:00
parent 645eb09ed4
commit 55533d021e
41 changed files with 865 additions and 175 deletions

View file

@ -1184,6 +1184,11 @@ if POSTHOG_ENABLED:
else: else:
posthog.disabled = True posthog.disabled = True
BASEROW_BUILDER_DOMAINS = os.getenv("BASEROW_BUILDER_DOMAINS", None)
BASEROW_BUILDER_DOMAINS = (
BASEROW_BUILDER_DOMAINS.split(",") if BASEROW_BUILDER_DOMAINS is not None else []
)
# Indicates whether we are running the tests or not. Set to True in the test.py settings # Indicates whether we are running the tests or not. Set to True in the test.py settings
# file used by pytest.ini # file used by pytest.ini
TESTS = False TESTS = False

View file

@ -11,3 +11,16 @@ ERROR_DOMAIN_NOT_IN_BUILDER = (
HTTP_400_BAD_REQUEST, HTTP_400_BAD_REQUEST,
"The domain id {e.domain_id} does not belong to the builder.", "The domain id {e.domain_id} does not belong to the builder.",
) )
ERROR_DOMAIN_NAME_NOT_UNIQUE = (
"ERROR_DOMAIN_NAME_NOT_UNIQUE",
HTTP_400_BAD_REQUEST,
"The domain name {e.domain_name} already exists.",
)
ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME = (
"ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME",
HTTP_400_BAD_REQUEST,
"The subdomain {e.domain_name} has an invalid domain name, "
"you can only use {e.available_domain_names}",
)

View file

@ -1,5 +1,7 @@
from typing import List from typing import List
from django.utils.functional import lazy
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
@ -8,6 +10,7 @@ from baserow.api.services.serializers import PublicServiceSerializer
from baserow.contrib.builder.api.pages.serializers import PathParamSerializer from baserow.contrib.builder.api.pages.serializers import PathParamSerializer
from baserow.contrib.builder.data_sources.models import DataSource from baserow.contrib.builder.data_sources.models import DataSource
from baserow.contrib.builder.domains.models import Domain from baserow.contrib.builder.domains.models import Domain
from baserow.contrib.builder.domains.registries import domain_type_registry
from baserow.contrib.builder.elements.models import Element from baserow.contrib.builder.elements.models import Element
from baserow.contrib.builder.elements.registries import element_type_registry from baserow.contrib.builder.elements.registries import element_type_registry
from baserow.contrib.builder.models import Builder from baserow.contrib.builder.models import Builder
@ -16,9 +19,15 @@ from baserow.core.services.registries import service_type_registry
class DomainSerializer(serializers.ModelSerializer): class DomainSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField(help_text="The type of the domain.")
@extend_schema_field(OpenApiTypes.STR)
def get_type(self, instance):
return domain_type_registry.get_by_model(instance.specific_class).type
class Meta: class Meta:
model = Domain model = Domain
fields = ("id", "domain_name", "order", "builder_id", "last_published") fields = ("id", "type", "domain_name", "order", "builder_id", "last_published")
extra_kwargs = { extra_kwargs = {
"id": {"read_only": True}, "id": {"read_only": True},
"builder_id": {"read_only": True}, "builder_id": {"read_only": True},
@ -27,9 +36,37 @@ class DomainSerializer(serializers.ModelSerializer):
class CreateDomainSerializer(serializers.ModelSerializer): class CreateDomainSerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(
choices=lazy(domain_type_registry.get_types, list)(),
required=True,
help_text="The type of the domain.",
)
class Meta: class Meta:
model = Domain model = Domain
fields = ("domain_name",) fields = (
"type",
"domain_name",
)
class UpdateDomainSerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(
choices=lazy(domain_type_registry.get_types, list)(),
required=False,
help_text="The type of the domain.",
)
domain_name = serializers.CharField(
required=False, help_text=Domain._meta.get_field("domain_name").help_text
)
class Meta:
model = Domain
fields = (
"type",
"domain_name",
)
class OrderDomainsSerializer(serializers.Serializer): class OrderDomainsSerializer(serializers.Serializer):

View file

@ -10,29 +10,43 @@ from rest_framework.status import HTTP_202_ACCEPTED
from rest_framework.views import APIView from rest_framework.views import APIView
from baserow.api.applications.errors import ERROR_APPLICATION_DOES_NOT_EXIST from baserow.api.applications.errors import ERROR_APPLICATION_DOES_NOT_EXIST
from baserow.api.decorators import map_exceptions, validate_body from baserow.api.decorators import (
map_exceptions,
validate_body,
validate_body_custom_fields,
)
from baserow.api.jobs.serializers import JobSerializer from baserow.api.jobs.serializers import JobSerializer
from baserow.api.schemas import CLIENT_SESSION_ID_SCHEMA_PARAMETER, get_error_schema from baserow.api.schemas import CLIENT_SESSION_ID_SCHEMA_PARAMETER, get_error_schema
from baserow.api.utils import DiscriminatorCustomFieldsMappingSerializer from baserow.api.utils import (
DiscriminatorCustomFieldsMappingSerializer,
type_from_data_or_registry,
validate_data_custom_fields,
)
from baserow.contrib.builder.api.data_sources.serializers import DataSourceSerializer from baserow.contrib.builder.api.data_sources.serializers import DataSourceSerializer
from baserow.contrib.builder.api.domains.errors import ( from baserow.contrib.builder.api.domains.errors import (
ERROR_DOMAIN_DOES_NOT_EXIST, ERROR_DOMAIN_DOES_NOT_EXIST,
ERROR_DOMAIN_NAME_NOT_UNIQUE,
ERROR_DOMAIN_NOT_IN_BUILDER, ERROR_DOMAIN_NOT_IN_BUILDER,
ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME,
) )
from baserow.contrib.builder.api.domains.serializers import ( from baserow.contrib.builder.api.domains.serializers import (
CreateDomainSerializer, CreateDomainSerializer,
DomainSerializer, DomainSerializer,
OrderDomainsSerializer, OrderDomainsSerializer,
PublicBuilderSerializer, PublicBuilderSerializer,
UpdateDomainSerializer,
) )
from baserow.contrib.builder.api.pages.errors import ERROR_PAGE_DOES_NOT_EXIST from baserow.contrib.builder.api.pages.errors import ERROR_PAGE_DOES_NOT_EXIST
from baserow.contrib.builder.data_sources.service import DataSourceService from baserow.contrib.builder.data_sources.service import DataSourceService
from baserow.contrib.builder.domains.exceptions import ( from baserow.contrib.builder.domains.exceptions import (
DomainDoesNotExist, DomainDoesNotExist,
DomainNameNotUniqueError,
DomainNotInBuilder, DomainNotInBuilder,
SubDomainHasInvalidDomainName,
) )
from baserow.contrib.builder.domains.handler import DomainHandler from baserow.contrib.builder.domains.handler import DomainHandler
from baserow.contrib.builder.domains.models import Domain from baserow.contrib.builder.domains.models import Domain
from baserow.contrib.builder.domains.registries import domain_type_registry
from baserow.contrib.builder.domains.service import DomainService from baserow.contrib.builder.domains.service import DomainService
from baserow.contrib.builder.elements.registries import element_type_registry from baserow.contrib.builder.elements.registries import element_type_registry
from baserow.contrib.builder.elements.service import ElementService from baserow.contrib.builder.elements.service import ElementService
@ -66,9 +80,13 @@ class DomainsView(APIView):
tags=["Builder domains"], tags=["Builder domains"],
operation_id="create_builder_domain", operation_id="create_builder_domain",
description="Creates a new domain for an application builder", description="Creates a new domain for an application builder",
request=CreateDomainSerializer, request=DiscriminatorCustomFieldsMappingSerializer(
domain_type_registry, CreateDomainSerializer, request=True
),
responses={ responses={
200: DomainSerializer, 200: DiscriminatorCustomFieldsMappingSerializer(
domain_type_registry, DomainSerializer
),
400: get_error_schema( 400: get_error_schema(
[ [
"ERROR_USER_NOT_IN_GROUP", "ERROR_USER_NOT_IN_GROUP",
@ -82,17 +100,22 @@ class DomainsView(APIView):
@map_exceptions( @map_exceptions(
{ {
ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST, ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST,
SubDomainHasInvalidDomainName: ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME,
} }
) )
@validate_body(CreateDomainSerializer) @validate_body_custom_fields(
domain_type_registry, base_serializer_class=CreateDomainSerializer
)
def post(self, request, data: Dict, builder_id: int): def post(self, request, data: Dict, builder_id: int):
builder = BuilderHandler().get_builder(builder_id) builder = BuilderHandler().get_builder(builder_id)
type_name = data.pop("type")
domain_type = domain_type_registry.get(type_name)
domain = DomainService().create_domain( domain = DomainService().create_domain(
request.user, builder, data["domain_name"] request.user, domain_type, builder, **data
) )
serializer = DomainSerializer(domain) serializer = domain_type_registry.get_serializer(domain, DomainSerializer)
return Response(serializer.data) return Response(serializer.data)
@extend_schema( @extend_schema(
@ -109,7 +132,9 @@ class DomainsView(APIView):
operation_id="get_builder_domains", operation_id="get_builder_domains",
description="Gets all the domains of a builder", description="Gets all the domains of a builder",
responses={ responses={
200: DomainSerializer(many=True), 200: DiscriminatorCustomFieldsMappingSerializer(
domain_type_registry, DomainSerializer, many=True
),
400: get_error_schema( 400: get_error_schema(
[ [
"ERROR_USER_NOT_IN_GROUP", "ERROR_USER_NOT_IN_GROUP",
@ -132,9 +157,12 @@ class DomainsView(APIView):
builder, builder,
) )
serializer = DomainSerializer(domains, many=True) data = [
domain_type_registry.get_serializer(domain, DomainSerializer).data
for domain in domains
]
return Response(serializer.data) return Response(data)
class DomainView(APIView): class DomainView(APIView):
@ -151,9 +179,11 @@ class DomainView(APIView):
tags=["Builder domains"], tags=["Builder domains"],
operation_id="update_builder_domain", operation_id="update_builder_domain",
description="Updates an existing domain of an application builder", description="Updates an existing domain of an application builder",
request=CreateDomainSerializer, request=UpdateDomainSerializer,
responses={ responses={
200: DomainSerializer, 200: DiscriminatorCustomFieldsMappingSerializer(
domain_type_registry, DomainSerializer
),
400: get_error_schema( 400: get_error_schema(
[ [
"ERROR_USER_NOT_IN_GROUP", "ERROR_USER_NOT_IN_GROUP",
@ -167,19 +197,35 @@ class DomainView(APIView):
@map_exceptions( @map_exceptions(
{ {
DomainDoesNotExist: ERROR_DOMAIN_DOES_NOT_EXIST, DomainDoesNotExist: ERROR_DOMAIN_DOES_NOT_EXIST,
DomainNameNotUniqueError: ERROR_DOMAIN_NAME_NOT_UNIQUE,
SubDomainHasInvalidDomainName: ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME,
} }
) )
@validate_body(CreateDomainSerializer) def patch(self, request, domain_id: int):
def patch(self, request, data: Dict, domain_id: int): base_queryset = Domain.objects
base_queryset = Domain.objects.select_for_update(of=("self",))
domain = DomainService().get_domain( domain = (
request.user, domain_id, base_queryset=base_queryset DomainService()
.get_domain(request.user, domain_id, base_queryset=base_queryset)
.specific
)
domain_type = type_from_data_or_registry(
request.data, domain_type_registry, domain
)
data = validate_data_custom_fields(
domain_type.type,
domain_type_registry,
request.data,
base_serializer_class=UpdateDomainSerializer,
partial=True,
) )
domain_updated = DomainService().update_domain(request.user, domain, **data) domain_updated = DomainService().update_domain(request.user, domain, **data)
serializer = DomainSerializer(domain_updated) serializer = domain_type_registry.get_serializer(
domain_updated, DomainSerializer
)
return Response(serializer.data) return Response(serializer.data)
@extend_schema( @extend_schema(

View file

@ -151,6 +151,12 @@ class BuilderConfig(AppConfig):
element_type_registry.register(InputTextElementType()) element_type_registry.register(InputTextElementType())
element_type_registry.register(ColumnElementType()) element_type_registry.register(ColumnElementType())
from .domains.domain_types import CustomDomainType, SubDomainType
from .domains.registries import domain_type_registry
domain_type_registry.register(CustomDomainType())
domain_type_registry.register(SubDomainType())
from .domains.trash_types import DomainTrashableItemType from .domains.trash_types import DomainTrashableItemType
trash_item_type_registry.register(DomainTrashableItemType()) trash_item_type_registry.register(DomainTrashableItemType())

View file

@ -0,0 +1,41 @@
from typing import Dict
from django.conf import settings
from baserow.contrib.builder.domains.exceptions import SubDomainHasInvalidDomainName
from baserow.contrib.builder.domains.models import CustomDomain, SubDomain
from baserow.contrib.builder.domains.registries import DomainType
class CustomDomainType(DomainType):
type = "custom"
model_class = CustomDomain
class SubDomainType(DomainType):
type = "sub_domain"
model_class = SubDomain
def prepare_values(self, values: Dict) -> Dict:
domain_name = values.get("domain_name", None)
if domain_name is not None:
self._validate_domain_name(domain_name)
return values
def _validate_domain_name(self, domain_name: str):
"""
Checks if the subdomain uses a domain name that is among the available domain
names defined in settings.
:param domain_name: The name that is being proposed
:raises SubDomainHasInvalidDomainName: If the domain name is not registered
"""
for domain in settings.BASEROW_BUILDER_DOMAINS:
if domain_name.endswith(f".{domain}"):
# The domain suffix is valid
return
raise SubDomainHasInvalidDomainName(domain_name)

View file

@ -1,3 +1,6 @@
from django.conf import settings
class DomainDoesNotExist(Exception): class DomainDoesNotExist(Exception):
"""Raised when trying to get a domain that doesn't exist.""" """Raised when trying to get a domain that doesn't exist."""
@ -12,3 +15,27 @@ class DomainNotInBuilder(Exception):
*args, *args,
**kwargs, **kwargs,
) )
class DomainNameNotUniqueError(Exception):
"""Raised when trying to set a domain name that already exists"""
def __init__(self, domain_name, *args, **kwargs):
self.domain_name = domain_name
super().__init__(
f"The domain name {domain_name} already exists", *args, **kwargs
)
class SubDomainHasInvalidDomainName(Exception):
"""Raised when a subdomain is using an invalid domain name"""
def __init__(self, domain_name, *args, **kwargs):
self.domain_name = domain_name
self.available_domain_names = settings.BASEROW_BUILDER_DOMAINS
super().__init__(
f"The subdomain {domain_name} has an invalid domain name, you can only use "
f"{self.available_domain_names}",
*args,
**kwargs,
)

View file

@ -1,23 +1,30 @@
from typing import List from typing import Iterable, List, cast
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.db.models import QuerySet from django.db.models import QuerySet
from django.db.utils import IntegrityError
from django.utils import timezone from django.utils import timezone
from baserow.contrib.builder.domains.exceptions import ( from baserow.contrib.builder.domains.exceptions import (
DomainDoesNotExist, DomainDoesNotExist,
DomainNameNotUniqueError,
DomainNotInBuilder, DomainNotInBuilder,
) )
from baserow.contrib.builder.domains.models import Domain from baserow.contrib.builder.domains.models import Domain
from baserow.contrib.builder.domains.registries import DomainType
from baserow.contrib.builder.exceptions import BuilderDoesNotExist from baserow.contrib.builder.exceptions import BuilderDoesNotExist
from baserow.contrib.builder.models import Builder from baserow.contrib.builder.models import Builder
from baserow.core.db import specific_iterator
from baserow.core.exceptions import IdDoesNotExist from baserow.core.exceptions import IdDoesNotExist
from baserow.core.registries import ImportExportConfig, application_type_registry from baserow.core.registries import ImportExportConfig, application_type_registry
from baserow.core.trash.handler import TrashHandler from baserow.core.trash.handler import TrashHandler
from baserow.core.utils import Progress from baserow.core.utils import Progress, extract_allowed
class DomainHandler: class DomainHandler:
allowed_fields_create = ["domain_name"]
allowed_fields_update = ["domain_name", "last_published"]
def get_domain(self, domain_id: int, base_queryset: QuerySet = None) -> Domain: def get_domain(self, domain_id: int, base_queryset: QuerySet = None) -> Domain:
""" """
Gets a domain by ID Gets a domain by ID
@ -39,20 +46,20 @@ class DomainHandler:
def get_domains( def get_domains(
self, builder: Builder, base_queryset: QuerySet = None self, builder: Builder, base_queryset: QuerySet = None
) -> QuerySet[Domain]: ) -> Iterable[Domain]:
""" """
Gets all the domains of a builder. Gets all the domains of a builder.
:param builder: The builder we are trying to get all domains for :param builder: The builder we are trying to get all domains for
:param base_queryset: Can be provided to already filter or apply performance :param base_queryset: Can be provided to already filter or apply performance
improvements to the queryset when it's being executed improvements to the queryset when it's being executed
:return: A queryset with all the domains :return: An iterable of all the specific domains
""" """
if base_queryset is None: if base_queryset is None:
base_queryset = Domain.objects base_queryset = Domain.objects
return base_queryset.filter(builder=builder) return specific_iterator(base_queryset.filter(builder=builder))
def get_public_builder_by_domain_name(self, domain_name: str) -> Builder: def get_public_builder_by_domain_name(self, domain_name: str) -> Builder:
""" """
@ -78,20 +85,31 @@ class DomainHandler:
return domain.published_to return domain.published_to
def create_domain(self, builder: Builder, domain_name: str) -> Domain: def create_domain(
self, domain_type: DomainType, builder: Builder, **kwargs
) -> Domain:
""" """
Creates a new domain Creates a new domain
:param domain_type: The type of domain that's being created
:param builder: The builder the domain belongs to :param builder: The builder the domain belongs to
:param domain_name: The name of the domain :param kwargs: Additional attributes of the domain
:return: The newly created domain instance :return: The newly created domain instance
""" """
last_order = Domain.get_last_order(builder) last_order = Domain.get_last_order(builder)
domain = Domain.objects.create(
builder=builder, domain_name=domain_name, order=last_order model_class = cast(Domain, domain_type.model_class)
allowed_values = extract_allowed(
kwargs, self.allowed_fields_create + domain_type.allowed_fields
) )
prepared_values = domain_type.prepare_values(allowed_values)
domain = model_class(builder=builder, order=last_order, **prepared_values)
domain.save()
return domain return domain
def delete_domain(self, domain: Domain): def delete_domain(self, domain: Domain):
@ -112,10 +130,23 @@ class DomainHandler:
:return: The updated domain :return: The updated domain
""" """
for key, value in kwargs.items(): domain_type = domain.get_type()
allowed_values = extract_allowed(
kwargs, self.allowed_fields_update + domain_type.allowed_fields
)
prepared_values = domain_type.prepare_values(allowed_values)
for key, value in prepared_values.items():
setattr(domain, key, value) setattr(domain, key, value)
domain.save() try:
domain.save()
except IntegrityError as error:
if "unique" in str(error) and "domain_name" in prepared_values:
raise DomainNameNotUniqueError(prepared_values["domain_name"])
raise error
return domain return domain

View file

@ -1,16 +1,21 @@
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.db.models import CASCADE, SET_NULL from django.db.models import CASCADE, SET_NULL
import validators import validators
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from baserow.contrib.builder.domains.registries import domain_type_registry
from baserow.core.jobs.mixins import JobWithUserIpAddress from baserow.core.jobs.mixins import JobWithUserIpAddress
from baserow.core.jobs.models import Job from baserow.core.jobs.models import Job
from baserow.core.mixins import ( from baserow.core.mixins import (
HierarchicalModelMixin, HierarchicalModelMixin,
OrderableMixin, OrderableMixin,
PolymorphicContentTypeMixin,
TrashableModelMixin, TrashableModelMixin,
WithRegistry,
) )
from baserow.core.registry import ModelRegistryMixin
def validate_domain(value: str): def validate_domain(value: str):
@ -29,7 +34,24 @@ def validate_domain(value: str):
raise ValidationError("Invalid domain syntax") raise ValidationError("Invalid domain syntax")
class Domain(HierarchicalModelMixin, TrashableModelMixin, OrderableMixin, models.Model): def get_default_domain_content_type():
return ContentType.objects.get_for_model(CustomDomain)
class Domain(
HierarchicalModelMixin,
TrashableModelMixin,
OrderableMixin,
WithRegistry,
PolymorphicContentTypeMixin,
models.Model,
):
content_type = models.ForeignKey(
ContentType,
verbose_name="content type",
related_name="builder_domains",
on_delete=models.SET(get_default_domain_content_type),
)
builder = models.ForeignKey( builder = models.ForeignKey(
"builder.Builder", "builder.Builder",
on_delete=CASCADE, on_delete=CASCADE,
@ -63,6 +85,18 @@ class Domain(HierarchicalModelMixin, TrashableModelMixin, OrderableMixin, models
queryset = Domain.objects.filter(builder=builder) queryset = Domain.objects.filter(builder=builder)
return cls.get_highest_order_of_queryset(queryset) + 1 return cls.get_highest_order_of_queryset(queryset) + 1
@staticmethod
def get_type_registry() -> ModelRegistryMixin:
return domain_type_registry
class CustomDomain(Domain):
pass
class SubDomain(Domain):
pass
class PublishDomainJob(JobWithUserIpAddress, Job): class PublishDomainJob(JobWithUserIpAddress, Job):
domain: Domain = models.ForeignKey(Domain, null=True, on_delete=models.SET_NULL) domain: Domain = models.ForeignKey(Domain, null=True, on_delete=models.SET_NULL)

View file

@ -0,0 +1,34 @@
from abc import ABC
from typing import Dict
from baserow.core.registry import (
CustomFieldsInstanceMixin,
CustomFieldsRegistryMixin,
Instance,
ModelInstanceMixin,
ModelRegistryMixin,
Registry,
)
class DomainType(Instance, ModelInstanceMixin, CustomFieldsInstanceMixin, ABC):
def prepare_values(self, values: Dict) -> Dict:
"""
Called before a domain is saved/updates to validate the data or transform the
data before it is being saved.
:param values: The values that are about to be saved
:return: The values after validation/transformation
"""
return values
class DomainTypeRegistry(Registry, ModelRegistryMixin, CustomFieldsRegistryMixin):
"""
Contains all the registered domain types.
"""
name = "domain_type"
domain_type_registry = DomainTypeRegistry()

View file

@ -29,6 +29,7 @@ from baserow.core.utils import Progress, extract_allowed
from .job_types import PublishDomainJobType from .job_types import PublishDomainJobType
from .operations import PublishDomainOperationType from .operations import PublishDomainOperationType
from .registries import DomainType
class DomainService: class DomainService:
@ -121,14 +122,19 @@ class DomainService:
return builder return builder
def create_domain( def create_domain(
self, user: AbstractUser, builder: Builder, domain_name: str self,
user: AbstractUser,
domain_type: DomainType,
builder: Builder,
**kwargs,
) -> Domain: ) -> Domain:
""" """
Creates a new domain Creates a new domain
:param user: The user trying to create the domain :param user: The user trying to create the domain
:param domain_type: The type of domain that's being created
:param builder: The builder the domain belongs to :param builder: The builder the domain belongs to
:param domain_name: The name of the domain :param kwargs: Additional attributes of the domain
:return: The newly created domain instance :return: The newly created domain instance
""" """
@ -139,7 +145,7 @@ class DomainService:
context=builder, context=builder,
) )
domain = self.handler.create_domain(builder, domain_name) domain = self.handler.create_domain(domain_type, builder, **kwargs)
domain_created.send(self, domain=domain, user=user) domain_created.send(self, domain=domain, user=user)

View file

@ -0,0 +1,73 @@
# Generated by Django 3.2.21 on 2023-09-14 13:34
import django.db.models.deletion
from django.db import migrations, models
import baserow.contrib.builder.domains.models
def get_default_domain_content_type_id():
return baserow.contrib.builder.domains.models.get_default_domain_content_type().id
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("builder", "0017_mainthemeconfigblock"),
]
operations = [
migrations.CreateModel(
name="CustomDomain",
fields=[
(
"domain_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="builder.domain",
),
),
],
options={
"abstract": False,
},
bases=("builder.domain",),
),
migrations.CreateModel(
name="SubDomain",
fields=[
(
"domain_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="builder.domain",
),
),
],
options={
"abstract": False,
},
bases=("builder.domain",),
),
migrations.AddField(
model_name="domain",
name="content_type",
field=models.ForeignKey(
default=get_default_domain_content_type_id,
on_delete=models.SET(
baserow.contrib.builder.domains.models.get_default_domain_content_type
),
related_name="builder_domains",
to="contenttypes.contenttype",
verbose_name="content type",
),
),
]

View file

@ -0,0 +1,34 @@
from django.db import migrations, transaction
from baserow.contrib.builder.domains.models import CustomDomain, Domain
def forward(apps, schema_editor):
"""
This migration introduces polymorphism, so we need to decide which type of
domain all the existing domains now become. In this case we are turning
all the existing domains into custom domains.
"""
with transaction.atomic():
domains = Domain.objects.all()
for domain in domains:
domain.delete()
CustomDomain.objects.create(
domain_name=domain.domain_name,
order=domain.order,
builder=domain.builder,
)
def reverse(apps, schema_editor):
...
class Migration(migrations.Migration):
dependencies = [("builder", "0018_sub_domains")]
operations = [
migrations.RunPython(forward, reverse),
]

View file

@ -4,7 +4,7 @@ from django.db import transaction
from faker import Faker from faker import Faker
from baserow.contrib.builder.data_sources.handler import DataSourceHandler from baserow.contrib.builder.data_sources.handler import DataSourceHandler
from baserow.contrib.builder.domains.models import Domain from baserow.contrib.builder.domains.models import CustomDomain
from baserow.contrib.builder.elements.handler import ElementHandler from baserow.contrib.builder.elements.handler import ElementHandler
from baserow.contrib.builder.elements.registries import element_type_registry from baserow.contrib.builder.elements.registries import element_type_registry
from baserow.contrib.builder.models import Builder from baserow.contrib.builder.models import Builder
@ -38,12 +38,18 @@ def load_test_data():
user, workspace, "builder", name="Back to local website" user, workspace, "builder", name="Back to local website"
) )
Domain.objects.filter(domain_name="test1.getbaserow.io").delete() CustomDomain.objects.filter(domain_name="test1.getbaserow.io").delete()
Domain.objects.filter(domain_name="test2.getbaserow.io").delete() CustomDomain.objects.filter(domain_name="test2.getbaserow.io").delete()
Domain.objects.filter(domain_name="test3.getbaserow.io").delete() CustomDomain.objects.filter(domain_name="test3.getbaserow.io").delete()
Domain.objects.create(builder=builder, domain_name="test1.getbaserow.io", order=1) CustomDomain.objects.create(
Domain.objects.create(builder=builder, domain_name="test2.getbaserow.io", order=2) builder=builder, domain_name="test1.getbaserow.io", order=1
Domain.objects.create(builder=builder, domain_name="test3.getbaserow.io", order=3) )
CustomDomain.objects.create(
builder=builder, domain_name="test2.getbaserow.io", order=2
)
CustomDomain.objects.create(
builder=builder, domain_name="test3.getbaserow.io", order=3
)
integration_type = integration_type_registry.get("local_baserow") integration_type = integration_type_registry.get("local_baserow")

View file

@ -1,8 +1,14 @@
from baserow.contrib.builder.domains.models import Domain from baserow.contrib.builder.domains.models import CustomDomain, SubDomain
class DomainFixtures: class DomainFixtures:
def create_builder_domain(self, user=None, **kwargs): def create_builder_custom_domain(self, user=None, **kwargs):
return self.create_builder_domain(CustomDomain, user, **kwargs)
def create_builder_sub_domain(self, user=None, **kwargs):
return self.create_builder_domain(SubDomain, user, **kwargs)
def create_builder_domain(self, model_class, user=None, **kwargs):
if user is None: if user is None:
user = self.create_user() user = self.create_user()
@ -15,6 +21,6 @@ class DomainFixtures:
if "order" not in kwargs: if "order" not in kwargs:
kwargs["order"] = 0 kwargs["order"] = 0
domain = Domain.objects.create(**kwargs) domain = model_class.objects.create(**kwargs)
return domain return domain

View file

@ -18,7 +18,7 @@ def test_get_public_builder_by_domain_name(api_client, data_fixture):
page = data_fixture.create_builder_page(user=user, builder=builder_to) page = data_fixture.create_builder_page(user=user, builder=builder_to)
page2 = data_fixture.create_builder_page(user=user, builder=builder_to) page2 = data_fixture.create_builder_page(user=user, builder=builder_to)
domain = data_fixture.create_builder_domain( domain = data_fixture.create_builder_custom_domain(
domain_name="test.getbaserow.io", published_to=builder_to domain_name="test.getbaserow.io", published_to=builder_to
) )
@ -56,7 +56,7 @@ def test_get_builder_missing_domain_name(api_client, data_fixture):
page = data_fixture.create_builder_page(user=user) page = data_fixture.create_builder_page(user=user)
page2 = data_fixture.create_builder_page(builder=page.builder, user=user) page2 = data_fixture.create_builder_page(builder=page.builder, user=user)
domain = data_fixture.create_builder_domain( domain = data_fixture.create_builder_custom_domain(
domain_name="test.getbaserow.io", published_to=page.builder domain_name="test.getbaserow.io", published_to=page.builder
) )
@ -78,7 +78,7 @@ def test_get_non_public_builder(api_client, data_fixture):
user, token = data_fixture.create_user_and_token() user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user) page = data_fixture.create_builder_page(user=user)
page2 = data_fixture.create_builder_page(builder=page.builder, user=user) page2 = data_fixture.create_builder_page(builder=page.builder, user=user)
domain = data_fixture.create_builder_domain( domain = data_fixture.create_builder_custom_domain(
domain_name="test.getbaserow.io", builder=page.builder domain_name="test.getbaserow.io", builder=page.builder
) )
@ -148,7 +148,7 @@ def test_publish_builder(mock_run_async_job, api_client, data_fixture):
page = data_fixture.create_builder_page(builder=builder_from, user=user) page = data_fixture.create_builder_page(builder=builder_from, user=user)
page2 = data_fixture.create_builder_page(builder=builder_from, user=user) page2 = data_fixture.create_builder_page(builder=builder_from, user=user)
domain = data_fixture.create_builder_domain( domain = data_fixture.create_builder_custom_domain(
domain_name="test.getbaserow.io", builder=builder_from domain_name="test.getbaserow.io", builder=builder_from
) )
@ -182,7 +182,7 @@ def test_get_elements_of_public_builder(api_client, data_fixture):
element2 = data_fixture.create_builder_heading_element(page=page) element2 = data_fixture.create_builder_heading_element(page=page)
element3 = data_fixture.create_builder_paragraph_element(page=page) element3 = data_fixture.create_builder_paragraph_element(page=page)
domain = data_fixture.create_builder_domain( domain = data_fixture.create_builder_custom_domain(
domain_name="test.getbaserow.io", domain_name="test.getbaserow.io",
published_to=page.builder, published_to=page.builder,
builder=builder_from, builder=builder_from,
@ -238,7 +238,7 @@ def test_get_data_source_of_public_builder(api_client, data_fixture):
page=page page=page
) )
domain = data_fixture.create_builder_domain( domain = data_fixture.create_builder_custom_domain(
domain_name="test.getbaserow.io", domain_name="test.getbaserow.io",
published_to=page.builder, published_to=page.builder,
builder=builder_from, builder=builder_from,

View file

@ -1,3 +1,4 @@
from django.test.utils import override_settings
from django.urls import reverse from django.urls import reverse
import pytest import pytest
@ -9,13 +10,15 @@ from rest_framework.status import (
HTTP_404_NOT_FOUND, HTTP_404_NOT_FOUND,
) )
from baserow.contrib.builder.domains.domain_types import CustomDomainType, SubDomainType
@pytest.mark.django_db @pytest.mark.django_db
def test_get_domains(api_client, data_fixture): def test_get_domains(api_client, data_fixture):
user, token = data_fixture.create_user_and_token() user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder) domain_one = data_fixture.create_builder_custom_domain(builder=builder)
domain_two = data_fixture.create_builder_domain(builder=builder) domain_two = data_fixture.create_builder_custom_domain(builder=builder)
url = reverse( url = reverse(
"api:builder:builder_id:domains:list", kwargs={"builder_id": builder.id} "api:builder:builder_id:domains:list", kwargs={"builder_id": builder.id}
@ -62,7 +65,7 @@ def test_create_domain(api_client, data_fixture):
) )
response = api_client.post( response = api_client.post(
url, url,
{"domain_name": domain_name}, {"type": CustomDomainType.type, "domain_name": domain_name},
format="json", format="json",
HTTP_AUTHORIZATION=f"JWT {token}", HTTP_AUTHORIZATION=f"JWT {token}",
) )
@ -82,7 +85,7 @@ def test_create_domain_user_not_in_workspace(api_client, data_fixture):
) )
response = api_client.post( response = api_client.post(
url, url,
{"domain_name": "test.com"}, {"type": CustomDomainType.type, "domain_name": "test.com"},
format="json", format="json",
HTTP_AUTHORIZATION=f"JWT {token}", HTTP_AUTHORIZATION=f"JWT {token}",
) )
@ -98,7 +101,7 @@ def test_create_domain_application_does_not_exist(api_client, data_fixture):
url = reverse("api:builder:builder_id:domains:list", kwargs={"builder_id": 9999}) url = reverse("api:builder:builder_id:domains:list", kwargs={"builder_id": 9999})
response = api_client.post( response = api_client.post(
url, url,
{"domain_name": "test.com"}, {"type": CustomDomainType.type, "domain_name": "test.com"},
format="json", format="json",
HTTP_AUTHORIZATION=f"JWT {token}", HTTP_AUTHORIZATION=f"JWT {token}",
) )
@ -114,14 +117,14 @@ def test_create_domain_domain_already_exists(api_client, data_fixture):
domain_name = "test.com" domain_name = "test.com"
data_fixture.create_builder_domain(builder=builder, domain_name=domain_name) data_fixture.create_builder_custom_domain(builder=builder, domain_name=domain_name)
url = reverse( url = reverse(
"api:builder:builder_id:domains:list", kwargs={"builder_id": builder.id} "api:builder:builder_id:domains:list", kwargs={"builder_id": builder.id}
) )
response = api_client.post( response = api_client.post(
url, url,
{"domain_name": domain_name}, {"type": CustomDomainType.type, "domain_name": domain_name},
format="json", format="json",
HTTP_AUTHORIZATION=f"JWT {token}", HTTP_AUTHORIZATION=f"JWT {token}",
) )
@ -142,7 +145,7 @@ def test_create_domain_invalid_domain_name(api_client, data_fixture):
) )
response = api_client.post( response = api_client.post(
url, url,
{"domain_name": domain_name}, {"type": CustomDomainType.type, "domain_name": domain_name},
format="json", format="json",
HTTP_AUTHORIZATION=f"JWT {token}", HTTP_AUTHORIZATION=f"JWT {token}",
) )
@ -151,11 +154,36 @@ def test_create_domain_invalid_domain_name(api_client, data_fixture):
assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION" assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION"
@pytest.mark.django_db
@override_settings(BASEROW_BUILDER_DOMAINS=["test.com"])
def test_create_invalid_sub_domain(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user)
domain_name = "hello.nottest.com"
url = reverse(
"api:builder:builder_id:domains:list", kwargs={"builder_id": builder.id}
)
response = api_client.post(
url,
{"type": SubDomainType.type, "domain_name": domain_name},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME"
assert "test.com" in response_json["detail"]
assert "nottest.com" in response_json["detail"]
@pytest.mark.django_db @pytest.mark.django_db
def test_update_domain(api_client, data_fixture): def test_update_domain(api_client, data_fixture):
user, token = data_fixture.create_user_and_token() user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain = data_fixture.create_builder_domain( domain = data_fixture.create_builder_custom_domain(
builder=builder, domain_name="something.com" builder=builder, domain_name="something.com"
) )
@ -187,12 +215,45 @@ def test_update_domain_domain_does_not_exist(api_client, data_fixture):
assert response.json()["error"] == "ERROR_DOMAIN_DOES_NOT_EXIST" assert response.json()["error"] == "ERROR_DOMAIN_DOES_NOT_EXIST"
@pytest.mark.django_db
def test_update_domain_with_same_name(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
domain = data_fixture.create_builder_custom_domain(user=user)
url = reverse("api:builder:domains:item", kwargs={"domain_id": domain.id})
response = api_client.patch(
url,
{"domain_name": domain.domain_name},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
@pytest.mark.django_db
def test_update_domain_name_uniqueness(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
domain = data_fixture.create_builder_custom_domain(user=user)
domain_2 = data_fixture.create_builder_custom_domain(user=user)
url = reverse("api:builder:domains:item", kwargs={"domain_id": domain.id})
response = api_client.patch(
url,
{"domain_name": domain_2.domain_name},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST, response.json()
@pytest.mark.django_db @pytest.mark.django_db
def test_order_domains(api_client, data_fixture): def test_order_domains(api_client, data_fixture):
user, token = data_fixture.create_user_and_token() user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder, order=1) domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2) domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
url = reverse( url = reverse(
"api:builder:builder_id:domains:order", kwargs={"builder_id": builder.id} "api:builder:builder_id:domains:order", kwargs={"builder_id": builder.id}
@ -211,8 +272,8 @@ def test_order_domains(api_client, data_fixture):
def test_order_domains_user_not_in_workspace(api_client, data_fixture): def test_order_domains_user_not_in_workspace(api_client, data_fixture):
user, token = data_fixture.create_user_and_token() user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain_one = data_fixture.create_builder_domain(builder=builder, order=1) domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2) domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
url = reverse( url = reverse(
"api:builder:builder_id:domains:order", kwargs={"builder_id": builder.id} "api:builder:builder_id:domains:order", kwargs={"builder_id": builder.id}
@ -232,8 +293,8 @@ def test_order_domains_user_not_in_workspace(api_client, data_fixture):
def test_order_domains_domain_not_in_builder(api_client, data_fixture): def test_order_domains_domain_not_in_builder(api_client, data_fixture):
user, token = data_fixture.create_user_and_token() user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder, order=1) domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(order=2) domain_two = data_fixture.create_builder_custom_domain(order=2)
url = reverse( url = reverse(
"api:builder:builder_id:domains:order", kwargs={"builder_id": builder.id} "api:builder:builder_id:domains:order", kwargs={"builder_id": builder.id}
@ -253,8 +314,8 @@ def test_order_domains_domain_not_in_builder(api_client, data_fixture):
def test_order_domains_application_does_not_exist(api_client, data_fixture): def test_order_domains_application_does_not_exist(api_client, data_fixture):
user, token = data_fixture.create_user_and_token() user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder, order=1) domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2) domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
url = reverse("api:builder:builder_id:domains:order", kwargs={"builder_id": 99999}) url = reverse("api:builder:builder_id:domains:order", kwargs={"builder_id": 99999})
response = api_client.post( response = api_client.post(
@ -272,7 +333,7 @@ def test_order_domains_application_does_not_exist(api_client, data_fixture):
def test_delete_domain(api_client, data_fixture): def test_delete_domain(api_client, data_fixture):
user, token = data_fixture.create_user_and_token() user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain = data_fixture.create_builder_domain(builder=builder, order=1) domain = data_fixture.create_builder_custom_domain(builder=builder, order=1)
url = reverse("api:builder:domains:item", kwargs={"domain_id": domain.id}) url = reverse("api:builder:domains:item", kwargs={"domain_id": domain.id})
response = api_client.delete( response = api_client.delete(
@ -288,7 +349,7 @@ def test_delete_domain(api_client, data_fixture):
def test_delete_domain_user_not_in_workspace(api_client, data_fixture): def test_delete_domain_user_not_in_workspace(api_client, data_fixture):
user, token = data_fixture.create_user_and_token() user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain = data_fixture.create_builder_domain(builder=builder, order=1) domain = data_fixture.create_builder_custom_domain(builder=builder, order=1)
url = reverse("api:builder:domains:item", kwargs={"domain_id": domain.id}) url = reverse("api:builder:domains:item", kwargs={"domain_id": domain.id})
response = api_client.delete( response = api_client.delete(

View file

@ -1,5 +1,6 @@
import pytest import pytest
from baserow.contrib.builder.domains.domain_types import CustomDomainType
from baserow.contrib.builder.domains.exceptions import ( from baserow.contrib.builder.domains.exceptions import (
DomainDoesNotExist, DomainDoesNotExist,
DomainNotInBuilder, DomainNotInBuilder,
@ -13,7 +14,7 @@ from baserow.core.utils import Progress
@pytest.mark.django_db @pytest.mark.django_db
def test_get_domain(data_fixture): def test_get_domain(data_fixture):
domain = data_fixture.create_builder_domain() domain = data_fixture.create_builder_custom_domain()
assert DomainHandler().get_domain(domain.id).id == domain.id assert DomainHandler().get_domain(domain.id).id == domain.id
@ -25,7 +26,7 @@ def test_get_domain_domain_does_not_exist(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
def test_get_domain_base_queryset(data_fixture, django_assert_num_queries): def test_get_domain_base_queryset(data_fixture, django_assert_num_queries):
domain = data_fixture.create_builder_domain() domain = data_fixture.create_builder_custom_domain()
# Without selecting related # Without selecting related
domain = DomainHandler().get_domain(domain.id) domain = DomainHandler().get_domain(domain.id)
@ -42,8 +43,8 @@ def test_get_domain_base_queryset(data_fixture, django_assert_num_queries):
@pytest.mark.django_db @pytest.mark.django_db
def test_get_domains(data_fixture): def test_get_domains(data_fixture):
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain_one = data_fixture.create_builder_domain(builder=builder) domain_one = data_fixture.create_builder_custom_domain(builder=builder)
domain_two = data_fixture.create_builder_domain(builder=builder) domain_two = data_fixture.create_builder_custom_domain(builder=builder)
domains = list(DomainHandler().get_domains(builder)) domains = list(DomainHandler().get_domains(builder))
domain_ids = [domain.id for domain in domains] domain_ids = [domain.id for domain in domains]
@ -58,7 +59,9 @@ def test_create_domain(data_fixture):
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
expected_order = Domain.get_last_order(builder) expected_order = Domain.get_last_order(builder)
domain = DomainHandler().create_domain(builder, "test.com") domain = DomainHandler().create_domain(
CustomDomainType(), builder, domain_name="test.com"
)
assert domain.order == expected_order assert domain.order == expected_order
assert domain.domain_name == "test.com" assert domain.domain_name == "test.com"
@ -66,7 +69,7 @@ def test_create_domain(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
def test_delete_domain(data_fixture): def test_delete_domain(data_fixture):
domain = data_fixture.create_builder_domain() domain = data_fixture.create_builder_custom_domain()
DomainHandler().delete_domain(domain) DomainHandler().delete_domain(domain)
@ -75,7 +78,7 @@ def test_delete_domain(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
def test_update_domain(data_fixture): def test_update_domain(data_fixture):
domain = data_fixture.create_builder_domain(domain_name="test.com") domain = data_fixture.create_builder_custom_domain(domain_name="test.com")
DomainHandler().update_domain(domain, domain_name="new.com") DomainHandler().update_domain(domain, domain_name="new.com")
@ -87,8 +90,8 @@ def test_update_domain(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
def test_order_domains(data_fixture): def test_order_domains(data_fixture):
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain_one = data_fixture.create_builder_domain(builder=builder, order=1) domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2) domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
assert DomainHandler().order_domains(builder, [domain_two.id, domain_one.id]) == [ assert DomainHandler().order_domains(builder, [domain_two.id, domain_one.id]) == [
domain_two.id, domain_two.id,
@ -105,8 +108,8 @@ def test_order_domains(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
def test_order_domains_domain_not_in_builder(data_fixture): def test_order_domains_domain_not_in_builder(data_fixture):
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain_one = data_fixture.create_builder_domain(builder=builder, order=1) domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2) domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
base_qs = Domain.objects.filter(id=domain_two.id) base_qs = Domain.objects.filter(id=domain_two.id)
@ -120,7 +123,7 @@ def test_order_domains_domain_not_in_builder(data_fixture):
def test_get_public_builder_by_name(data_fixture): def test_get_public_builder_by_name(data_fixture):
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
builder_to = data_fixture.create_builder_application(workspace=None) builder_to = data_fixture.create_builder_application(workspace=None)
domain1 = data_fixture.create_builder_domain( domain1 = data_fixture.create_builder_custom_domain(
builder=builder, published_to=builder_to builder=builder, published_to=builder_to
) )
@ -132,7 +135,7 @@ def test_get_public_builder_by_name(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
def test_get_published_builder_by_missing_domain_name(data_fixture): def test_get_published_builder_by_missing_domain_name(data_fixture):
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain1 = data_fixture.create_builder_domain(builder=builder) domain1 = data_fixture.create_builder_custom_domain(builder=builder)
with pytest.raises(BuilderDoesNotExist): with pytest.raises(BuilderDoesNotExist):
DomainHandler().get_public_builder_by_domain_name(domain1.domain_name) DomainHandler().get_public_builder_by_domain_name(domain1.domain_name)
@ -142,7 +145,7 @@ def test_get_published_builder_by_missing_domain_name(data_fixture):
def test_get_published_builder_for_trashed_builder(data_fixture): def test_get_published_builder_for_trashed_builder(data_fixture):
builder = data_fixture.create_builder_application(trashed=True) builder = data_fixture.create_builder_application(trashed=True)
builder_to = data_fixture.create_builder_application(workspace=None) builder_to = data_fixture.create_builder_application(workspace=None)
domain1 = data_fixture.create_builder_domain( domain1 = data_fixture.create_builder_custom_domain(
builder=builder, published_to=builder_to builder=builder, published_to=builder_to
) )
@ -151,7 +154,7 @@ def test_get_published_builder_for_trashed_builder(data_fixture):
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
builder_to = data_fixture.create_builder_application(workspace=None) builder_to = data_fixture.create_builder_application(workspace=None)
domain1 = data_fixture.create_builder_domain( domain1 = data_fixture.create_builder_custom_domain(
builder=builder, published_to=builder_to, trashed=True builder=builder, published_to=builder_to, trashed=True
) )
@ -163,7 +166,7 @@ def test_get_published_builder_for_trashed_builder(data_fixture):
def test_domain_publishing(data_fixture): def test_domain_publishing(data_fixture):
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain1 = data_fixture.create_builder_domain(builder=builder) domain1 = data_fixture.create_builder_custom_domain(builder=builder)
page1 = data_fixture.create_builder_page(builder=builder) page1 = data_fixture.create_builder_page(builder=builder)
page2 = data_fixture.create_builder_page(builder=builder) page2 = data_fixture.create_builder_page(builder=builder)

View file

@ -22,10 +22,10 @@ def test_allow_public_builder_manager_type(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
builder_to = data_fixture.create_builder_application(workspace=None) builder_to = data_fixture.create_builder_application(workspace=None)
domain1 = data_fixture.create_builder_domain( domain1 = data_fixture.create_builder_custom_domain(
builder=builder, published_to=builder_to builder=builder, published_to=builder_to
) )
domain2 = data_fixture.create_builder_domain(builder=builder) domain2 = data_fixture.create_builder_custom_domain(builder=builder)
public_page = data_fixture.create_builder_page(builder=builder_to) public_page = data_fixture.create_builder_page(builder=builder_to)
non_public_page = data_fixture.create_builder_page(builder=builder) non_public_page = data_fixture.create_builder_page(builder=builder)

View file

@ -2,6 +2,7 @@ from unittest.mock import patch
import pytest import pytest
from baserow.contrib.builder.domains.domain_types import CustomDomainType
from baserow.contrib.builder.domains.exceptions import DomainNotInBuilder from baserow.contrib.builder.domains.exceptions import DomainNotInBuilder
from baserow.contrib.builder.domains.models import Domain from baserow.contrib.builder.domains.models import Domain
from baserow.contrib.builder.domains.service import DomainService from baserow.contrib.builder.domains.service import DomainService
@ -15,7 +16,9 @@ def test_domain_created_signal_sent(domain_created_mock, data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain = DomainService().create_domain(user, builder, "test") domain = DomainService().create_domain(
user, CustomDomainType(), builder, domain_name="test"
)
assert domain_created_mock.called_with(domain=domain, user=user) assert domain_created_mock.called_with(domain=domain, user=user)
@ -26,7 +29,9 @@ def test_create_domain_user_not_in_workspace(data_fixture):
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
with pytest.raises(UserNotInWorkspace): with pytest.raises(UserNotInWorkspace):
DomainService().create_domain(user, builder, "test") DomainService().create_domain(
user, CustomDomainType, builder, domain_name="test"
)
@patch("baserow.contrib.builder.domains.service.domain_deleted") @patch("baserow.contrib.builder.domains.service.domain_deleted")
@ -34,7 +39,7 @@ def test_create_domain_user_not_in_workspace(data_fixture):
def test_domain_deleted_signal_sent(domain_deleted_mock, data_fixture): def test_domain_deleted_signal_sent(domain_deleted_mock, data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain = data_fixture.create_builder_domain(builder=builder) domain = data_fixture.create_builder_custom_domain(builder=builder)
DomainService().delete_domain(user, domain) DomainService().delete_domain(user, domain)
@ -49,7 +54,9 @@ def test_delete_domain_user_not_in_workspace(data_fixture):
user_unrelated = data_fixture.create_user() user_unrelated = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain = DomainService().create_domain(user, builder, "test") domain = DomainService().create_domain(
user, CustomDomainType(), builder, domain_name="test"
)
with pytest.raises(UserNotInWorkspace): with pytest.raises(UserNotInWorkspace):
DomainService().delete_domain(user_unrelated, domain) DomainService().delete_domain(user_unrelated, domain)
@ -61,7 +68,7 @@ def test_delete_domain_user_not_in_workspace(data_fixture):
def test_get_domain_user_not_in_workspace(data_fixture): def test_get_domain_user_not_in_workspace(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain = data_fixture.create_builder_domain(builder=builder) domain = data_fixture.create_builder_custom_domain(builder=builder)
with pytest.raises(UserNotInWorkspace): with pytest.raises(UserNotInWorkspace):
DomainService().get_domain(user, domain.id) DomainService().get_domain(user, domain.id)
@ -71,7 +78,7 @@ def test_get_domain_user_not_in_workspace(data_fixture):
def test_get_domains_user_not_in_workspace(data_fixture): def test_get_domains_user_not_in_workspace(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain = data_fixture.create_builder_domain(builder=builder) domain = data_fixture.create_builder_custom_domain(builder=builder)
with pytest.raises(UserNotInWorkspace): with pytest.raises(UserNotInWorkspace):
DomainService().get_domains(user, builder) DomainService().get_domains(user, builder)
@ -81,8 +88,8 @@ def test_get_domains_user_not_in_workspace(data_fixture):
def test_get_domains_partial_permissions(data_fixture, stub_check_permissions): def test_get_domains_partial_permissions(data_fixture, stub_check_permissions):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain_with_access = data_fixture.create_builder_domain(builder=builder) domain_with_access = data_fixture.create_builder_custom_domain(builder=builder)
domain_without_access = data_fixture.create_builder_domain(builder=builder) domain_without_access = data_fixture.create_builder_custom_domain(builder=builder)
def exclude_domain_without_access( def exclude_domain_without_access(
actor, actor,
@ -107,7 +114,7 @@ def test_get_domains_partial_permissions(data_fixture, stub_check_permissions):
def test_domain_updated_signal_sent(domain_updated_mock, data_fixture): def test_domain_updated_signal_sent(domain_updated_mock, data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain = data_fixture.create_builder_domain(builder=builder) domain = data_fixture.create_builder_custom_domain(builder=builder)
DomainService().update_domain(user, domain, domain_name="new.com") DomainService().update_domain(user, domain, domain_name="new.com")
@ -118,7 +125,7 @@ def test_domain_updated_signal_sent(domain_updated_mock, data_fixture):
def test_update_domain_user_not_in_workspace(data_fixture): def test_update_domain_user_not_in_workspace(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain = data_fixture.create_builder_domain(builder=builder) domain = data_fixture.create_builder_custom_domain(builder=builder)
with pytest.raises(UserNotInWorkspace): with pytest.raises(UserNotInWorkspace):
DomainService().update_domain(user, domain, domain_name="test.com") DomainService().update_domain(user, domain, domain_name="test.com")
@ -128,7 +135,7 @@ def test_update_domain_user_not_in_workspace(data_fixture):
def test_update_domain_invalid_values(data_fixture): def test_update_domain_invalid_values(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain = data_fixture.create_builder_domain(builder=builder) domain = data_fixture.create_builder_custom_domain(builder=builder)
domain_updated = DomainService().update_domain(user, domain, nonsense="hello") domain_updated = DomainService().update_domain(user, domain, nonsense="hello")
@ -140,8 +147,8 @@ def test_update_domain_invalid_values(data_fixture):
def test_domains_reordered_signal_sent(domains_reordered_mock, data_fixture): def test_domains_reordered_signal_sent(domains_reordered_mock, data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder, order=1) domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2) domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
full_order = DomainService().order_domains( full_order = DomainService().order_domains(
user, builder, [domain_two.id, domain_one.id] user, builder, [domain_two.id, domain_one.id]
@ -156,8 +163,8 @@ def test_domains_reordered_signal_sent(domains_reordered_mock, data_fixture):
def test_order_domains_user_not_in_workspace(data_fixture): def test_order_domains_user_not_in_workspace(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
domain_one = data_fixture.create_builder_domain(builder=builder, order=1) domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2) domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
with pytest.raises(UserNotInWorkspace): with pytest.raises(UserNotInWorkspace):
DomainService().order_domains(user, builder, [domain_two.id, domain_one.id]) DomainService().order_domains(user, builder, [domain_two.id, domain_one.id])
@ -167,8 +174,8 @@ def test_order_domains_user_not_in_workspace(data_fixture):
def test_order_domains_domain_not_in_builder(data_fixture): def test_order_domains_domain_not_in_builder(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder, order=1) domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(order=2) domain_two = data_fixture.create_builder_custom_domain(order=2)
with pytest.raises(DomainNotInBuilder): with pytest.raises(DomainNotInBuilder):
DomainService().order_domains(user, builder, [domain_two.id, domain_one.id]) DomainService().order_domains(user, builder, [domain_two.id, domain_one.id])
@ -179,7 +186,7 @@ def test_get_published_builder_by_domain_name(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
builder_to = data_fixture.create_builder_application(workspace=None) builder_to = data_fixture.create_builder_application(workspace=None)
domain1 = data_fixture.create_builder_domain( domain1 = data_fixture.create_builder_custom_domain(
builder=builder, published_to=builder_to builder=builder, published_to=builder_to
) )
@ -197,7 +204,7 @@ def test_get_published_builder_by_domain_name_unauthorized(
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
builder_to = data_fixture.create_builder_application(workspace=None) builder_to = data_fixture.create_builder_application(workspace=None)
domain1 = data_fixture.create_builder_domain( domain1 = data_fixture.create_builder_custom_domain(
builder=builder, published_to=builder_to builder=builder, published_to=builder_to
) )
@ -213,7 +220,7 @@ def test_async_publish_domain(mock_run_async_job, data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain1 = data_fixture.create_builder_domain(builder=builder) domain1 = data_fixture.create_builder_custom_domain(builder=builder)
job = DomainService().async_publish(user, domain1) job = DomainService().async_publish(user, domain1)
@ -225,7 +232,7 @@ def test_async_publish_domain(mock_run_async_job, data_fixture):
@pytest.mark.django_db(transaction=True) @pytest.mark.django_db(transaction=True)
def test_async_publish_domain_no_permission(data_fixture, stub_check_permissions): def test_async_publish_domain_no_permission(data_fixture, stub_check_permissions):
user = data_fixture.create_user() user = data_fixture.create_user()
domain1 = data_fixture.create_builder_domain() domain1 = data_fixture.create_builder_custom_domain()
with stub_check_permissions(raise_permission_denied=True), pytest.raises( with stub_check_permissions(raise_permission_denied=True), pytest.raises(
PermissionException PermissionException
@ -239,7 +246,7 @@ def test_publish_domain(domain_updated_mock, data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain1 = data_fixture.create_builder_domain(builder=builder) domain1 = data_fixture.create_builder_custom_domain(builder=builder)
progress = Progress(100) progress = Progress(100)
domain = DomainService().publish(user, domain1, progress) domain = DomainService().publish(user, domain1, progress)
@ -252,7 +259,7 @@ def test_publish_domain_unauthorized(data_fixture, stub_check_permissions):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain1 = data_fixture.create_builder_domain(builder=builder) domain1 = data_fixture.create_builder_custom_domain(builder=builder)
progress = Progress(100) progress = Progress(100)

View file

@ -9,7 +9,7 @@ from baserow.core.jobs.handler import JobHandler
def test_publish_domain_job_type(data_fixture): def test_publish_domain_job_type(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user) builder = data_fixture.create_builder_application(user=user)
domain1 = data_fixture.create_builder_domain(builder=builder) domain1 = data_fixture.create_builder_custom_domain(builder=builder)
publish_domain_job = JobHandler().create_and_start_job( publish_domain_job = JobHandler().create_and_start_job(
user, user,

View file

@ -410,7 +410,7 @@ def test_builder_application_import(data_fixture):
def test_delete_builder_application_with_published_builder(data_fixture): def test_delete_builder_application_with_published_builder(data_fixture):
builder = data_fixture.create_builder_application() builder = data_fixture.create_builder_application()
builder_to = data_fixture.create_builder_application(workspace=None) builder_to = data_fixture.create_builder_application(workspace=None)
domain1 = data_fixture.create_builder_domain( domain1 = data_fixture.create_builder_custom_domain(
builder=builder, published_to=builder_to builder=builder, published_to=builder_to
) )

View file

@ -139,6 +139,7 @@ x-backend-variables: &backend-variables
BASEROW_DISABLE_LOCKED_MIGRATIONS: BASEROW_DISABLE_LOCKED_MIGRATIONS:
BASEROW_USE_PG_FULLTEXT_SEARCH: BASEROW_USE_PG_FULLTEXT_SEARCH:
BASEROW_AUTO_VACUUM: BASEROW_AUTO_VACUUM:
BASEROW_BUILDER_DOMAINS:
services: services:
# A caddy reverse proxy sitting in-front of all the services. Responsible for routing # A caddy reverse proxy sitting in-front of all the services. Responsible for routing
@ -206,6 +207,7 @@ services:
BASEROW_USE_PG_FULLTEXT_SEARCH: BASEROW_USE_PG_FULLTEXT_SEARCH:
BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT: BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT:
BASEROW_ROW_PAGE_SIZE_LIMIT: BASEROW_ROW_PAGE_SIZE_LIMIT:
BASEROW_BUILDER_DOMAINS:
depends_on: depends_on:
- backend - backend
networks: networks:

View file

@ -158,6 +158,7 @@ x-backend-variables: &backend-variables
BASEROW_DISABLE_LOCKED_MIGRATIONS: BASEROW_DISABLE_LOCKED_MIGRATIONS:
BASEROW_USE_PG_FULLTEXT_SEARCH: BASEROW_USE_PG_FULLTEXT_SEARCH:
BASEROW_AUTO_VACUUM: BASEROW_AUTO_VACUUM:
BASEROW_BUILDER_DOMAINS:
services: services:
backend: backend:
@ -201,6 +202,7 @@ services:
BASEROW_USE_PG_FULLTEXT_SEARCH: BASEROW_USE_PG_FULLTEXT_SEARCH:
BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT: BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT:
BASEROW_ROW_PAGE_SIZE_LIMIT: BASEROW_ROW_PAGE_SIZE_LIMIT:
BASEROW_BUILDER_DOMAINS:
depends_on: depends_on:
- backend - backend
networks: networks:

View file

@ -157,6 +157,7 @@ x-backend-variables: &backend-variables
BASEROW_DISABLE_LOCKED_MIGRATIONS: BASEROW_DISABLE_LOCKED_MIGRATIONS:
BASEROW_USE_PG_FULLTEXT_SEARCH: BASEROW_USE_PG_FULLTEXT_SEARCH:
BASEROW_AUTO_VACUUM: BASEROW_AUTO_VACUUM:
BASEROW_BUILDER_DOMAINS:
services: services:
# A caddy reverse proxy sitting in-front of all the services. Responsible for routing # A caddy reverse proxy sitting in-front of all the services. Responsible for routing
@ -220,6 +221,7 @@ services:
POSTHOG_HOST: POSTHOG_HOST:
BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT: BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT:
BASEROW_ROW_PAGE_SIZE_LIMIT: BASEROW_ROW_PAGE_SIZE_LIMIT:
BASEROW_BUILDER_DOMAINS:
depends_on: depends_on:
- backend - backend
networks: networks:

View file

@ -139,6 +139,12 @@ The installation methods referred to in the variable descriptions are:
| BASEROW\_INITIAL\_CREATE\_SYNC\_TABLE\_DATA\_LIMIT | The maximum number of rows you can import in a synchronous way | 5000 | | BASEROW\_INITIAL\_CREATE\_SYNC\_TABLE\_DATA\_LIMIT | The maximum number of rows you can import in a synchronous way | 5000 |
| BASEROW\_MAX\_ROW\_REPORT\_ERROR\_COUNT | The maximum row error count tolerated before a file import fails. Before this max error count the import will continue and the non failing rows will be imported and after it, no rows are imported at all. | 30 | | BASEROW\_MAX\_ROW\_REPORT\_ERROR\_COUNT | The maximum row error count tolerated before a file import fails. Before this max error count the import will continue and the non failing rows will be imported and after it, no rows are imported at all. | 30 |
### Backend Application Builder Configuration
| Name | Description | Defaults |
|---------------------------|--------------------------------------------------------------------------------------------------------------------------|------------------------|
| BASEROW\_BUILDER\_DOMAINS | A comma separated list of domain names that can be used as the domains to create sub domains in the application builder. | |
### User file upload Configuration ### User file upload Configuration
Baserow needs somewhere to store the following types of files: Baserow needs somewhere to store the following types of files:
@ -242,6 +248,8 @@ domain than your Baserow, you need to make sure CORS is configured correctly.
| BASEROW_MAX_SNAPSHOTS_PER_GROUP | Controls how many application snapshots can be created per workspace. | -1 (unlimited) | | BASEROW_MAX_SNAPSHOTS_PER_GROUP | Controls how many application snapshots can be created per workspace. | -1 (unlimited) |
| BASEROW\_USE\_PG\_FULLTEXT\_SEARCH | By default, Baserow will use Postgres full-text as its search backend. If the product is installed on a system with limited disk space, and less accurate results / degraded search performance is acceptable, then switch this setting off by setting it to false. | true | | BASEROW\_USE\_PG\_FULLTEXT\_SEARCH | By default, Baserow will use Postgres full-text as its search backend. If the product is installed on a system with limited disk space, and less accurate results / degraded search performance is acceptable, then switch this setting off by setting it to false. | true |
| BASEROW\_UNIQUE\_ROW\_VALUES\_SIZE\_LIMIT | Sets the limit for the automatic detection of multiselect options when converting a text field to a multiselect field. Increase the value to detect more options automatically, but consider performance implications. | 100 | | BASEROW\_UNIQUE\_ROW\_VALUES\_SIZE\_LIMIT | Sets the limit for the automatic detection of multiselect options when converting a text field to a multiselect field. Increase the value to detect more options automatically, but consider performance implications. | 100 |
| BASEROW\_BUILDER\_DOMAINS | A comma separated list of domain names that can be used as the domains to create sub domains in the application builder. | |
### SSO Configuration ### SSO Configuration
| Name | Description | Defaults | | Name | Description | Defaults |

View file

@ -29,26 +29,25 @@
</div> </div>
<template v-if="serverErrors.domain_name"> <template v-if="serverErrors.domain_name">
<div v-if="serverErrors.domain_name.code === 'invalid'" class="error"> <div v-if="serverErrors.domain_name.code === 'invalid'" class="error">
{{ $t('customDomainForm.invalidDomain') }} {{ $t('domainForm.invalidDomain') }}
</div> </div>
<div v-if="serverErrors.domain_name.code === 'unique'" class="error"> <div v-if="serverErrors.domain_name.code === 'unique'" class="error">
{{ $t('customDomainForm.notUniqueDomain') }} {{ $t('domainForm.notUniqueDomain') }}
</div></template </div>
> </template>
</FormElement> </FormElement>
</form> </form>
</template> </template>
<script> <script>
import form from '@baserow/modules/core/mixins/form'
import { required, maxLength } from 'vuelidate/lib/validators' import { required, maxLength } from 'vuelidate/lib/validators'
import domainForm from '@baserow/modules/builder/mixins/domainForm'
export default { export default {
name: 'DomainForm', name: 'CustomDomainForm',
mixins: [form], mixins: [domainForm],
data() { data() {
return { return {
serverErrors: { domain_name: null },
values: { values: {
domain_name: '', domain_name: '',
}, },
@ -64,22 +63,5 @@ export default {
}, },
} }
}, },
methods: {
handleServerError(error) {
if (error.handler.code !== 'ERROR_REQUEST_BODY_VALIDATION') return false
this.serverErrors = Object.fromEntries(
Object.entries(error.handler.detail || {}).map(([key, value]) => [
key,
value[0],
])
)
return true
},
hasError() {
return !this.isFormValid() || this.serverErrors.domain_name !== null
},
},
} }
</script> </script>

View file

@ -4,15 +4,15 @@
<Error :error="error"></Error> <Error :error="error"></Error>
<FormElement class="control"> <FormElement class="control">
<Dropdown <Dropdown
v-model="selectedDomainType" v-model="selectedDomain"
:show-search="false" :show-search="false"
class="domain-settings__domain-type" class="domain-settings__domain-type"
> >
<DropdownItem <DropdownItem
v-for="domainType in domainTypes" v-for="option in options"
:key="domainType.getType()" :key="option.value.domain"
:name="domainType.name" :name="option.name"
:value="domainType.getType()" :value="option.value"
/> />
</Dropdown> </Dropdown>
</FormElement> </FormElement>
@ -20,7 +20,9 @@
:is="currentDomainType.formComponent" :is="currentDomainType.formComponent"
ref="domainForm" ref="domainForm"
:builder="builder" :builder="builder"
:domain="selectedDomain.domain"
@submitted="createDomain($event)" @submitted="createDomain($event)"
@error="formHasError = $event"
/> />
<div class="actions"> <div class="actions">
<a @click="hideForm"> <a @click="hideForm">
@ -29,7 +31,7 @@
</a> </a>
<button <button
class="button button--large" class="button button--large"
:disabled="createLoading || formHasError()" :disabled="createLoading || formHasError"
:class="{ 'button--loading': createLoading }" :class="{ 'button--loading': createLoading }"
@click="onSubmit" @click="onSubmit"
> >
@ -58,17 +60,29 @@ export default {
}, },
data() { data() {
return { return {
selectedDomainType: 'custom', selectedDomain: { type: 'custom', domain: 'custom' },
createLoading: false, createLoading: false,
formHasError: false,
} }
}, },
computed: { computed: {
domainTypes() { domainTypes() {
return this.$registry.getAll('domain') return Object.values(this.$registry.getAll('domain')) || []
},
selectedDomainType() {
return this.selectedDomain.type
}, },
currentDomainType() { currentDomainType() {
return this.$registry.get('domain', this.selectedDomainType) return this.$registry.get('domain', this.selectedDomainType)
}, },
options() {
return this.domainTypes.map((domainType) => domainType.options).flat()
},
},
watch: {
selectedDomainType() {
this.$refs?.domainForm?.reset()
},
}, },
methods: { methods: {
...mapActions({ ...mapActions({
@ -92,13 +106,6 @@ export default {
} }
this.createLoading = false this.createLoading = false
}, },
formHasError() {
if (this.$refs.domainForm) {
return this.$refs.domainForm.hasError()
} else {
return false
}
},
handleAnyError(error) { handleAnyError(error) {
if ( if (
!this.$refs.domainForm || !this.$refs.domainForm ||

View file

@ -0,0 +1,28 @@
<template>
<div>
<p>
{{ $t('subDomainDetails.text') }}
</p>
<div class="actions actions--right">
<a
class="button button--error"
:class="{ 'button--loading': domain._.loading }"
@click="$emit('delete')"
>
{{ $t('action.delete') }}
</a>
</div>
</div>
</template>
<script>
export default {
name: 'SubDomainDetails',
props: {
domain: {
type: Object,
required: true,
},
},
}
</script>

View file

@ -0,0 +1,59 @@
<template>
<form @submit.prevent="submit">
<FormElement :error="fieldHasErrors('domain_name')" class="control">
<FormInput
v-model="domainPrefix"
:label="$t('subDomainForm.domainNameLabel')"
:error="
$v.values.domain_name.$dirty && !$v.values.domain_name.required
? $t('error.requiredField')
: $v.values.domain_name.$dirty && !$v.values.domain_name.maxLength
? $t('error.maxLength', { max: 255 })
: serverErrors.domain_name &&
serverErrors.domain_name.code === 'invalid'
? $t('domainForm.invalidDomain')
: serverErrors.domain_name &&
serverErrors.domain_name.code === 'unique'
? $t('domainForm.notUniqueDomain')
: ''
"
@input="serverErrors.domain_name = null"
>
<template #suffix> .{{ domain }} </template>
</FormInput>
</FormElement>
</form>
</template>
<script>
import { maxLength, required } from 'vuelidate/lib/validators'
import domainForm from '@baserow/modules/builder/mixins/domainForm'
export default {
name: 'SubDomainForm',
mixins: [domainForm],
data() {
return {
domainPrefix: '',
values: {
domain_name: '',
},
}
},
watch: {
domainPrefix(value) {
this.values.domain_name = `${value}.${this.domain}`
},
},
validations() {
return {
values: {
domain_name: {
required,
maxLength: maxLength(255),
},
},
}
},
}
</script>

View file

@ -1,6 +1,8 @@
import { Registerable } from '@baserow/modules/core/registry' import { Registerable } from '@baserow/modules/core/registry'
import CustomDomainDetails from '@baserow/modules/builder/components/domain/CustomDomainDetails' import CustomDomainDetails from '@baserow/modules/builder/components/domain/CustomDomainDetails'
import CustomDomainForm from '@baserow/modules/builder/components/domain/CustomDomainForm' import CustomDomainForm from '@baserow/modules/builder/components/domain/CustomDomainForm'
import SubDomainForm from '@baserow/modules/builder/components/domain/SubDomainForm'
import SubDomainDetails from '@baserow/modules/builder/components/domain/SubDomainDetails'
export class DomainType extends Registerable { export class DomainType extends Registerable {
get name() { get name() {
@ -10,6 +12,14 @@ export class DomainType extends Registerable {
get detailsComponent() { get detailsComponent() {
return null return null
} }
get formComponent() {
return null
}
get options() {
return []
}
} }
export class CustomDomainType extends DomainType { export class CustomDomainType extends DomainType {
@ -28,4 +38,44 @@ export class CustomDomainType extends DomainType {
get formComponent() { get formComponent() {
return CustomDomainForm return CustomDomainForm
} }
get options() {
return [
{
name: this.app.i18n.t('domainTypes.customName'),
value: {
type: this.getType(),
domain: 'custom',
},
},
]
}
}
export class SubDomainType extends DomainType {
static getType() {
return 'sub_domain'
}
get name() {
return this.app.i18n.t('domainTypes.subDomainName')
}
get options() {
return this.app.$config.BASEROW_BUILDER_DOMAINS.map((domain) => ({
name: this.app.i18n.t('domainTypes.subDomain', { domain }),
value: {
type: this.getType(),
domain,
},
}))
}
get detailsComponent() {
return SubDomainDetails
}
get formComponent() {
return SubDomainForm
}
} }

View file

@ -155,17 +155,27 @@
"hostHeader": "Host", "hostHeader": "Host",
"valueHeader": "Value" "valueHeader": "Value"
}, },
"domainForm": {
"invalidDomain": "The provided domain name is invalid",
"notUniqueDomain": "The provided domain is already used"
},
"customDomainForm": { "customDomainForm": {
"domainNameLabel": "Domain name", "domainNameLabel": "Domain name"
"invalidDomain": "The provided domain name is invalid", },
"notUniqueDomain": "The provided domain is already used" "subDomainForm": {
"domainNameLabel": "Domain name"
},
"subDomainDetails": {
"text": "The DNS settings of the domain have already been configured and checked. It works without making any additional changes."
}, },
"domainCard": { "domainCard": {
"refresh": "Refresh settings", "refresh": "Refresh settings",
"detailLabel": "Show details" "detailLabel": "Show details"
}, },
"domainTypes": { "domainTypes": {
"customName": "Custom domain" "customName": "Custom domain",
"subDomainName": "Subdomain",
"subDomain": "Subdomain of {domain}"
}, },
"linkElement": { "linkElement": {
"noValue": "Unnamed..." "noValue": "Unnamed..."

View file

@ -0,0 +1,46 @@
import form from '@baserow/modules/core/mixins/form'
export default {
mixins: [form],
props: {
domain: {
type: String,
required: true,
},
},
data() {
return {
serverErrors: {},
}
},
computed: {
hasServerErrors() {
return Object.values(this.serverErrors).some((value) => value !== null)
},
hasError() {
return !this.isFormValid() || this.hasServerErrors
},
},
watch: {
hasError: {
handler(value) {
this.$emit('error', value)
},
immediate: true,
},
},
methods: {
handleServerError(error) {
if (error.handler.code !== 'ERROR_REQUEST_BODY_VALIDATION') return false
this.serverErrors = Object.fromEntries(
Object.entries(error.handler.detail || {}).map(([key, value]) => [
key,
value[0],
])
)
return true
},
},
}

View file

@ -49,7 +49,10 @@ import {
VisibilityPageSidePanelType, VisibilityPageSidePanelType,
StylePageSidePanelType, StylePageSidePanelType,
} from '@baserow/modules/builder/pageSidePanelTypes' } from '@baserow/modules/builder/pageSidePanelTypes'
import { CustomDomainType } from '@baserow/modules/builder/domainTypes' import {
CustomDomainType,
SubDomainType,
} from '@baserow/modules/builder/domainTypes'
import { PagePageSettingsType } from '@baserow/modules/builder/pageSettingsTypes' import { PagePageSettingsType } from '@baserow/modules/builder/pageSettingsTypes'
import { import {
TextPathParamType, TextPathParamType,
@ -158,6 +161,7 @@ export default (context) => {
app.$registry.register('pageSidePanel', new EventsPageSidePanelType(context)) app.$registry.register('pageSidePanel', new EventsPageSidePanelType(context))
app.$registry.register('domain', new CustomDomainType(context)) app.$registry.register('domain', new CustomDomainType(context))
app.$registry.register('domain', new SubDomainType(context))
app.$registry.register('pageSettings', new PagePageSettingsType(context)) app.$registry.register('pageSettings', new PagePageSettingsType(context))

View file

@ -1,16 +1,10 @@
export default (client) => { export default (client) => {
return { return {
async fetchAll(builderId) { fetchAll(builderId) {
// TODO Manually add the domain type while the backend doesn't support it. return client.get(`builder/${builderId}/domains/`)
const result = await client.get(`builder/${builderId}/domains/`)
result.data = result.data.map((domain) => ({ type: 'custom', ...domain }))
return result
}, },
async create(builderId, { type, ...data }) { create(builderId, data) {
// TODO For now we manage the type manually. return client.post(`builder/${builderId}/domains/`, data)
const result = await client.post(`builder/${builderId}/domains/`, data)
result.data.type = 'custom'
return result
}, },
delete(domainId) { delete(domainId) {
return client.delete(`builder/domains/${domainId}/`) return client.delete(`builder/domains/${domainId}/`)

View file

@ -54,10 +54,10 @@ const actions = {
commit('SET_ITEMS', { domains }) commit('SET_ITEMS', { domains })
}, },
async create({ commit }, { builderId, ...data }) { async create({ commit }, { builderId, type, ...data }) {
const { data: domain } = await DomainService(this.$client).create( const { data: domain } = await DomainService(this.$client).create(
builderId, builderId,
data { type, ...data }
) )
commit('ADD_ITEM', { domain }) commit('ADD_ITEM', { domain })

View file

@ -1,6 +1,10 @@
.domain-settings__domain-type { .domain-settings__domain-type {
display: inline-block; display: inline-block;
width: 30%; width: fit-content;
.dropdown__items {
width: min-content;
}
} }
.domains-settings__loading { .domains-settings__loading {

View file

@ -1,5 +1,5 @@
.form-input { .form-input {
display: block; display: flex;
border: 1px solid $color-neutral-400; border: 1px solid $color-neutral-400;
position: relative; position: relative;
@include rounded($rounded); @include rounded($rounded);
@ -96,3 +96,14 @@
display: none; display: none;
} }
} }
.form-input__suffix {
display: flex;
align-items: center;
width: fit-content;
border-left: 1px solid $color-neutral-400;
appearance: none;
padding: 0 12px;
outline: none;
line-height: 100%;
}

View file

@ -43,6 +43,11 @@
class="form-input__icon fas" class="form-input__icon fas"
:class="[`fa-${icon}`]" :class="[`fa-${icon}`]"
/> />
<div class="form-input__suffix disabled">
<div>
<slot name="suffix"></slot>
</div>
</div>
</div> </div>
<div v-if="hasError" class="error"> <div v-if="hasError" class="error">
{{ error }} {{ error }}

View file

@ -5,6 +5,7 @@ import {
} from '@baserow/modules/core/utils/dom' } from '@baserow/modules/core/utils/dom'
import dropdownHelpers from './dropdownHelpers' import dropdownHelpers from './dropdownHelpers'
import _ from 'lodash'
export default { export default {
mixins: [dropdownHelpers], mixins: [dropdownHelpers],
@ -325,7 +326,7 @@ export default {
getSelectedProperty(value, property) { getSelectedProperty(value, property) {
for (const i in this.getDropdownItemComponents()) { for (const i in this.getDropdownItemComponents()) {
const item = this.getDropdownItemComponents()[i] const item = this.getDropdownItemComponents()[i]
if (item.value === value) { if (_.isEqual(item.value, value)) {
return item[property] return item[property]
} }
} }
@ -338,7 +339,7 @@ export default {
hasValue() { hasValue() {
for (const i in this.getDropdownItemComponents()) { for (const i in this.getDropdownItemComponents()) {
const item = this.getDropdownItemComponents()[i] const item = this.getDropdownItemComponents()[i]
if (item.value === this.value) { if (_.isEqual(item.value, this.value)) {
return true return true
} }
} }
@ -366,7 +367,9 @@ export default {
) )
const isArrowUp = event.key === 'ArrowUp' const isArrowUp = event.key === 'ArrowUp'
let index = children.findIndex((item) => item.value === this.hover) let index = children.findIndex((item) =>
_.isEqual(item.value, this.hover)
)
index = isArrowUp ? index - 1 : index + 1 index = isArrowUp ? index - 1 : index + 1
// Check if the new index is within the allowed range. // Check if the new index is within the allowed range.

View file

@ -91,6 +91,9 @@ export default function CoreModule(options) {
process.env.BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT ?? 100, process.env.BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT ?? 100,
BASEROW_ROW_PAGE_SIZE_LIMIT: BASEROW_ROW_PAGE_SIZE_LIMIT:
parseInt(process.env.BASEROW_ROW_PAGE_SIZE_LIMIT) ?? 200, parseInt(process.env.BASEROW_ROW_PAGE_SIZE_LIMIT) ?? 200,
BASEROW_BUILDER_DOMAINS: process.env.BASEROW_BUILDER_DOMAINS
? process.env.BASEROW_BUILDER_DOMAINS.split(',')
: [],
} }
const locales = [ const locales = [