1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-06 22:08:52 +00:00

Grouped aggregate service group by registry

This commit is contained in:
Petr Stribny 2025-03-04 03:43:50 +00:00
parent 054468ec06
commit ddd4a4d257
6 changed files with 247 additions and 5 deletions
enterprise
backend
src/baserow_enterprise
apps.py
integrations
tests/baserow_enterprise_tests/integrations/local_baserow/service_types
web-frontend/modules/baserow_enterprise
dashboard/components/data_source
plugin.js

View file

@ -156,6 +156,33 @@ class BaserowEnterpriseConfig(AppConfig):
grouped_aggregation_registry.register(VarianceFieldAggregationType())
grouped_aggregation_registry.register(MedianFieldAggregationType())
from baserow.contrib.database.fields.field_types import (
AutonumberFieldType,
BooleanFieldType,
EmailFieldType,
LongTextFieldType,
NumberFieldType,
PhoneNumberFieldType,
RatingFieldType,
SingleSelectFieldType,
TextFieldType,
URLFieldType,
)
from baserow_enterprise.integrations.registries import (
grouped_aggregation_group_by_registry,
)
grouped_aggregation_group_by_registry.register(TextFieldType())
grouped_aggregation_group_by_registry.register(LongTextFieldType())
grouped_aggregation_group_by_registry.register(URLFieldType())
grouped_aggregation_group_by_registry.register(EmailFieldType())
grouped_aggregation_group_by_registry.register(NumberFieldType())
grouped_aggregation_group_by_registry.register(RatingFieldType())
grouped_aggregation_group_by_registry.register(BooleanFieldType())
grouped_aggregation_group_by_registry.register(PhoneNumberFieldType())
grouped_aggregation_group_by_registry.register(AutonumberFieldType())
grouped_aggregation_group_by_registry.register(SingleSelectFieldType())
from baserow.core.registries import subject_type_registry
subject_type_registry.register(TeamSubjectType())

View file

@ -3,6 +3,7 @@ from django.db.models import F
from rest_framework.exceptions import ValidationError as DRFValidationError
from baserow.contrib.database.fields.exceptions import FieldTypeDoesNotExist
from baserow.contrib.database.views.exceptions import AggregationTypeDoesNotExist
from baserow.contrib.database.views.utils import AnnotatedAggregation
from baserow.contrib.integrations.local_baserow.integration_types import (
@ -28,7 +29,10 @@ from baserow_enterprise.api.integrations.local_baserow.serializers import (
from baserow_enterprise.integrations.local_baserow.models import (
LocalBaserowGroupedAggregateRows,
)
from baserow_enterprise.integrations.registries import grouped_aggregation_registry
from baserow_enterprise.integrations.registries import (
grouped_aggregation_group_by_registry,
grouped_aggregation_registry,
)
from baserow_enterprise.services.types import (
ServiceAggregationGroupByDict,
ServiceAggregationSeriesDict,
@ -186,7 +190,8 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
group_bys: list[ServiceAggregationGroupByDict] | None = None,
):
with atomic_if_not_already():
table_field_ids = service.table.field_set.values_list("id", flat=True)
table_fields = service.table.field_set.all()
table_field_ids = [field.id for field in table_fields]
def validate_agg_group_by(group_by):
if (
@ -199,6 +204,23 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
code="invalid_field",
)
field = next(
(
field
for field in table_fields
if field.id == group_by["field_id"]
)
)
try:
grouped_aggregation_group_by_registry.get_by_type(field.get_type())
except FieldTypeDoesNotExist:
raise DRFValidationError(
detail=f"The field with ID {group_by['field_id']} cannot "
"be used as a group by field.",
code="invalid_field",
)
return True
service.service_aggregation_group_bys.all().delete()
@ -405,6 +427,14 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
raise ServiceImproperlyConfigured(
f"The field with ID {group_by.field.id} is trashed."
)
try:
grouped_aggregation_group_by_registry.get_by_type(
group_by.field.get_type()
)
except FieldTypeDoesNotExist:
raise ServiceImproperlyConfigured(
f"The field with ID {group_by.field.id} cannot be used for group by."
)
group_by_values.append(group_by.field.db_column)
if len(group_by_values) > 0:

View file

@ -1,4 +1,8 @@
from baserow.contrib.database.fields.registries import FieldAggregationType
from baserow.contrib.database.fields.exceptions import (
FieldTypeAlreadyRegistered,
FieldTypeDoesNotExist,
)
from baserow.contrib.database.fields.registries import FieldAggregationType, FieldType
from baserow.contrib.database.views.exceptions import (
AggregationTypeAlreadyRegistered,
AggregationTypeDoesNotExist,
@ -20,3 +24,20 @@ class GroupedAggregationTypeRegistry(Registry[FieldAggregationType]):
grouped_aggregation_registry: GroupedAggregationTypeRegistry = (
GroupedAggregationTypeRegistry()
)
class GroupedAggregationGroupByRegistry(Registry[FieldType]):
"""
The main registry for storing field types compatible
with the grouped aggregate service to be used as group by
fields.
"""
name = "grouped_aggregations_group_by"
does_not_exist_exception_class = FieldTypeDoesNotExist
already_registered_exception_class = FieldTypeAlreadyRegistered
grouped_aggregation_group_by_registry: GroupedAggregationGroupByRegistry = (
GroupedAggregationGroupByRegistry()
)

View file

@ -220,6 +220,41 @@ def test_create_grouped_aggregate_rows_service_group_by_field_not_in_table(
ServiceHandler().create_service(service_type, **values)
@pytest.mark.django_db
def test_create_grouped_aggregate_rows_service_group_by_field_not_compatible(
data_fixture,
):
user = data_fixture.create_user()
dashboard = data_fixture.create_dashboard_application(user=user)
table = data_fixture.create_database_table(user=user)
table_2 = data_fixture.create_database_table(user=user)
field = data_fixture.create_number_field(table=table)
field_2 = data_fixture.create_uuid_field(table=table)
view = data_fixture.create_grid_view(user=user, table=table)
integration = data_fixture.create_local_baserow_integration(
application=dashboard, user=user
)
service_type = service_type_registry.get("local_baserow_grouped_aggregate_rows")
values = service_type.prepare_values(
{
"view_id": view.id,
"table_id": view.table_id,
"integration_id": integration.id,
"service_aggregation_series": [
{"field_id": field.id, "aggregation_type": "sum"},
],
"service_aggregation_group_bys": [{"field_id": field_2.id}],
},
user,
)
with pytest.raises(
ValidationError,
match=f"The field with ID {field_2.id} cannot be used as a group by field.",
):
ServiceHandler().create_service(service_type, **values)
@pytest.mark.django_db
def test_create_grouped_aggregate_rows_service_max_series_exceeded(
data_fixture,
@ -662,6 +697,46 @@ def test_update_grouped_aggregate_rows_service_group_by_field_not_in_table(
ServiceHandler().update_service(service_type, service=service, **values)
@pytest.mark.django_db
def test_update_grouped_aggregate_rows_service_group_by_field_not_in_compatible(
data_fixture,
):
user = data_fixture.create_user()
dashboard = data_fixture.create_dashboard_application(user=user)
table = data_fixture.create_database_table(user=user)
field = data_fixture.create_number_field(table=table)
field_2 = data_fixture.create_uuid_field(table=table)
view = data_fixture.create_grid_view(user=user, table=table)
integration = data_fixture.create_local_baserow_integration(
application=dashboard, user=user
)
service_type = service_type_registry.get("local_baserow_grouped_aggregate_rows")
service = data_fixture.create_service(
LocalBaserowGroupedAggregateRows,
integration=integration,
table=table,
view=view,
)
values = service_type.prepare_values(
{
"table_id": table.id,
"service_aggregation_series": [
{"field_id": field.id, "aggregation_type": "sum"},
],
"service_aggregation_group_bys": [{"field_id": field_2.id}],
},
user,
service,
)
with pytest.raises(
ValidationError,
match=f"The field with ID {field_2.id} cannot be used as a group by field.",
):
ServiceHandler().update_service(service_type, service=service, **values)
@pytest.mark.django_db
def test_update_grouped_aggregate_rows_service_max_series_exceeded(
data_fixture,
@ -1298,6 +1373,38 @@ def test_grouped_aggregate_rows_service_group_by_field_trashed(data_fixture):
assert exc.value.args[0] == f"The field with ID {field_2.id} is trashed."
@pytest.mark.django_db
def test_grouped_aggregate_rows_service_group_by_field_not_compatible(data_fixture):
user = data_fixture.create_user()
dashboard = data_fixture.create_dashboard_application(user=user)
table = data_fixture.create_database_table(user=user)
field = data_fixture.create_number_field(table=table)
field_2 = data_fixture.create_uuid_field(table=table)
integration = data_fixture.create_local_baserow_integration(
application=dashboard, user=user
)
service = data_fixture.create_service(
LocalBaserowGroupedAggregateRows,
integration=integration,
table=table,
)
LocalBaserowTableServiceAggregationSeries.objects.create(
service=service, field=field, aggregation_type="sum", order=1
)
LocalBaserowTableServiceAggregationGroupBy.objects.create(
service=service, field=field_2, order=1
)
dispatch_context = FakeDispatchContext()
with pytest.raises(ServiceImproperlyConfigured) as exc:
ServiceHandler().dispatch_service(service, dispatch_context)
assert (
exc.value.args[0]
== f"The field with ID {field_2.id} cannot be used for group by."
)
@pytest.mark.django_db
def test_grouped_aggregate_rows_service_table_trashed(data_fixture):
user = data_fixture.create_user()

View file

@ -40,8 +40,13 @@ export default {
}
},
computed: {
compatibleFields() {
return this.tableFields.filter((field) =>
this.$registry.exists('groupedAggregationGroupedBy', field.type)
)
},
groupByOptions() {
const tableFieldOptions = this.tableFields.map((field) => {
const tableFieldOptions = this.compatibleFields.map((field) => {
return {
name: field.name,
value: field.id,

View file

@ -72,7 +72,18 @@ import {
MedianViewAggregationType,
} from '@baserow/modules/database/viewAggregationTypes'
import { PeriodicDataSyncDeactivatedNotificationType } from '@baserow_enterprise/notificationTypes'
import {
TextFieldType,
LongTextFieldType,
URLFieldType,
EmailFieldType,
NumberFieldType,
RatingFieldType,
BooleanFieldType,
SingleSelectFieldType,
PhoneNumberFieldType,
AutonumberFieldType,
} from '@baserow/modules/database/fieldTypes'
import {
FF_AB_SSO,
FF_DASHBOARDS,
@ -244,6 +255,47 @@ export default (context) => {
new UniqueCountViewAggregationType(context)
)
app.$registry.register(
'groupedAggregationGroupedBy',
new TextFieldType(context)
)
app.$registry.register(
'groupedAggregationGroupedBy',
new LongTextFieldType(context)
)
app.$registry.register(
'groupedAggregationGroupedBy',
new NumberFieldType(context)
)
app.$registry.register(
'groupedAggregationGroupedBy',
new URLFieldType(context)
)
app.$registry.register(
'groupedAggregationGroupedBy',
new RatingFieldType(context)
)
app.$registry.register(
'groupedAggregationGroupedBy',
new BooleanFieldType(context)
)
app.$registry.register(
'groupedAggregationGroupedBy',
new EmailFieldType(context)
)
app.$registry.register(
'groupedAggregationGroupedBy',
new SingleSelectFieldType(context)
)
app.$registry.register(
'groupedAggregationGroupedBy',
new PhoneNumberFieldType(context)
)
app.$registry.register(
'groupedAggregationGroupedBy',
new AutonumberFieldType(context)
)
app.$registry.register(
'notification',
new PeriodicDataSyncDeactivatedNotificationType(context)