(core) Add a row to summary tables grouped by list column(s) corresponding to empty lists

Summary:
Adds some special handling to summary table and lookup logic:

- Source rows with empty choicelists/reflists get a corresponding summary row with an empty string/reference when grouping by that column, instead of excluding them from any group
- Adds a new `QueryOperation` 'empty' in the client which is used in `LinkingState`, `QuerySet`, and `recursiveMoveToCursorPos` to match empty lists in source tables against falsy values in linked summary tables.
- Adds a new parameter `match_empty` to the Python `CONTAINS` function so that regular formulas can implement the same behaviour as summary tables. See https://grist.slack.com/archives/C0234CPPXPA/p1654030490932119
- Uses the new `match_empty` argument in the formula generated for the `group` column when detaching a summary table.

Test Plan: Updated and extended Python and nbrowser tests of summary tables grouped by choicelists to test for new behaviour with empty lists.

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3471
This commit is contained in:
Alex Hall
2022-06-07 16:57:29 +02:00
parent 3b30c052bc
commit 1c89d08ea3
10 changed files with 128 additions and 43 deletions

View File

@@ -8,6 +8,7 @@ from six.moves import xrange
import column
import depend
import docmodel
import functions
import logger
import lookup
import records
@@ -336,8 +337,8 @@ class Table(object):
lookup_values = []
for group_col in groupby_cols:
lookup_value = getattr(rec, group_col)
if isinstance(self.all_columns[group_col],
(column.ChoiceListColumn, column.ReferenceListColumn)):
group_col_obj = self.all_columns[group_col]
if isinstance(group_col_obj, (column.ChoiceListColumn, column.ReferenceListColumn)):
# Check that ChoiceList/ReferenceList cells have appropriate types.
# Don't iterate over characters of a string.
if isinstance(lookup_value, (six.binary_type, six.text_type)):
@@ -347,6 +348,13 @@ class Table(object):
lookup_value = set(lookup_value)
except TypeError:
return []
if not lookup_value:
if isinstance(group_col_obj, column.ChoiceListColumn):
lookup_value = {""}
else:
lookup_value = {0}
else:
lookup_value = [lookup_value]
lookup_values.append(lookup_value)
@@ -459,11 +467,11 @@ class Table(object):
for col_id in sorted(kwargs):
value = kwargs[col_id]
if isinstance(value, lookup._Contains):
value = value.value
# While users should use CONTAINS on lookup values,
# the marker is moved to col_id so that the LookupMapColumn knows how to
# update its index correctly for that column.
col_id = lookup._Contains(col_id)
col_id = value._replace(value=col_id)
value = value.value
else:
col = self.get_column(col_id)
# Convert `value` to the correct type of rich value for that column
@@ -527,7 +535,7 @@ class Table(object):
# _summary_source_table._summary_simple determines whether
# the column named self._summary_helper_col_id is a single reference
# or a reference list.
lookup_value = rec if self._summary_simple else lookup._Contains(rec)
lookup_value = rec if self._summary_simple else functions.CONTAINS(rec)
return self._summary_source_table.lookup_records(**{
self._summary_helper_col_id: lookup_value
})