Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions seed/static/seed/js/controllers/inventory_list_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1148,16 +1148,8 @@ angular.module('SEED.controller.inventory_list', []).controller('inventory_list_
);
}

// disable sorting (but not filtering) on related data until the backend can filter/sort over two models
for (const i in $scope.columns) {
const column = $scope.columns[i];
if (column.related) {
column.enableSorting = false;
// let title = 'Filtering disabled for property columns on the taxlot list.';
// if ($scope.inventory_type === 'properties') {
// title = 'Filtering disabled for taxlot columns on the property list.';
// }
}
if (column.derived_column != null) {
column.enableSorting = false;
const title = 'Sorting and filtering disabled for derived columns.';
Expand Down
35 changes: 34 additions & 1 deletion seed/tests/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import pytest
from django.db import models
from django.db.models import Q
from django.db.models import Min, Q
from django.db.models.fields.json import KeyTextTransform
from django.db.models.functions import Cast, Coalesce, Collate, Replace
from django.http.request import QueryDict
Expand Down Expand Up @@ -312,6 +312,39 @@ class TestCase:
f'Failed "{test_case.name}"; actual: {annotations}; expected: {test_case.expected_annotations}',
)

def test_order_by_related_columns(self):
columns = Column.retrieve_all(self.fake_org, "property", only_used=False, include_related=True)
related_column = Column.objects.get(table_name="TaxLotState", column_name="jurisdiction_tax_lot_id")
query_dict = QueryDict(f"order_by=jurisdiction_tax_lot_id_{related_column.id}")

_, annotations, order_by = build_view_filters_and_sorts(query_dict, columns, "property")

annotation_key = f"related_jurisdiction_tax_lot_id_{related_column.id}_sort"
self.assertEqual(len(order_by), 1)
self.assertIsInstance(order_by[0], Collate)
self.assertIn(annotation_key, annotations)
self.assertIsInstance(annotations[annotation_key], Min)

def test_order_by_related_extra_data_columns(self):
Column.objects.create(
column_name="related_extra",
data_type="string",
is_extra_data=True,
table_name="TaxLotState",
organization=self.fake_org,
)
columns = Column.retrieve_all(self.fake_org, "property", only_used=False, include_related=True)
related_column = Column.objects.get(table_name="TaxLotState", column_name="related_extra")
query_dict = QueryDict(f"order_by=related_extra_{related_column.id}")

_, annotations, order_by = build_view_filters_and_sorts(query_dict, columns, "property")

annotation_key = f"related_related_extra_{related_column.id}_sort"
self.assertEqual(len(order_by), 1)
self.assertIsInstance(order_by[0], Collate)
self.assertIn(annotation_key, annotations)
self.assertIsInstance(annotations[annotation_key], Min)

def test_filter_and_sorts_parser_annotations_works(self):
# -- Setup
# create extra data column with a number type
Expand Down
68 changes: 63 additions & 5 deletions seed/utils/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from typing import Any, Union

from django.db import models
from django.db.models import Case, IntegerField, Q, Value, When
from django.db.models import Case, IntegerField, Min, Q, Value, When
from django.db.models.fields.json import KeyTextTransform
from django.db.models.functions import Cast, Coalesce, Collate, Replace
from django.http.request import QueryDict
Expand Down Expand Up @@ -311,6 +311,36 @@ def _build_extra_data_annotations(column_name: str, data_type: str) -> tuple[str
return final_field_name, annotations


def _build_related_extra_data_expression(column_name: str, data_type: str, state_prefix: str):
"""
Build a casted expression for a related column's extra_data field.

This mirrors the conversions in `_build_extra_data_annotations`, but allows
specifying which state relationship to traverse (property vs taxlot).
"""
json_path = f"{state_prefix}__extra_data"
expression = KeyTextTransform(column_name, json_path)

if data_type == "integer":
expression = Cast(
Replace(expression, models.Value(","), models.Value("")),
output_field=models.IntegerField(),
)
elif data_type in {"number", "float", "area", "eui", "ghg", "ghg_intensity"}:
expression = Cast(
Replace(expression, models.Value(","), models.Value("")),
output_field=models.FloatField(),
)
elif data_type in {"date", "datetime"}:
expression = Cast(expression, output_field=models.DateField())
elif data_type == "boolean":
expression = Cast(expression, output_field=models.BooleanField())
else:
expression = Coalesce(expression, models.Value(""), output_field=models.TextField())

return expression


def _parse_view_filter(
filter_expression: str,
filter_value: Union[str, bool],
Expand Down Expand Up @@ -371,7 +401,11 @@ def _parse_view_filter(


def _parse_view_sort(
sort_expression: str, columns_by_name: dict[str, dict], inventory_type: str, access_level_names: list[str]
sort_expression: str,
columns_by_name: dict[str, dict],
related_columns_by_name: dict[str, dict],
inventory_type: str,
access_level_names: list[str],
) -> tuple[Union[None, str, Collate], AnnotationDict]:
"""Parse a sort expression

Expand Down Expand Up @@ -401,6 +435,26 @@ def _parse_view_sort(
return f"{direction}{new_field_name}", annotations
else:
return f"{direction}state__{column_name}", {}
elif column_name in related_columns_by_name:
column = related_columns_by_name[column_name]
related_prefix = "taxlotproperty__taxlot_view__" if inventory_type == "property" else "taxlotproperty__property_view__"
state_prefix = f"{related_prefix}state"
annotation_name = f"related_{column['name']}_sort"

if column["is_extra_data"]:
expression = _build_related_extra_data_expression(column["column_name"], column["data_type"], state_prefix)
else:
expression = models.F(f"{state_prefix}__{column['column_name']}")

annotations = {annotation_name: Min(expression)}
if column["data_type"] in {"None", "string"}:
order_expression = Collate(annotation_name, "natural_sort")
if direction:
order_expression = order_expression.desc()
else:
order_expression = f"{direction}{annotation_name}"

return order_expression, annotations
elif column_name in access_level_names:
return f"{direction}{inventory_type}__access_level_instance__path__{column_name}", {}
else:
Expand Down Expand Up @@ -447,10 +501,12 @@ def build_view_filters_and_sorts(
:return: filters, annotations and sorts
"""
columns_by_name = {}
related_columns_by_name = {}
for column in columns:
if column["related"]:
continue
columns_by_name[column["name"]] = column
related_columns_by_name[column["name"]] = column
else:
columns_by_name[column["name"]] = column

new_filters = Q()
annotations = {}
Expand Down Expand Up @@ -507,7 +563,9 @@ def build_view_filters_and_sorts(
order_by = []

for sort_expression in filters.getlist("order_by", ["id"]):
parsed_sort, parsed_annotations = _parse_view_sort(sort_expression, columns_by_name, inventory_type, access_level_names)
parsed_sort, parsed_annotations = _parse_view_sort(
sort_expression, columns_by_name, related_columns_by_name, inventory_type, access_level_names
)
if parsed_sort is not None:
order_by.append(parsed_sort)
annotations.update(parsed_annotations)
Expand Down
Loading