From 7a6d0c5b12200c96228874f29b8cc1779e0c1768 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:35:36 -0700 Subject: [PATCH] lookup --> query --- docs/api.md | 16 +++++----- .../filter-editor.component.spec.ts | 12 ++++---- .../filter-editor/filter-editor.component.ts | 8 ++--- src-ui/src/app/data/filter-rule-type.ts | 6 ++-- src-ui/src/app/utils/query-params.spec.ts | 6 ++-- src-ui/src/app/utils/query-params.ts | 4 +-- src/documents/filters.py | 24 +++++++-------- src/documents/models.py | 2 +- .../tests/test_api_filter_by_custom_fields.py | 30 +++++++++---------- 9 files changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/api.md b/docs/api.md index 2db23b477..d15d6207b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -277,39 +277,39 @@ attribute with various information about the search results: ### Filtering by custom fields You can filter documents by their custom field values by specifying the -`custom_field_lookup` query parameter. Here are some recipes for common +`custom_field_query` query parameter. Here are some recipes for common use cases: 1. Documents with a custom field "due" (date) between Aug 1, 2024 and Sept 1, 2024 (inclusive): - `?custom_field_lookup=["due", "range", ["2024-08-01", "2024-09-01"]]` + `?custom_field_query=["due", "range", ["2024-08-01", "2024-09-01"]]` 2. Documents with a custom field "customer" (text) that equals "bob" (case sensitive): - `?custom_field_lookup=["customer", "exact", "bob"]` + `?custom_field_query=["customer", "exact", "bob"]` 3. Documents with a custom field "answered" (boolean) set to `true`: - `?custom_field_lookup=["answered", "exact", true]` + `?custom_field_query=["answered", "exact", true]` 4. Documents with a custom field "favorite animal" (select) set to either "cat" or "dog": - `?custom_field_lookup=["favorite animal", "in", ["cat", "dog"]]` + `?custom_field_query=["favorite animal", "in", ["cat", "dog"]]` 5. Documents with a custom field "address" (text) that is empty: - `?custom_field_lookup=["OR", ["address", "isnull", true], ["address", "exact", ""]]` + `?custom_field_query=["OR", ["address", "isnull", true], ["address", "exact", ""]]` 6. Documents that don't have a field called "foo": - `?custom_field_lookup=["foo", "exists", false]` + `?custom_field_query=["foo", "exists", false]` 7. Documents that have document links "references" to both document 3 and 7: - `?custom_field_lookup=["references", "contains", [3, 7]]` + `?custom_field_query=["references", "contains", [3, 7]]` All field types support basic operations including `exact`, `in`, `isnull`, and `exists`. String, URL, and monetary fields support case-insensitive diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts index 7d218710e..19a53f76c 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts @@ -55,7 +55,7 @@ import { FILTER_HAS_ANY_CUSTOM_FIELDS, FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS, FILTER_HAS_CUSTOM_FIELDS_ALL, - FILTER_CUSTOM_FIELDS_LOOKUP, + FILTER_CUSTOM_FIELDS_QUERY, } from 'src/app/data/filter-rule-type' import { Correspondent } from 'src/app/data/correspondent' import { DocumentType } from 'src/app/data/document-type' @@ -887,11 +887,11 @@ describe('FilterEditorComponent', () => { ).toEqual(['42', CustomFieldQueryOperator.Exists, 'true']) })) - it('should ingest filter rules for custom field lookup', fakeAsync(() => { + it('should ingest filter rules for custom field queries', fakeAsync(() => { expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy() component.filterRules = [ { - rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, + rule_type: FILTER_CUSTOM_FIELDS_QUERY, value: '["AND", [[42, "exists", "true"],[43, "exists", "true"]]]', }, ] @@ -909,7 +909,7 @@ describe('FilterEditorComponent', () => { // atom component.filterRules = [ { - rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, + rule_type: FILTER_CUSTOM_FIELDS_QUERY, value: '[42, "exists", "true"]', }, ] @@ -1459,7 +1459,7 @@ describe('FilterEditorComponent', () => { expect(component.customFieldQueriesModel.queries[0].value.length).toEqual(1) expect(component.filterRules).toEqual([ { - rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, + rule_type: FILTER_CUSTOM_FIELDS_QUERY, value: JSON.stringify([ CustomFieldQueryLogicalOperator.Or, [[custom_fields[0].id, 'exists', 'true']], @@ -1876,7 +1876,7 @@ describe('FilterEditorComponent', () => { component.filterRules = [ { - rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, + rule_type: FILTER_CUSTOM_FIELDS_QUERY, value: '["AND",[["42","exists","true"],["43","exists","true"]]]', }, ] diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts index b7723f6f0..24ef1b347 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts @@ -62,7 +62,7 @@ import { FILTER_HAS_CUSTOM_FIELDS_ANY, FILTER_HAS_CUSTOM_FIELDS_ALL, FILTER_HAS_ANY_CUSTOM_FIELDS, - FILTER_CUSTOM_FIELDS_LOOKUP, + FILTER_CUSTOM_FIELDS_QUERY, } from 'src/app/data/filter-rule-type' import { FilterableDropdownSelectionModel, @@ -234,7 +234,7 @@ export class FilterEditorComponent return $localize`Without any tag` } - case FILTER_CUSTOM_FIELDS_LOOKUP: + case FILTER_CUSTOM_FIELDS_QUERY: return $localize`Custom fields query` case FILTER_TITLE: @@ -525,7 +525,7 @@ export class FilterEditorComponent false ) break - case FILTER_CUSTOM_FIELDS_LOOKUP: + case FILTER_CUSTOM_FIELDS_QUERY: try { const query = JSON.parse(rule.value) if (Array.isArray(query)) { @@ -786,7 +786,7 @@ export class FilterEditorComponent ) if (queries.length > 0) { filterRules.push({ - rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, + rule_type: FILTER_CUSTOM_FIELDS_QUERY, value: JSON.stringify(queries[0]), }) } diff --git a/src-ui/src/app/data/filter-rule-type.ts b/src-ui/src/app/data/filter-rule-type.ts index 7fa1d2179..1c6b1cdf8 100644 --- a/src-ui/src/app/data/filter-rule-type.ts +++ b/src-ui/src/app/data/filter-rule-type.ts @@ -55,7 +55,7 @@ export const FILTER_HAS_CUSTOM_FIELDS_ANY = 39 export const FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS = 40 export const FILTER_HAS_ANY_CUSTOM_FIELDS = 41 -export const FILTER_CUSTOM_FIELDS_LOOKUP = 42 +export const FILTER_CUSTOM_FIELDS_QUERY = 42 export const FILTER_RULE_TYPES: FilterRuleType[] = [ { @@ -320,8 +320,8 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ default: true, }, { - id: FILTER_CUSTOM_FIELDS_LOOKUP, - filtervar: 'custom_field_lookup', + id: FILTER_CUSTOM_FIELDS_QUERY, + filtervar: 'custom_field_query', datatype: 'string', multi: false, }, diff --git a/src-ui/src/app/utils/query-params.spec.ts b/src-ui/src/app/utils/query-params.spec.ts index 201770a78..64a89efec 100644 --- a/src-ui/src/app/utils/query-params.spec.ts +++ b/src-ui/src/app/utils/query-params.spec.ts @@ -2,7 +2,7 @@ import { convertToParamMap } from '@angular/router' import { FilterRule } from '../data/filter-rule' import { FILTER_CORRESPONDENT, - FILTER_CUSTOM_FIELDS_LOOKUP, + FILTER_CUSTOM_FIELDS_QUERY, FILTER_HAS_ANY_TAG, FILTER_HAS_CUSTOM_FIELDS_ALL, FILTER_HAS_CUSTOM_FIELDS_ANY, @@ -214,7 +214,7 @@ describe('QueryParams Utils', () => { expect(transformedFilterRules).toEqual([ { - rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, + rule_type: FILTER_CUSTOM_FIELDS_QUERY, value: JSON.stringify([ CustomFieldQueryLogicalOperator.Or, [ @@ -240,7 +240,7 @@ describe('QueryParams Utils', () => { expect(transformedFilterRules).toEqual([ { - rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, + rule_type: FILTER_CUSTOM_FIELDS_QUERY, value: JSON.stringify([ CustomFieldQueryLogicalOperator.And, [ diff --git a/src-ui/src/app/utils/query-params.ts b/src-ui/src/app/utils/query-params.ts index 68009fec1..608d4edfb 100644 --- a/src-ui/src/app/utils/query-params.ts +++ b/src-ui/src/app/utils/query-params.ts @@ -4,7 +4,7 @@ import { FilterRuleType, FILTER_RULE_TYPES, FILTER_HAS_CUSTOM_FIELDS_ANY, - FILTER_CUSTOM_FIELDS_LOOKUP, + FILTER_CUSTOM_FIELDS_QUERY, FILTER_HAS_CUSTOM_FIELDS_ALL, } from '../data/filter-rule-type' import { ListViewState } from '../services/document-list-view.service' @@ -83,7 +83,7 @@ export function transformLegacyFilterRules( ], ] filterRules.push({ - rule_type: FILTER_CUSTOM_FIELDS_LOOKUP, + rule_type: FILTER_CUSTOM_FIELDS_QUERY, value: JSON.stringify(customFieldQueryExpression), }) } diff --git a/src/documents/filters.py b/src/documents/filters.py index 9ea7920c7..b6ac591fe 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -36,8 +36,8 @@ ID_KWARGS = ["in", "exact"] INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"] DATE_KWARGS = ["year", "month", "day", "date__gt", "gt", "date__lt", "lt"] -CUSTOM_FIELD_LOOKUP_MAX_DEPTH = 10 -CUSTOM_FIELD_LOOKUP_MAX_ATOMS = 20 +CUSTOM_FIELD_QUERY_MAX_DEPTH = 10 +CUSTOM_FIELD_QUERY_MAX_ATOMS = 20 class CorrespondentFilterSet(FilterSet): @@ -237,7 +237,7 @@ def handle_validation_prefix(func: Callable): return wrapper -class CustomFieldLookupParser: +class CustomFieldQueryParser: EXPR_BY_CATEGORY = { "basic": ["exact", "in", "isnull", "exists"], "string": [ @@ -351,7 +351,7 @@ class CustomFieldLookupParser: elif len(expr) == 3: return self._parse_atom(*expr) raise serializers.ValidationError( - [_("Invalid custom field lookup expression")], + [_("Invalid custom field query expression")], ) @handle_validation_prefix @@ -485,7 +485,7 @@ class CustomFieldLookupParser: if not supported: raise serializers.ValidationError( [ - _("{data_type} does not support lookup expr {expr!r}.").format( + _("{data_type} does not support query expr {expr!r}.").format( data_type=custom_field.data_type, expr=raw_op, ), @@ -506,7 +506,7 @@ class CustomFieldLookupParser: custom_field.data_type == CustomField.FieldDataType.DATE and prefix in self.DATE_COMPONENTS ): - # DateField admits lookups in the form of `year__exact`, etc. These take integers. + # DateField admits queries in the form of `year__exact`, etc. These take integers. field = serializers.IntegerField() elif custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK: # We can be more specific here and make sure the value is a list. @@ -568,7 +568,7 @@ class CustomFieldLookupParser: custom_fields__value_document_ids__isnull=False, ) - # First we lookup reverse links from the requested documents. + # First we look up reverse links from the requested documents. links = CustomFieldInstance.objects.filter( document_id__in=value, field__data_type=CustomField.FieldDataType.DOCUMENTLINK, @@ -600,7 +600,7 @@ class CustomFieldLookupParser: self._current_depth -= 1 -class CustomFieldLookupFilter(Filter): +class CustomFieldQueryFilter(Filter): def __init__(self, validation_prefix): """ A filter that filters documents based on custom field name and value. @@ -615,10 +615,10 @@ class CustomFieldLookupFilter(Filter): if not value: return qs - parser = CustomFieldLookupParser( + parser = CustomFieldQueryParser( self._validation_prefix, - max_query_depth=CUSTOM_FIELD_LOOKUP_MAX_DEPTH, - max_atom_count=CUSTOM_FIELD_LOOKUP_MAX_ATOMS, + max_query_depth=CUSTOM_FIELD_QUERY_MAX_DEPTH, + max_atom_count=CUSTOM_FIELD_QUERY_MAX_ATOMS, ) q, annotations = parser.parse(value) @@ -672,7 +672,7 @@ class DocumentFilterSet(FilterSet): exclude=True, ) - custom_field_lookup = CustomFieldLookupFilter("custom_field_lookup") + custom_field_query = CustomFieldQueryFilter("custom_field_query") shared_by__id = SharedByUser() diff --git a/src/documents/models.py b/src/documents/models.py index ed6560934..452c57c78 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -507,7 +507,7 @@ class SavedViewFilterRule(models.Model): (39, _("has custom field in")), (40, _("does not have custom field in")), (41, _("does not have custom field")), - (42, _("custom fields lookup")), + (42, _("custom fields query")), ] saved_view = models.ForeignKey( diff --git a/src/documents/tests/test_api_filter_by_custom_fields.py b/src/documents/tests/test_api_filter_by_custom_fields.py index c26f61d3e..421376e44 100644 --- a/src/documents/tests/test_api_filter_by_custom_fields.py +++ b/src/documents/tests/test_api_filter_by_custom_fields.py @@ -181,7 +181,7 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase): "/api/documents/?" + "&".join( ( - f"custom_field_lookup={query_string}", + f"custom_field_query={query_string}", "ordering=archive_serial_number", "page=1", f"page_size={len(self.documents)}", @@ -205,7 +205,7 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase): "/api/documents/?" + "&".join( ( - f"custom_field_lookup={query_string}", + f"custom_field_query={query_string}", "ordering=archive_serial_number", "page=1", f"page_size={len(self.documents)}", @@ -477,57 +477,57 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase): def test_invalid_json(self): self._assert_validation_error( "not valid json", - ["custom_field_lookup"], + ["custom_field_query"], "must be valid JSON", ) def test_invalid_expression(self): self._assert_validation_error( json.dumps("valid json but not valid expr"), - ["custom_field_lookup"], - "Invalid custom field lookup expression", + ["custom_field_query"], + "Invalid custom field query expression", ) def test_invalid_custom_field_name(self): self._assert_validation_error( json.dumps(["invalid name", "iexact", "foo"]), - ["custom_field_lookup", "0"], + ["custom_field_query", "0"], "is not a valid custom field", ) def test_invalid_operator(self): self._assert_validation_error( json.dumps(["integer_field", "iexact", "foo"]), - ["custom_field_lookup", "1"], - "does not support lookup expr", + ["custom_field_query", "1"], + "does not support query expr", ) def test_invalid_value(self): self._assert_validation_error( json.dumps(["select_field", "exact", "not an option"]), - ["custom_field_lookup", "2"], + ["custom_field_query", "2"], "integer", ) def test_invalid_logical_operator(self): self._assert_validation_error( json.dumps(["invalid op", ["integer_field", "gt", 0]]), - ["custom_field_lookup", "0"], + ["custom_field_query", "0"], "Invalid logical operator", ) def test_invalid_expr_list(self): self._assert_validation_error( json.dumps(["AND", "not a list"]), - ["custom_field_lookup", "1"], + ["custom_field_query", "1"], "Invalid expression list", ) def test_invalid_operator_prefix(self): self._assert_validation_error( json.dumps(["integer_field", "foo__gt", 0]), - ["custom_field_lookup", "1"], - "does not support lookup expr", + ["custom_field_query", "1"], + "does not support query expr", ) def test_query_too_deep(self): @@ -536,7 +536,7 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase): query = ["NOT", query] self._assert_validation_error( json.dumps(query), - ["custom_field_lookup", *(["1"] * 10)], + ["custom_field_query", *(["1"] * 10)], "Maximum nesting depth exceeded", ) @@ -545,6 +545,6 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase): query = ["AND", [atom for _ in range(21)]] self._assert_validation_error( json.dumps(query), - ["custom_field_lookup", "1", "20"], + ["custom_field_query", "1", "20"], "Maximum number of query conditions exceeded", )