Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to portfolio summary #4862

Merged
merged 14 commits into from
Oct 31, 2024
2 changes: 1 addition & 1 deletion seed/models/data_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ def append_to_apply_labels():
return
value = baseline if cycle_key == "baseline" else current
if rule.condition == rule.RULE_RANGE:
if value:
if value is not None:
result = check_range()
results.append(result)
append_to_apply_labels()
Expand Down
32 changes: 21 additions & 11 deletions seed/static/seed/js/controllers/portfolio_summary_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,13 +238,13 @@ angular.module('SEED.controller.portfolio_summary', [])
// order of cycle property filter is dynamic based on column_sorts
const cycle_priority = baseline_first ? [baseline_cycle, current_cycle] : [current_cycle, baseline_cycle];

get_paginated_properties(page, per_page, cycle_priority[0], access_level_instance_id, true, null, $scope.goal.id).then((result0) => {
get_paginated_properties(page, per_page, cycle_priority[0], access_level_instance_id, true, null).then((result0) => {
$scope.inventory_pagination = result0.pagination;
let properties = result0.results;
combined_result[cycle_priority[0].id] = properties;
const property_ids = properties.map((p) => p.id);

get_paginated_properties(page, per_page, cycle_priority[1], access_level_instance_id, false, property_ids, $scope.goal.id).then((result1) => {
get_paginated_properties(page, per_page, cycle_priority[1], access_level_instance_id, false, property_ids).then((result1) => {
properties = result1.results;
// if result0 returns fewer properties than result1, use result1 for ui-grid config
if (result1.pagination.num_pages > $scope.inventory_pagination.num_pages) {
Expand All @@ -261,7 +261,7 @@ angular.module('SEED.controller.portfolio_summary', [])
});
};

const get_paginated_properties = (page, chunk, cycle, access_level_instance_id, include_filters_sorts, include_property_ids = null, goal_id = null) => {
const get_paginated_properties = (page, chunk, cycle, access_level_instance_id, include_filters_sorts, include_property_ids = null) => {
const fn = inventory_service.get_properties;
const [filters, sorts] = include_filters_sorts ? [$scope.column_filters, $scope.column_sorts] : [[], []];

Expand All @@ -281,7 +281,8 @@ angular.module('SEED.controller.portfolio_summary', [])
table_column_ids.join(),
access_level_instance_id,
include_property_ids,
goal_id // optional param to retrieve goal note details
$scope.goal.id, // optional param to retrieve goal note details
$scope.related_model_sort // optional param to sort on related models
);
};

Expand Down Expand Up @@ -553,7 +554,7 @@ angular.module('SEED.controller.portfolio_summary', [])
field: 'goal_note.question',
displayName: 'Question',
enableFiltering: false,
enableSorting: false,
enableSorting: true,
editableCellTemplate: 'ui-grid/dropdownEditor',
editDropdownOptionsArray: $scope.question_options,
editDropdownIdLabel: 'value',
Expand All @@ -574,7 +575,7 @@ angular.module('SEED.controller.portfolio_summary', [])
field: 'goal_note.resolution',
displayName: 'Resolution',
enableFiltering: false,
enableSorting: false,
enableSorting: true,
enableCellEdit: !$scope.viewer,
cellClass: !$scope.viewer && 'cell-edit',
width: 300
Expand All @@ -583,7 +584,7 @@ angular.module('SEED.controller.portfolio_summary', [])
field: 'historical_note.text',
displayName: 'Historical Notes',
enableFiltering: false,
enableSorting: false,
enableSorting: true,
enableCellEdit: !$scope.viewer,
cellClass: !$scope.viewer && 'cell-edit',
width: 300
Expand All @@ -592,7 +593,7 @@ angular.module('SEED.controller.portfolio_summary', [])
field: 'goal_note.passed_checks',
displayName: 'Passed Checks',
enableFiltering: false,
enableSorting: false,
enableSorting: true,
editableCellTemplate: 'ui-grid/dropdownEditor',
editDropdownOptionsArray: [{ id: 1, value: true }, { id: 2, value: false }],
editDropdownIdLabel: 'value',
Expand All @@ -611,7 +612,7 @@ angular.module('SEED.controller.portfolio_summary', [])
field: 'goal_note.new_or_acquired',
displayName: 'New Build or Acquired',
enableFiltering: false,
enableSorting: false,
enableSorting: true,
editableCellTemplate: 'ui-grid/dropdownEditor',
editDropdownOptionsArray: [{ id: 1, value: true }, { id: 2, value: false }],
editDropdownIdLabel: 'value',
Expand Down Expand Up @@ -791,7 +792,8 @@ angular.module('SEED.controller.portfolio_summary', [])
// parse the filters and sorts
for (const column of formatted_columns) {
// format column if cycle specific
const { name, filters, sort } = column;
let { name } = column;
const { filters, sort } = column;
// remove the column id at the end of the name
const column_name = name.split('_').slice(0, -1).join('_');

Expand All @@ -818,9 +820,17 @@ angular.module('SEED.controller.portfolio_summary', [])
}
}

$scope.related_model_sort = false;
if (sort.direction) {
// remove the column id at the end of the name
const column_name = name.split('_').slice(0, -1).join('_');
let column_name;
$scope.related_model_sort = ['historical_note.', 'goal_note.'].some((value) => name.includes(value));
if ($scope.related_model_sort) {
name = `property__${name.replace('.', '__')}`;
column_name = name;
} else {
column_name = name.split('_').slice(0, -1).join('_');
}
const display = [$scope.columnDisplayByName[name], sort.direction].join(' ');
$scope.column_sorts = [{
name,
Expand Down
15 changes: 7 additions & 8 deletions seed/static/seed/js/services/inventory_service.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ angular.module('SEED.service.inventory', []).factory('inventory_service', [
shown_column_ids = null,
access_level_instance_id = null,
include_property_ids = null,
goal_id = null
goal_id = null,
related_model_sort = null
) => {
organization_id = organization_id ?? user_service.get_organization().id;

Expand Down Expand Up @@ -100,15 +101,13 @@ angular.module('SEED.service.inventory', []).factory('inventory_service', [
exclude_view_ids,
include_property_ids,
// Pass the current profile (if one exists) to limit the column data that is returned
profile_id
profile_id,
// conditionally add optional params
...(access_level_instance_id && { access_level_instance_id }),
...(goal_id && { goal_id }),
...(related_model_sort && { related_model_sort })
};
// add access_level_instance if it exists
if (access_level_instance_id) {
data.access_level_instance_id = access_level_instance_id;
}
if (goal_id) {
data.goal_id = goal_id;
}

return $http
.post(
Expand Down
11 changes: 7 additions & 4 deletions seed/utils/goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,15 @@ def get_portfolio_summary(org, goal):
summary["total_passing"] = GoalNote.objects.filter(goal=goal, passed_checks=True).count()
summary["total_new_or_acquired"] = GoalNote.objects.filter(goal=goal, new_or_acquired=True).count()

# Remaining Calcs are restricted to passing checks and not new/acquired
# Remaining calculations are restricted to passing check
# New builds in the baseline year will be excluded from calculations
# use goal notes relation to properties to get valid properties views
valid_property_ids = GoalNote.objects.filter(goal=goal, passed_checks=True, new_or_acquired=False).values_list(
"property__id", flat=True
goal_notes = GoalNote.objects.filter(goal=goal)
new_property_ids = goal_notes.filter(new_or_acquired=True).values_list("property__id", flat=True)
valid_property_ids = goal_notes.filter(passed_checks=True).values_list("property__id", flat=True)
property_views = property_views.filter(property__id__in=valid_property_ids).exclude(
cycle=goal.baseline_cycle, property__id__in=new_property_ids
)
property_views = property_views.filter(property__id__in=valid_property_ids)

# Create annotations for kbtu calcs. "eui" is based on goal column priority
property_views = property_views.annotate(
Expand Down
14 changes: 10 additions & 4 deletions seed/utils/inventory_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
TaxLotView,
)
from seed.serializers.pint import apply_display_unit_preferences
from seed.utils.search import FilterError, build_view_filters_and_sorts
from seed.utils.search import FilterError, build_related_model_filters_and_sorts, build_view_filters_and_sorts


def get_filtered_results(request: Request, inventory_type: Literal["property", "taxlot"], profile_id: int) -> JsonResponse:
Expand All @@ -38,6 +38,7 @@ def get_filtered_results(request: Request, inventory_type: Literal["property", "
profile_id = request.query_params.get("profile_id", profile_id)
shown_column_ids = request.query_params.get("shown_column_ids")
goal_id = request.data.get("goal_id")
related_model_sort = request.data.get("related_model_sort")

if not org_id:
return JsonResponse(
Expand Down Expand Up @@ -102,10 +103,15 @@ def get_filtered_results(request: Request, inventory_type: Literal["property", "
only_used=False,
include_related=include_related,
)

try:
filters, annotations, order_by = build_view_filters_and_sorts(
request.query_params, columns_from_database, inventory_type, org.access_level_names
)
# Sorts initiated from Portfolio Summary that contain related model names (goal_note, historical_note) require custom handling
if related_model_sort:
filters, annotations, order_by = build_related_model_filters_and_sorts(request.query_params, columns_from_database)
else:
filters, annotations, order_by = build_view_filters_and_sorts(
request.query_params, columns_from_database, inventory_type, org.access_level_names
)
except FilterError as e:
return JsonResponse({"status": "error", "message": f"Error filtering: {e!s}"}, status=status.HTTP_400_BAD_REQUEST)

Expand Down
40 changes: 39 additions & 1 deletion 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 Q
from django.db.models import Case, IntegerField, 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 @@ -507,3 +507,41 @@ def build_view_filters_and_sorts(
annotations.update(parsed_annotations)

return new_filters, annotations, order_by


def build_related_model_filters_and_sorts(filters: QueryDict, columns: list[dict]) -> tuple[Q, AnnotationDict, list[str]]:
"""Primarily used for sorting the Portfolio Summary on related columns like goal_notes and historical_notes"""
order_by = []
annotations = {}
columns_by_name = {}
for column in columns:
if column["related"]:
continue
columns_by_name[column["name"]] = column

column_name = filters.get("order_by")
if not column_name:
return Q(), {}, ["id"]

direction = "-" if column_name.startswith("-") else ""
column_name = column_name.lstrip("-")

if "goal_note" in column_name:
column_name = column_name.replace("goal_note", "goalnote")

boolean_column = column_name in ["property__goalnote__passed_checks", "property__goalnote__new_or_acquired"]
target: Union[bool, str]
if boolean_column:
target = False
else:
target = ""

related_model = Case(When(**{column_name: target}, then=Value(1)), default=Value(0), output_field=IntegerField())
Copy link
Contributor Author

@perryr16 perryr16 Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By defautt, django .order_by(...) will put blank values above "A" values.

  • asc: None, "A", "B", "Z"
  • desc: "Z", "B", "A", None

I thought this was counterintuitive. If a user sorted a text column like historical notes, "A" should be at the top, and blanks should be below "Z". If this doesn't matter, I can simplify this function by removing the related_model annotation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the way you implemented it!

parsed_annotations = {"related_model": related_model}
parsed_sort = [direction + "related_model", direction + column_name]

if parsed_sort is not None:
order_by.extend(parsed_sort)
annotations.update(parsed_annotations)

return Q(), annotations, order_by
Loading