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

Merge branch 'chart-prevent-two-same-series' into 'develop'

Prevent multiple series to be configured with the same field and aggregation type

See merge request 
This commit is contained in:
Petr Stribny 2025-03-07 15:11:12 +00:00
commit f750a27909
4 changed files with 137 additions and 3 deletions
enterprise
backend
src/baserow_enterprise/integrations/local_baserow
tests/baserow_enterprise_tests/integrations/local_baserow/service_types
web-frontend/modules/baserow_enterprise/dashboard/components/data_source

View file

@ -128,6 +128,7 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
with atomic_if_not_already():
table_fields = service.table.field_set.all()
table_field_ids = [field.id for field in table_fields]
series_agg_used = set()
def validate_agg_series(agg_series):
if agg_series["aggregation_type"]:
@ -164,6 +165,18 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
code="invalid_aggregation_raw_type",
)
series_aggregation_reference = (
f"field_{agg_series['field_id']}_{agg_series['aggregation_type']}"
)
if series_aggregation_reference in series_agg_used:
raise DRFValidationError(
detail=f"The series with the field ID {agg_series['field_id']} and "
f"aggregation type {agg_series['aggregation_type']} can only be defined once.",
code="invalid_aggregation_raw_type",
)
else:
series_agg_used.add(series_aggregation_reference)
return True
service.service_aggregation_series.all().delete()
@ -505,6 +518,8 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
raise ServiceImproperlyConfigured(
f"There are no aggregation series defined."
)
series_agg_used = set()
for agg_series in defined_agg_series:
if agg_series.field is None:
raise ServiceImproperlyConfigured(
@ -527,6 +542,17 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
f"with the aggregation type {agg_series.aggregation_type}."
)
series_aggregation_reference = (
f"field_{agg_series.field.id}_{agg_series.aggregation_type}"
)
if series_aggregation_reference in series_agg_used:
raise ServiceImproperlyConfigured(
f"The series with field ID {agg_series.field.id} and "
f"aggregation type {agg_series.aggregation_type} can only be defined once."
)
else:
series_agg_used.add(series_aggregation_reference)
combined_agg_dict |= agg_type._get_aggregation_dict(
queryset, model_field, agg_series.field, include_agg_type=True
)

View file

@ -255,6 +255,41 @@ def test_create_grouped_aggregate_rows_service_group_by_field_not_compatible(
ServiceHandler().create_service(service_type, **values)
@pytest.mark.django_db
def test_create_grouped_aggregate_rows_service_duplicate_series(
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_number_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"},
{"field_id": field_2.id, "aggregation_type": "sum"},
{"field_id": field_2.id, "aggregation_type": "sum"},
],
},
user,
)
with pytest.raises(
ValidationError,
match=f"The series with the field ID {field_2.id} and aggregation type sum can only be defined once.",
):
ServiceHandler().create_service(service_type, **values)
@pytest.mark.django_db
def test_create_grouped_aggregate_rows_service_max_series_exceeded(
data_fixture,
@ -1318,6 +1353,41 @@ def test_grouped_aggregate_rows_service_dispatch_incompatible_aggregation(data_f
)
@pytest.mark.django_db
def test_dispatch_grouped_aggregate_rows_service_duplicate_series(
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)
view = data_fixture.create_grid_view(user=user, table=table)
integration = data_fixture.create_local_baserow_integration(
application=dashboard, user=user
)
service = data_fixture.create_service(
LocalBaserowGroupedAggregateRows,
integration=integration,
table=table,
view=view,
)
LocalBaserowTableServiceAggregationSeries.objects.create(
service=service, field=field, aggregation_type="sum", order=1
)
LocalBaserowTableServiceAggregationSeries.objects.create(
service=service, field=field, aggregation_type="sum", order=1
)
dispatch_context = FakeDispatchContext()
with pytest.raises(ServiceImproperlyConfigured) as exc:
ServiceHandler().dispatch_service(service, dispatch_context)
assert (
exc.value.args[0]
== f"The series with field ID {field.id} and aggregation type sum can only be defined once."
)
@pytest.mark.django_db
def test_grouped_aggregate_rows_service_agg_series_field_trashed(data_fixture):
user = data_fixture.create_user()

View file

@ -15,7 +15,7 @@
@change="aggregationTypeChanged"
>
<DropdownItem
v-for="aggregation in groupedAggregationTypes"
v-for="aggregation in aggregationTypesAvailableForSelection"
:key="aggregation.getType()"
:name="aggregation.getName()"
:value="aggregation.getType()"
@ -33,11 +33,11 @@
<Dropdown
v-model="values.field_id"
:error="fieldHasErrors('field_id')"
:disabled="compatibleFields.length === 0"
:disabled="fieldsAvailableForSelection.length === 0"
@change="v$.values.field_id.$touch"
>
<DropdownItem
v-for="field in compatibleFields"
v-for="field in fieldsAvailableForSelection"
:key="field.id"
:name="field.name"
:value="field.id"
@ -75,6 +75,10 @@ export default {
type: Array,
required: true,
},
aggregationSeries: {
type: Array,
required: true,
},
seriesIndex: {
type: Number,
required: true,
@ -97,9 +101,29 @@ export default {
groupedAggregationTypes() {
return this.$registry.getOrderedList('groupedAggregation')
},
aggregationTypesAvailableForSelection() {
return this.groupedAggregationTypes.filter(
(aggregationType) =>
this.isSeriesRepeated(
this.values.field_id,
aggregationType.getType()
) === false ||
this.values.aggregation_type === aggregationType.getType()
)
},
aggregationTypeNames() {
return this.groupedAggregationTypes.map((aggType) => aggType.getType())
},
usedSeries() {
return this.aggregationSeries
.filter(
(series) =>
series.aggregation_type !== null && series.field_id !== null
)
.map((series) => {
return `${series.field_id}_${series.aggregation_type}`
})
},
compatibleFields() {
if (!this.values.aggregation_type) {
return []
@ -112,6 +136,13 @@ export default {
aggType.fieldIsCompatible(tableField)
)
},
fieldsAvailableForSelection() {
return this.compatibleFields.filter(
(field) =>
this.isSeriesRepeated(field.id, this.values.aggregation_type) ===
false || this.values.field_id === field.id
)
},
compatibleTableFieldIds() {
return this.compatibleFields.map((field) => field.id)
},
@ -141,6 +172,12 @@ export default {
}
},
methods: {
isSeriesRepeated(fieldId, aggregationType) {
if (fieldId === null || aggregationType === null) {
return false
}
return this.usedSeries.includes(`${fieldId}_${aggregationType}`)
},
fieldIconClass(field) {
const fieldType = this.$registry.get('field', field.type)
return fieldType.iconClass

View file

@ -85,6 +85,7 @@
v-for="(series, index) in values.aggregation_series"
:key="index"
:table-fields="tableFields"
:aggregation-series="values.aggregation_series"
:series-index="index"
:default-values="series"
@delete-series="deleteSeries"