1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 00:59:06 +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:
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
# file used by pytest.ini
TESTS = False

View file

@ -11,3 +11,16 @@ ERROR_DOMAIN_NOT_IN_BUILDER = (
HTTP_400_BAD_REQUEST,
"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 django.utils.functional import lazy
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
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.data_sources.models import DataSource
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.registries import element_type_registry
from baserow.contrib.builder.models import Builder
@ -16,9 +19,15 @@ from baserow.core.services.registries import service_type_registry
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:
model = Domain
fields = ("id", "domain_name", "order", "builder_id", "last_published")
fields = ("id", "type", "domain_name", "order", "builder_id", "last_published")
extra_kwargs = {
"id": {"read_only": True},
"builder_id": {"read_only": True},
@ -27,9 +36,37 @@ class DomainSerializer(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:
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):

View file

@ -10,29 +10,43 @@ from rest_framework.status import HTTP_202_ACCEPTED
from rest_framework.views import APIView
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.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.domains.errors import (
ERROR_DOMAIN_DOES_NOT_EXIST,
ERROR_DOMAIN_NAME_NOT_UNIQUE,
ERROR_DOMAIN_NOT_IN_BUILDER,
ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME,
)
from baserow.contrib.builder.api.domains.serializers import (
CreateDomainSerializer,
DomainSerializer,
OrderDomainsSerializer,
PublicBuilderSerializer,
UpdateDomainSerializer,
)
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.domains.exceptions import (
DomainDoesNotExist,
DomainNameNotUniqueError,
DomainNotInBuilder,
SubDomainHasInvalidDomainName,
)
from baserow.contrib.builder.domains.handler import DomainHandler
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.elements.registries import element_type_registry
from baserow.contrib.builder.elements.service import ElementService
@ -66,9 +80,13 @@ class DomainsView(APIView):
tags=["Builder domains"],
operation_id="create_builder_domain",
description="Creates a new domain for an application builder",
request=CreateDomainSerializer,
request=DiscriminatorCustomFieldsMappingSerializer(
domain_type_registry, CreateDomainSerializer, request=True
),
responses={
200: DomainSerializer,
200: DiscriminatorCustomFieldsMappingSerializer(
domain_type_registry, DomainSerializer
),
400: get_error_schema(
[
"ERROR_USER_NOT_IN_GROUP",
@ -82,17 +100,22 @@ class DomainsView(APIView):
@map_exceptions(
{
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):
builder = BuilderHandler().get_builder(builder_id)
type_name = data.pop("type")
domain_type = domain_type_registry.get(type_name)
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)
@extend_schema(
@ -109,7 +132,9 @@ class DomainsView(APIView):
operation_id="get_builder_domains",
description="Gets all the domains of a builder",
responses={
200: DomainSerializer(many=True),
200: DiscriminatorCustomFieldsMappingSerializer(
domain_type_registry, DomainSerializer, many=True
),
400: get_error_schema(
[
"ERROR_USER_NOT_IN_GROUP",
@ -132,9 +157,12 @@ class DomainsView(APIView):
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):
@ -151,9 +179,11 @@ class DomainView(APIView):
tags=["Builder domains"],
operation_id="update_builder_domain",
description="Updates an existing domain of an application builder",
request=CreateDomainSerializer,
request=UpdateDomainSerializer,
responses={
200: DomainSerializer,
200: DiscriminatorCustomFieldsMappingSerializer(
domain_type_registry, DomainSerializer
),
400: get_error_schema(
[
"ERROR_USER_NOT_IN_GROUP",
@ -167,19 +197,35 @@ class DomainView(APIView):
@map_exceptions(
{
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, data: Dict, domain_id: int):
base_queryset = Domain.objects.select_for_update(of=("self",))
def patch(self, request, domain_id: int):
base_queryset = Domain.objects
domain = DomainService().get_domain(
request.user, domain_id, base_queryset=base_queryset
domain = (
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)
serializer = DomainSerializer(domain_updated)
serializer = domain_type_registry.get_serializer(
domain_updated, DomainSerializer
)
return Response(serializer.data)
@extend_schema(

View file

@ -151,6 +151,12 @@ class BuilderConfig(AppConfig):
element_type_registry.register(InputTextElementType())
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
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):
"""Raised when trying to get a domain that doesn't exist."""
@ -12,3 +15,27 @@ class DomainNotInBuilder(Exception):
*args,
**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.db.models import QuerySet
from django.db.utils import IntegrityError
from django.utils import timezone
from baserow.contrib.builder.domains.exceptions import (
DomainDoesNotExist,
DomainNameNotUniqueError,
DomainNotInBuilder,
)
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.models import Builder
from baserow.core.db import specific_iterator
from baserow.core.exceptions import IdDoesNotExist
from baserow.core.registries import ImportExportConfig, application_type_registry
from baserow.core.trash.handler import TrashHandler
from baserow.core.utils import Progress
from baserow.core.utils import Progress, extract_allowed
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:
"""
Gets a domain by ID
@ -39,20 +46,20 @@ class DomainHandler:
def get_domains(
self, builder: Builder, base_queryset: QuerySet = None
) -> QuerySet[Domain]:
) -> Iterable[Domain]:
"""
Gets all the domains of a builder.
:param builder: The builder we are trying to get all domains for
:param base_queryset: Can be provided to already filter or apply performance
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:
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:
"""
@ -78,20 +85,31 @@ class DomainHandler:
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
:param domain_type: The type of domain that's being created
: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
"""
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
def delete_domain(self, domain: Domain):
@ -112,10 +130,23 @@ class DomainHandler:
: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)
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

View file

@ -1,16 +1,21 @@
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import CASCADE, SET_NULL
import validators
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.models import Job
from baserow.core.mixins import (
HierarchicalModelMixin,
OrderableMixin,
PolymorphicContentTypeMixin,
TrashableModelMixin,
WithRegistry,
)
from baserow.core.registry import ModelRegistryMixin
def validate_domain(value: str):
@ -29,7 +34,24 @@ def validate_domain(value: str):
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.Builder",
on_delete=CASCADE,
@ -63,6 +85,18 @@ class Domain(HierarchicalModelMixin, TrashableModelMixin, OrderableMixin, models
queryset = Domain.objects.filter(builder=builder)
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):
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 .operations import PublishDomainOperationType
from .registries import DomainType
class DomainService:
@ -121,14 +122,19 @@ class DomainService:
return builder
def create_domain(
self, user: AbstractUser, builder: Builder, domain_name: str
self,
user: AbstractUser,
domain_type: DomainType,
builder: Builder,
**kwargs,
) -> Domain:
"""
Creates a new 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 domain_name: The name of the domain
:param kwargs: Additional attributes of the domain
:return: The newly created domain instance
"""
@ -139,7 +145,7 @@ class DomainService:
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)

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 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.registries import element_type_registry
from baserow.contrib.builder.models import Builder
@ -38,12 +38,18 @@ def load_test_data():
user, workspace, "builder", name="Back to local website"
)
Domain.objects.filter(domain_name="test1.getbaserow.io").delete()
Domain.objects.filter(domain_name="test2.getbaserow.io").delete()
Domain.objects.filter(domain_name="test3.getbaserow.io").delete()
Domain.objects.create(builder=builder, domain_name="test1.getbaserow.io", order=1)
Domain.objects.create(builder=builder, domain_name="test2.getbaserow.io", order=2)
Domain.objects.create(builder=builder, domain_name="test3.getbaserow.io", order=3)
CustomDomain.objects.filter(domain_name="test1.getbaserow.io").delete()
CustomDomain.objects.filter(domain_name="test2.getbaserow.io").delete()
CustomDomain.objects.filter(domain_name="test3.getbaserow.io").delete()
CustomDomain.objects.create(
builder=builder, domain_name="test1.getbaserow.io", order=1
)
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")

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:
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:
user = self.create_user()
@ -15,6 +21,6 @@ class DomainFixtures:
if "order" not in kwargs:
kwargs["order"] = 0
domain = Domain.objects.create(**kwargs)
domain = model_class.objects.create(**kwargs)
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)
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
)
@ -56,7 +56,7 @@ def test_get_builder_missing_domain_name(api_client, data_fixture):
page = data_fixture.create_builder_page(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
)
@ -78,7 +78,7 @@ def test_get_non_public_builder(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(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
)
@ -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)
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
)
@ -182,7 +182,7 @@ def test_get_elements_of_public_builder(api_client, data_fixture):
element2 = data_fixture.create_builder_heading_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",
published_to=page.builder,
builder=builder_from,
@ -238,7 +238,7 @@ def test_get_data_source_of_public_builder(api_client, data_fixture):
page=page
)
domain = data_fixture.create_builder_domain(
domain = data_fixture.create_builder_custom_domain(
domain_name="test.getbaserow.io",
published_to=page.builder,
builder=builder_from,

View file

@ -1,3 +1,4 @@
from django.test.utils import override_settings
from django.urls import reverse
import pytest
@ -9,13 +10,15 @@ from rest_framework.status import (
HTTP_404_NOT_FOUND,
)
from baserow.contrib.builder.domains.domain_types import CustomDomainType, SubDomainType
@pytest.mark.django_db
def test_get_domains(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder)
domain_two = data_fixture.create_builder_domain(builder=builder)
domain_one = data_fixture.create_builder_custom_domain(builder=builder)
domain_two = data_fixture.create_builder_custom_domain(builder=builder)
url = reverse(
"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(
url,
{"domain_name": domain_name},
{"type": CustomDomainType.type, "domain_name": domain_name},
format="json",
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(
url,
{"domain_name": "test.com"},
{"type": CustomDomainType.type, "domain_name": "test.com"},
format="json",
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})
response = api_client.post(
url,
{"domain_name": "test.com"},
{"type": CustomDomainType.type, "domain_name": "test.com"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
@ -114,14 +117,14 @@ def test_create_domain_domain_already_exists(api_client, data_fixture):
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(
"api:builder:builder_id:domains:list", kwargs={"builder_id": builder.id}
)
response = api_client.post(
url,
{"domain_name": domain_name},
{"type": CustomDomainType.type, "domain_name": domain_name},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
@ -142,7 +145,7 @@ def test_create_domain_invalid_domain_name(api_client, data_fixture):
)
response = api_client.post(
url,
{"domain_name": domain_name},
{"type": CustomDomainType.type, "domain_name": domain_name},
format="json",
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"
@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
def test_update_domain(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
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"
)
@ -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"
@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
def test_order_domains(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
url = reverse(
"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):
user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application()
domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
url = reverse(
"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):
user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(order=2)
domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_custom_domain(order=2)
url = reverse(
"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):
user, token = data_fixture.create_user_and_token()
builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
url = reverse("api:builder:builder_id:domains:order", kwargs={"builder_id": 99999})
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):
user, token = data_fixture.create_user_and_token()
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})
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):
user, token = data_fixture.create_user_and_token()
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})
response = api_client.delete(

View file

@ -1,5 +1,6 @@
import pytest
from baserow.contrib.builder.domains.domain_types import CustomDomainType
from baserow.contrib.builder.domains.exceptions import (
DomainDoesNotExist,
DomainNotInBuilder,
@ -13,7 +14,7 @@ from baserow.core.utils import Progress
@pytest.mark.django_db
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
@ -25,7 +26,7 @@ def test_get_domain_domain_does_not_exist(data_fixture):
@pytest.mark.django_db
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
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
def test_get_domains(data_fixture):
builder = data_fixture.create_builder_application()
domain_one = data_fixture.create_builder_domain(builder=builder)
domain_two = data_fixture.create_builder_domain(builder=builder)
domain_one = data_fixture.create_builder_custom_domain(builder=builder)
domain_two = data_fixture.create_builder_custom_domain(builder=builder)
domains = list(DomainHandler().get_domains(builder))
domain_ids = [domain.id for domain in domains]
@ -58,7 +59,9 @@ def test_create_domain(data_fixture):
builder = data_fixture.create_builder_application()
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.domain_name == "test.com"
@ -66,7 +69,7 @@ def test_create_domain(data_fixture):
@pytest.mark.django_db
def test_delete_domain(data_fixture):
domain = data_fixture.create_builder_domain()
domain = data_fixture.create_builder_custom_domain()
DomainHandler().delete_domain(domain)
@ -75,7 +78,7 @@ def test_delete_domain(data_fixture):
@pytest.mark.django_db
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")
@ -87,8 +90,8 @@ def test_update_domain(data_fixture):
@pytest.mark.django_db
def test_order_domains(data_fixture):
builder = data_fixture.create_builder_application()
domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
assert DomainHandler().order_domains(builder, [domain_two.id, domain_one.id]) == [
domain_two.id,
@ -105,8 +108,8 @@ def test_order_domains(data_fixture):
@pytest.mark.django_db
def test_order_domains_domain_not_in_builder(data_fixture):
builder = data_fixture.create_builder_application()
domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
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):
builder = data_fixture.create_builder_application()
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
)
@ -132,7 +135,7 @@ def test_get_public_builder_by_name(data_fixture):
@pytest.mark.django_db
def test_get_published_builder_by_missing_domain_name(data_fixture):
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):
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):
builder = data_fixture.create_builder_application(trashed=True)
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
)
@ -151,7 +154,7 @@ def test_get_published_builder_for_trashed_builder(data_fixture):
builder = data_fixture.create_builder_application()
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
)
@ -163,7 +166,7 @@ def test_get_published_builder_for_trashed_builder(data_fixture):
def test_domain_publishing(data_fixture):
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)
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()
builder = data_fixture.create_builder_application(user=user)
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
)
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)
non_public_page = data_fixture.create_builder_page(builder=builder)

View file

@ -2,6 +2,7 @@ from unittest.mock import patch
import pytest
from baserow.contrib.builder.domains.domain_types import CustomDomainType
from baserow.contrib.builder.domains.exceptions import DomainNotInBuilder
from baserow.contrib.builder.domains.models import Domain
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()
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)
@ -26,7 +29,9 @@ def test_create_domain_user_not_in_workspace(data_fixture):
builder = data_fixture.create_builder_application()
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")
@ -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):
user = data_fixture.create_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)
@ -49,7 +54,9 @@ def test_delete_domain_user_not_in_workspace(data_fixture):
user_unrelated = data_fixture.create_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):
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):
user = data_fixture.create_user()
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):
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):
user = data_fixture.create_user()
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):
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):
user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user)
domain_with_access = data_fixture.create_builder_domain(builder=builder)
domain_without_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_custom_domain(builder=builder)
def exclude_domain_without_access(
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):
user = data_fixture.create_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")
@ -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):
user = data_fixture.create_user()
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):
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):
user = data_fixture.create_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")
@ -140,8 +147,8 @@ def test_update_domain_invalid_values(data_fixture):
def test_domains_reordered_signal_sent(domains_reordered_mock, data_fixture):
user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
full_order = DomainService().order_domains(
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):
user = data_fixture.create_user()
builder = data_fixture.create_builder_application()
domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
with pytest.raises(UserNotInWorkspace):
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):
user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user)
domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_domain(order=2)
domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
domain_two = data_fixture.create_builder_custom_domain(order=2)
with pytest.raises(DomainNotInBuilder):
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()
builder = data_fixture.create_builder_application(user=user)
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
)
@ -197,7 +204,7 @@ def test_get_published_builder_by_domain_name_unauthorized(
user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user=user)
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
)
@ -213,7 +220,7 @@ def test_async_publish_domain(mock_run_async_job, data_fixture):
user = data_fixture.create_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)
@ -225,7 +232,7 @@ def test_async_publish_domain(mock_run_async_job, data_fixture):
@pytest.mark.django_db(transaction=True)
def test_async_publish_domain_no_permission(data_fixture, stub_check_permissions):
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(
PermissionException
@ -239,7 +246,7 @@ def test_publish_domain(domain_updated_mock, data_fixture):
user = data_fixture.create_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)
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()
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)

View file

@ -9,7 +9,7 @@ from baserow.core.jobs.handler import JobHandler
def test_publish_domain_job_type(data_fixture):
user = data_fixture.create_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(
user,

View file

@ -410,7 +410,7 @@ def test_builder_application_import(data_fixture):
def test_delete_builder_application_with_published_builder(data_fixture):
builder = data_fixture.create_builder_application()
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
)

View file

@ -139,6 +139,7 @@ x-backend-variables: &backend-variables
BASEROW_DISABLE_LOCKED_MIGRATIONS:
BASEROW_USE_PG_FULLTEXT_SEARCH:
BASEROW_AUTO_VACUUM:
BASEROW_BUILDER_DOMAINS:
services:
# 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_UNIQUE_ROW_VALUES_SIZE_LIMIT:
BASEROW_ROW_PAGE_SIZE_LIMIT:
BASEROW_BUILDER_DOMAINS:
depends_on:
- backend
networks:

View file

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

View file

@ -157,6 +157,7 @@ x-backend-variables: &backend-variables
BASEROW_DISABLE_LOCKED_MIGRATIONS:
BASEROW_USE_PG_FULLTEXT_SEARCH:
BASEROW_AUTO_VACUUM:
BASEROW_BUILDER_DOMAINS:
services:
# A caddy reverse proxy sitting in-front of all the services. Responsible for routing
@ -220,6 +221,7 @@ services:
POSTHOG_HOST:
BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT:
BASEROW_ROW_PAGE_SIZE_LIMIT:
BASEROW_BUILDER_DOMAINS:
depends_on:
- backend
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\_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
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\_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\_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
| Name | Description | Defaults |

View file

@ -29,26 +29,25 @@
</div>
<template v-if="serverErrors.domain_name">
<div v-if="serverErrors.domain_name.code === 'invalid'" class="error">
{{ $t('customDomainForm.invalidDomain') }}
{{ $t('domainForm.invalidDomain') }}
</div>
<div v-if="serverErrors.domain_name.code === 'unique'" class="error">
{{ $t('customDomainForm.notUniqueDomain') }}
</div></template
>
{{ $t('domainForm.notUniqueDomain') }}
</div>
</template>
</FormElement>
</form>
</template>
<script>
import form from '@baserow/modules/core/mixins/form'
import { required, maxLength } from 'vuelidate/lib/validators'
import domainForm from '@baserow/modules/builder/mixins/domainForm'
export default {
name: 'DomainForm',
mixins: [form],
name: 'CustomDomainForm',
mixins: [domainForm],
data() {
return {
serverErrors: { domain_name: null },
values: {
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>

View file

@ -4,15 +4,15 @@
<Error :error="error"></Error>
<FormElement class="control">
<Dropdown
v-model="selectedDomainType"
v-model="selectedDomain"
:show-search="false"
class="domain-settings__domain-type"
>
<DropdownItem
v-for="domainType in domainTypes"
:key="domainType.getType()"
:name="domainType.name"
:value="domainType.getType()"
v-for="option in options"
:key="option.value.domain"
:name="option.name"
:value="option.value"
/>
</Dropdown>
</FormElement>
@ -20,7 +20,9 @@
:is="currentDomainType.formComponent"
ref="domainForm"
:builder="builder"
:domain="selectedDomain.domain"
@submitted="createDomain($event)"
@error="formHasError = $event"
/>
<div class="actions">
<a @click="hideForm">
@ -29,7 +31,7 @@
</a>
<button
class="button button--large"
:disabled="createLoading || formHasError()"
:disabled="createLoading || formHasError"
:class="{ 'button--loading': createLoading }"
@click="onSubmit"
>
@ -58,17 +60,29 @@ export default {
},
data() {
return {
selectedDomainType: 'custom',
selectedDomain: { type: 'custom', domain: 'custom' },
createLoading: false,
formHasError: false,
}
},
computed: {
domainTypes() {
return this.$registry.getAll('domain')
return Object.values(this.$registry.getAll('domain')) || []
},
selectedDomainType() {
return this.selectedDomain.type
},
currentDomainType() {
return this.$registry.get('domain', this.selectedDomainType)
},
options() {
return this.domainTypes.map((domainType) => domainType.options).flat()
},
},
watch: {
selectedDomainType() {
this.$refs?.domainForm?.reset()
},
},
methods: {
...mapActions({
@ -92,13 +106,6 @@ export default {
}
this.createLoading = false
},
formHasError() {
if (this.$refs.domainForm) {
return this.$refs.domainForm.hasError()
} else {
return false
}
},
handleAnyError(error) {
if (
!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 CustomDomainDetails from '@baserow/modules/builder/components/domain/CustomDomainDetails'
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 {
get name() {
@ -10,6 +12,14 @@ export class DomainType extends Registerable {
get detailsComponent() {
return null
}
get formComponent() {
return null
}
get options() {
return []
}
}
export class CustomDomainType extends DomainType {
@ -28,4 +38,44 @@ export class CustomDomainType extends DomainType {
get formComponent() {
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",
"valueHeader": "Value"
},
"domainForm": {
"invalidDomain": "The provided domain name is invalid",
"notUniqueDomain": "The provided domain is already used"
},
"customDomainForm": {
"domainNameLabel": "Domain name",
"invalidDomain": "The provided domain name is invalid",
"notUniqueDomain": "The provided domain is already used"
"domainNameLabel": "Domain name"
},
"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": {
"refresh": "Refresh settings",
"detailLabel": "Show details"
},
"domainTypes": {
"customName": "Custom domain"
"customName": "Custom domain",
"subDomainName": "Subdomain",
"subDomain": "Subdomain of {domain}"
},
"linkElement": {
"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,
StylePageSidePanelType,
} 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 {
TextPathParamType,
@ -158,6 +161,7 @@ export default (context) => {
app.$registry.register('pageSidePanel', new EventsPageSidePanelType(context))
app.$registry.register('domain', new CustomDomainType(context))
app.$registry.register('domain', new SubDomainType(context))
app.$registry.register('pageSettings', new PagePageSettingsType(context))

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
.form-input {
display: block;
display: flex;
border: 1px solid $color-neutral-400;
position: relative;
@include rounded($rounded);
@ -96,3 +96,14 @@
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="[`fa-${icon}`]"
/>
<div class="form-input__suffix disabled">
<div>
<slot name="suffix"></slot>
</div>
</div>
</div>
<div v-if="hasError" class="error">
{{ error }}

View file

@ -5,6 +5,7 @@ import {
} from '@baserow/modules/core/utils/dom'
import dropdownHelpers from './dropdownHelpers'
import _ from 'lodash'
export default {
mixins: [dropdownHelpers],
@ -325,7 +326,7 @@ export default {
getSelectedProperty(value, property) {
for (const i in this.getDropdownItemComponents()) {
const item = this.getDropdownItemComponents()[i]
if (item.value === value) {
if (_.isEqual(item.value, value)) {
return item[property]
}
}
@ -338,7 +339,7 @@ export default {
hasValue() {
for (const i in this.getDropdownItemComponents()) {
const item = this.getDropdownItemComponents()[i]
if (item.value === this.value) {
if (_.isEqual(item.value, this.value)) {
return true
}
}
@ -366,7 +367,9 @@ export default {
)
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
// 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,
BASEROW_ROW_PAGE_SIZE_LIMIT:
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 = [