mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-28 06:22:24 +00:00
Grouped aggregate service group by registry
This commit is contained in:
parent
054468ec06
commit
ddd4a4d257
6 changed files with 247 additions and 5 deletions
enterprise
backend
src/baserow_enterprise
tests/baserow_enterprise_tests/integrations/local_baserow/service_types
web-frontend/modules/baserow_enterprise
|
@ -156,6 +156,33 @@ class BaserowEnterpriseConfig(AppConfig):
|
||||||
grouped_aggregation_registry.register(VarianceFieldAggregationType())
|
grouped_aggregation_registry.register(VarianceFieldAggregationType())
|
||||||
grouped_aggregation_registry.register(MedianFieldAggregationType())
|
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
|
from baserow.core.registries import subject_type_registry
|
||||||
|
|
||||||
subject_type_registry.register(TeamSubjectType())
|
subject_type_registry.register(TeamSubjectType())
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.db.models import F
|
||||||
|
|
||||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
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.exceptions import AggregationTypeDoesNotExist
|
||||||
from baserow.contrib.database.views.utils import AnnotatedAggregation
|
from baserow.contrib.database.views.utils import AnnotatedAggregation
|
||||||
from baserow.contrib.integrations.local_baserow.integration_types import (
|
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 (
|
from baserow_enterprise.integrations.local_baserow.models import (
|
||||||
LocalBaserowGroupedAggregateRows,
|
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 (
|
from baserow_enterprise.services.types import (
|
||||||
ServiceAggregationGroupByDict,
|
ServiceAggregationGroupByDict,
|
||||||
ServiceAggregationSeriesDict,
|
ServiceAggregationSeriesDict,
|
||||||
|
@ -186,7 +190,8 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
|
||||||
group_bys: list[ServiceAggregationGroupByDict] | None = None,
|
group_bys: list[ServiceAggregationGroupByDict] | None = None,
|
||||||
):
|
):
|
||||||
with atomic_if_not_already():
|
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):
|
def validate_agg_group_by(group_by):
|
||||||
if (
|
if (
|
||||||
|
@ -199,6 +204,23 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
|
||||||
code="invalid_field",
|
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
|
return True
|
||||||
|
|
||||||
service.service_aggregation_group_bys.all().delete()
|
service.service_aggregation_group_bys.all().delete()
|
||||||
|
@ -405,6 +427,14 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
|
||||||
raise ServiceImproperlyConfigured(
|
raise ServiceImproperlyConfigured(
|
||||||
f"The field with ID {group_by.field.id} is trashed."
|
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)
|
group_by_values.append(group_by.field.db_column)
|
||||||
|
|
||||||
if len(group_by_values) > 0:
|
if len(group_by_values) > 0:
|
||||||
|
|
|
@ -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 (
|
from baserow.contrib.database.views.exceptions import (
|
||||||
AggregationTypeAlreadyRegistered,
|
AggregationTypeAlreadyRegistered,
|
||||||
AggregationTypeDoesNotExist,
|
AggregationTypeDoesNotExist,
|
||||||
|
@ -20,3 +24,20 @@ class GroupedAggregationTypeRegistry(Registry[FieldAggregationType]):
|
||||||
grouped_aggregation_registry: GroupedAggregationTypeRegistry = (
|
grouped_aggregation_registry: GroupedAggregationTypeRegistry = (
|
||||||
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()
|
||||||
|
)
|
||||||
|
|
|
@ -220,6 +220,41 @@ def test_create_grouped_aggregate_rows_service_group_by_field_not_in_table(
|
||||||
ServiceHandler().create_service(service_type, **values)
|
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
|
@pytest.mark.django_db
|
||||||
def test_create_grouped_aggregate_rows_service_max_series_exceeded(
|
def test_create_grouped_aggregate_rows_service_max_series_exceeded(
|
||||||
data_fixture,
|
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)
|
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
|
@pytest.mark.django_db
|
||||||
def test_update_grouped_aggregate_rows_service_max_series_exceeded(
|
def test_update_grouped_aggregate_rows_service_max_series_exceeded(
|
||||||
data_fixture,
|
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."
|
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
|
@pytest.mark.django_db
|
||||||
def test_grouped_aggregate_rows_service_table_trashed(data_fixture):
|
def test_grouped_aggregate_rows_service_table_trashed(data_fixture):
|
||||||
user = data_fixture.create_user()
|
user = data_fixture.create_user()
|
||||||
|
|
|
@ -40,8 +40,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
compatibleFields() {
|
||||||
|
return this.tableFields.filter((field) =>
|
||||||
|
this.$registry.exists('groupedAggregationGroupedBy', field.type)
|
||||||
|
)
|
||||||
|
},
|
||||||
groupByOptions() {
|
groupByOptions() {
|
||||||
const tableFieldOptions = this.tableFields.map((field) => {
|
const tableFieldOptions = this.compatibleFields.map((field) => {
|
||||||
return {
|
return {
|
||||||
name: field.name,
|
name: field.name,
|
||||||
value: field.id,
|
value: field.id,
|
||||||
|
|
|
@ -72,7 +72,18 @@ import {
|
||||||
MedianViewAggregationType,
|
MedianViewAggregationType,
|
||||||
} from '@baserow/modules/database/viewAggregationTypes'
|
} from '@baserow/modules/database/viewAggregationTypes'
|
||||||
import { PeriodicDataSyncDeactivatedNotificationType } from '@baserow_enterprise/notificationTypes'
|
import { PeriodicDataSyncDeactivatedNotificationType } from '@baserow_enterprise/notificationTypes'
|
||||||
|
import {
|
||||||
|
TextFieldType,
|
||||||
|
LongTextFieldType,
|
||||||
|
URLFieldType,
|
||||||
|
EmailFieldType,
|
||||||
|
NumberFieldType,
|
||||||
|
RatingFieldType,
|
||||||
|
BooleanFieldType,
|
||||||
|
SingleSelectFieldType,
|
||||||
|
PhoneNumberFieldType,
|
||||||
|
AutonumberFieldType,
|
||||||
|
} from '@baserow/modules/database/fieldTypes'
|
||||||
import {
|
import {
|
||||||
FF_AB_SSO,
|
FF_AB_SSO,
|
||||||
FF_DASHBOARDS,
|
FF_DASHBOARDS,
|
||||||
|
@ -244,6 +255,47 @@ export default (context) => {
|
||||||
new UniqueCountViewAggregationType(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(
|
app.$registry.register(
|
||||||
'notification',
|
'notification',
|
||||||
new PeriodicDataSyncDeactivatedNotificationType(context)
|
new PeriodicDataSyncDeactivatedNotificationType(context)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue