Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .agents/skills/add-django-config-env-var/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
name: add-django-config-env-var
name: Add Django Config Env Var
description: Add a new environment variable for a Django setting in Baserow and propagate it to the few repo files that usually need it. Use this when a request says a config env var must be added in several places or references `INTEGRATION_LOCAL_BASEROW_PAGE_SIZE_LIMIT` as the pattern to follow.
version: 1.0.0
---

# Add Django Config Env Var
Expand All @@ -17,6 +18,7 @@ When adding a new setting, usually check these files:
- `docker-compose.yml`
- `docker-compose.no-caddy.yml`
- `web-frontend/env-remap.mjs`
- `docs/installation/configuration.md` — the canonical env-var reference table; add a row in the right section
- Backend or frontend code that uses the setting
- A focused test if behavior changes

Expand Down Expand Up @@ -44,7 +46,7 @@ MY_SETTING = int(os.getenv("BASEROW_MY_SETTING", 123))

5. Add or update a targeted test if the setting changes behavior.

6. Add the related documentation
6. Add the related documentation in `docs/installation/configuration.md` — find the right section (e.g. Backend Configuration, Integration Configuration) and add a table row matching the format of the nearest existing entry.

## Quick Checklist

Expand All @@ -53,7 +55,7 @@ MY_SETTING = int(os.getenv("BASEROW_MY_SETTING", 123))
3. Add the Nuxt remap if frontend code needs it
4. Use `settings.<NAME>` in code
5. Add a focused test if needed
6. Add the documentation
6. Add a row to `docs/installation/configuration.md`

## Guardrails

Expand Down
5 changes: 3 additions & 2 deletions .agents/skills/create-update-service/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
name: create-update-service
description: Allow to create or update Baserow Integrations and Services
name: Integrations and Services
description: Create or update Baserow integration types and service types in `contrib/integrations`. Use when adding a new ServiceType/IntegrationType subclass, registering one in `apps.py` or `plugin.js`, or updating an existing dispatch/auth flow.
version: 1.0.0
---

# Create Or Update Baserow Services And Integrations
Expand Down
3 changes: 2 additions & 1 deletion .agents/skills/write-frontend-unit-test/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
name: write-frontend-unit-test
name: Write Frontend Unit Test
description: Write or update Baserow frontend unit tests for core, premium, or enterprise code using the repo's existing Vitest, Nuxt, Vue Test Utils, TestApp, and snapshot patterns.
version: 1.0.0
---

# Write Baserow Frontend Unit Tests
Expand Down
1 change: 1 addition & 0 deletions .claude/skills
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ Examples: `just b test backend/tests/path/`, `just b test-coverage`, `just f tes

Recent history favors short, imperative subjects, often with Conventional Commit prefixes such as `fix:`, `feat:`, and `chore(deps):`. Branch from `develop`, keep PRs focused, and link the related issue or discussion. Include a clear summary, note schema or env changes, attach screenshots for UI work, add a changelog entry when required, and make sure the relevant lint and test commands pass before opening the PR.

## Project Skills

Reusable skills live in `.agents/skills/`. Each subdirectory is a self-contained skill with a `SKILL.md` that describes when and how to apply it. Use these instead of re-deriving the same workflow from scratch.

| Skill directory | When to use |
|---|---|
| `add-django-config-env-var` | Adding a new Django setting backed by an env var and propagating it to `base.py`, docker-compose files, `env-remap.mjs`, and `docs/installation/configuration.md` |
| `write-frontend-unit-test` | Writing or fixing frontend unit tests in `web-frontend`, `premium/web-frontend`, or `enterprise/web-frontend` |
| `create-update-service` | Creating or updating an integration type or service type in `contrib/integrations` |

## Security & Configuration Tips

Do not commit secrets or local overrides. Use `.env.local` for development, keep production settings in the documented deploy configs, and report vulnerabilities privately via the contact path in `CONTRIBUTING.md` rather than opening a public issue.
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@AGENTS.md

## Skills

`.claude/skills` is a symlink to `.agents/skills`, the canonical location for project skills. Both paths resolve to the same directory.
11 changes: 9 additions & 2 deletions backend/src/baserow/contrib/builder/data_sources/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,15 @@ def get_data_source(self, user: AbstractUser, data_source_id: int) -> DataSource

return data_source

def get_data_sources(self, user: AbstractUser, page: Page) -> List[DataSource]:
def get_data_sources(
self, user: AbstractUser, page: Page, with_shared: bool = False
) -> List[DataSource]:
"""
Gets all the data_sources of a given page visible to the given user.

:param user: The user trying to get the data_sources.
:param page: The page that holds the data_sources.
:param with_shared: Whether shared data sources should be included in the result.
:return: The data_sources of that page.
"""

Expand All @@ -86,7 +89,11 @@ def get_data_sources(self, user: AbstractUser, page: Page) -> List[DataSource]:
workspace=page.builder.workspace,
)

return self.handler.get_data_sources(page, base_queryset=user_data_sources)
return self.handler.get_data_sources(
page,
base_queryset=user_data_sources,
with_shared=with_shared,
)

def get_builder_data_sources(
self, user: AbstractUser, builder: "Builder", with_cache=False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,10 @@ def to_internal_value(self, data) -> Optional[str]:
return make_password(data)


class ListFieldsQueryParamsSerializer(serializers.Serializer):
view = serializers.IntegerField(required=False)


class LinkRowFieldSerializerMixin(serializers.ModelSerializer):
link_row_table_primary_field = serializers.SerializerMethodField(
help_text="The primary field of the table that is linked to."
Expand Down
58 changes: 51 additions & 7 deletions backend/src/baserow/contrib/database/api/fields/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
from drf_spectacular.utils import extend_schema
from rest_framework import status
from rest_framework.decorators import permission_classes as method_permission_classes
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
Expand Down Expand Up @@ -120,23 +119,35 @@
from baserow.contrib.database.table.handler import TableHandler
from baserow.contrib.database.tokens.exceptions import NoPermissionToTable
from baserow.contrib.database.tokens.handler import TokenHandler
from baserow.contrib.database.views.exceptions import ViewDoesNotSupportListingRows
from baserow.contrib.database.views.exceptions import (
ViewDoesNotExist,
ViewDoesNotSupportListingRows,
)
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.operations import ListViewFieldsOperationType
from baserow.contrib.database.views.registries import view_ownership_type_registry
from baserow.core.action.registries import action_type_registry
from baserow.core.db import atomic_with_retry_on_deadlock, specific_iterator
from baserow.core.exceptions import UserNotInWorkspace
from baserow.core.exceptions import (
PermissionException,
UserNotInWorkspace,
)
from baserow.core.handler import CoreHandler
from baserow.core.jobs.exceptions import MaxJobCountExceeded
from baserow.core.jobs.handler import JobHandler
from baserow.core.jobs.registries import job_type_registry
from baserow.core.trash.exceptions import CannotDeleteAlreadyDeletedItem
from baserow.core.types import PermissionCheck

from ...rows.handler import RowHandler
from ..views.errors import ERROR_VIEW_DOES_NOT_EXIST
from .serializers import (
ChangePrimaryFieldParamsSerializer,
CreateFieldSerializer,
DuplicateFieldParamsSerializer,
FieldSerializer,
FieldSerializerWithRelatedFields,
ListFieldsQueryParamsSerializer,
PasswordFieldAuthenticationResponseSerializer,
PasswordFieldAuthenticationSerializer,
RelatedFieldsSerializer,
Expand Down Expand Up @@ -190,29 +201,62 @@ def get_permissions(self):
TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST,
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE,
ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST,
}
)
@method_permission_classes([AllowAny])
def get(self, request, table_id):
@validate_query_parameters(ListFieldsQueryParamsSerializer)
def get(self, request, table_id, query_params):
"""
Responds with a list of serialized fields that belong to the table if the user
has access to that workspace.
"""

table = TableHandler().get_table(table_id)

CoreHandler().check_permissions(
view_id = query_params.get("view")
view = ViewHandler().get_view(view_id, table_id=table.id) if view_id else None

table_check = PermissionCheck(
request.user,
ListFieldsOperationType.type,
workspace=table.database.workspace,
context=table,
)
view_check = PermissionCheck(
request.user,
ListViewFieldsOperationType.type,
context=view,
)

checks = [table_check]
if view is not None:
checks.append(view_check)

check_results = CoreHandler().check_multiple_permissions(
checks,
workspace=table.database.workspace,
return_permissions_exceptions=True,
)

if view is None and isinstance(check_results[table_check], PermissionException):
raise check_results[table_check]

if view is not None and isinstance(
check_results[view_check], PermissionException
):
raise check_results[view_check]

TokenHandler().check_table_permissions(
request, ["read", "create", "update"], table, False
)

base_field_queryset = FieldHandler().get_base_fields_queryset()

if view is not None:
ownership_type = view_ownership_type_registry.get(view.ownership_type)
base_field_queryset = ownership_type.enhance_list_fields_queryset(
request.user, view, base_field_queryset
)

fields = specific_iterator(
base_field_queryset.filter(table=table),
per_content_type_queryset_hook=(
Expand Down
9 changes: 8 additions & 1 deletion backend/src/baserow/contrib/database/api/rows/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def get_row_serializer_class(
base_class=None,
is_response=False,
field_ids=None,
exclude_field_ids=None,
field_names_to_include=None,
user_field_names=False,
field_kwargs=None,
Expand Down Expand Up @@ -128,6 +129,9 @@ def get_row_serializer_class(
to be included. Note that the field id must exist in the model in
order to work.
:type field_ids: Optional[Iterable[int]]
:param exclude_field_ids: If provided the field ids in the list will be
excluded from the serializer.
:type exclude_field_ids: Optional[Iterable[int]]
:param field_names_to_include: If provided only the field names in the list will be
included in the serializer. By default all the fields of the model are going
to be included. Note that the field name must exist in the model in
Expand Down Expand Up @@ -158,11 +162,14 @@ def get_row_serializer_class(

for field in field_objects.values():
field_id_matches = field_ids is None or (field["field"].id in field_ids)
field_id_excluded = (
exclude_field_ids is not None and field["field"].id in exclude_field_ids
)
field_name_matches = field_names_to_include is None or (
field["field"].name in field_names_to_include
)

if field_id_matches and field_name_matches:
if field_id_matches and field_name_matches and not field_id_excluded:
name = field["field"].name if user_field_names else field["name"]
field_extra_kwargs = field_kwargs.get(field["name"], {})
# If the field is configured to be read-only, then we want the API to
Expand Down
Loading
Loading