From ee17f94edfd356a9194cc314ac06c77696241271 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 18 Dec 2024 15:36:52 -0700 Subject: [PATCH 01/14] frontend table ui fucntional --- .../inventory_create_controller.js | 138 ++++++++++++++++++ seed/static/seed/js/seed.js | 36 +++++ .../seed/js/services/inventory_service.js | 8 + .../seed/partials/inventory_create.html | 135 +++++++++++++++++ seed/static/seed/partials/inventory_nav.html | 1 + seed/static/seed/scss/style.scss | 4 + seed/templates/seed/_scripts.html | 1 + 7 files changed, 323 insertions(+) create mode 100644 seed/static/seed/js/controllers/inventory_create_controller.js create mode 100644 seed/static/seed/partials/inventory_create.html diff --git a/seed/static/seed/js/controllers/inventory_create_controller.js b/seed/static/seed/js/controllers/inventory_create_controller.js new file mode 100644 index 0000000000..3ffd91c167 --- /dev/null +++ b/seed/static/seed/js/controllers/inventory_create_controller.js @@ -0,0 +1,138 @@ +/** + * SEED Platform (TM), Copyright (c) Alliance for Sustainable Energy, LLC, and other contributors. + * See also https://github.com/SEED-platform/seed/blob/main/LICENSE.md + */ +angular.module('SEED.controller.inventory_create', []).controller('inventory_create_controller', [ + '$scope', + '$window', + '$stateParams', + 'ah_service', + 'inventory_service', + 'access_level_tree', + 'all_columns', + 'cycles', + 'profiles', + 'organization_payload', + // eslint-disable-next-line func-names + function ( + $scope, + $window, + $stateParams, + ah_service, + inventory_service, + access_level_tree, + all_columns, + cycles, + profiles, + organization_payload, + ) { + $scope.inventory = {}; + $scope.inventory_type = $stateParams.inventory_type; + $scope.inventory_types = ['Property', 'TaxLot']; + const table_name = $scope.inventory_type === 'taxlots' ? 'TaxLotState' : 'PropertyState'; + $scope.cycles = cycles.cycles; + $scope.profiles = profiles; + $scope.profile = []; + $scope.columns = all_columns; + + $scope.matching_columns = []; + $scope.extra_columns = []; + $scope.canonical_columns = []; + $scope.columns.forEach((c) => { + if (c.table_name == table_name) { + if (c.is_matching_criteria) $scope.matching_columns.push(c); + if (c.is_extra_data) $scope.extra_columns.push(c); + if (!c.is_extra_data && !c.derived_column) $scope.canonical_columns.push(c); + } + }); + $scope.inventory.form_columns =[...$scope.matching_columns]; + $scope.form_values = []; + + $scope.remove_column = (index) => { + $scope.inventory.form_columns.splice(index, 1) + $scope.form_values[index] = null; + }; + $scope.add_column = () => $scope.inventory.form_columns.push({displayName: '', table_name: table_name}); + + + // ACCESS LEVEL TREE + $scope.access_level_tree = access_level_tree.access_level_tree; + $scope.level_names = access_level_tree.access_level_names.map((level, i) => ({ + index: i, + name: level + })); + const access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth($scope.access_level_tree); + $scope.change_selected_level_index = () => { + const new_level_instance_depth = parseInt($scope.inventory.level_name_index, 10) + 1; + $scope.potential_level_instances = access_level_instances_by_depth[new_level_instance_depth]; + }; + $scope.inventory.level_name_index = $scope.level_names.at(-1).index; + $scope.change_selected_level_index(); + $scope.inventory.access_level_instance = $scope.potential_level_instances.at(0).id; + + // COLUMN LIST PROFILES + $scope.set_columns = (type) => { + remove_empty_last_column(); + switch (type) { + case 'canonical': + $scope.inventory.form_columns = Array.from(new Set([...$scope.inventory.form_columns, ...$scope.canonical_columns])); + break; + case 'extra': + $scope.inventory.form_columns = Array.from(new Set([...$scope.inventory.form_columns, ...$scope.extra_columns])); + break; + default: + $scope.inventory.form_columns = [...$scope.matching_columns]; + $scope.inventory.form_columns = $scope.inventory.form_columns.map(c => ({...c, value: null})); + $scope.form_values = []; + } + } + + const remove_empty_last_column = () => { + if (!_.isEmpty($scope.inventory.form_columns) && $scope.inventory.form_columns.at(-1).displayName === '') { + $scope.inventory.form_columns.pop(); + } + } + + + // FORM LOGIC + $scope.change_profile = () => { + const profile_column_names = $scope.profile.columns.map(p => p.column_name); + $scope.inventory.form_columns = $scope.columns.filter(c => profile_column_names.includes(c.column_name)) + } + + $scope.select_column = (column, idx) => { + column.value = $scope.form_values.at(idx); + $scope.inventory.form_columns[idx] = column; + } + + $scope.change_column = (displayName, idx) => { + const defaults = { + table_name: table_name, + is_extra_data: true, + is_matching_criteria: false, + data_type: 'string', + } + let column = $scope.columns.find(c => c.displayName === displayName) || {displayName: displayName}; + column = {...defaults, ...column}; + + column.value = $scope.form_values.at(idx); + $scope.inventory.form_columns[idx] = column; + }; + + $scope.change_value = (value, idx) => { + $scope.form_values[idx] = value; + } + + $scope.save_inventory = () => { + console.log('save_inventory', $scope.inventory) + } + + + + + + + + + } +]); diff --git a/seed/static/seed/js/seed.js b/seed/static/seed/js/seed.js index 00be6a3b4b..1f07f79071 100644 --- a/seed/static/seed/js/seed.js +++ b/seed/static/seed/js/seed.js @@ -89,6 +89,7 @@ 'SEED.controller.insights_program', 'SEED.controller.insights_property', 'SEED.controller.inventory_column_list_profiles', + 'SEED.controller.inventory_create', 'SEED.controller.inventory_cycles', 'SEED.controller.inventory_detail', 'SEED.controller.inventory_detail_analyses', @@ -2243,6 +2244,41 @@ ] } }) + .state({ + name: 'inventory_create', + url: '/{inventory_type:properties|taxlots}/create', + templateUrl: `${static_url}seed/partials/inventory_create.html`, + controller: 'inventory_create_controller', + resolve: { + access_level_tree: [ + 'organization_service', 'user_service', + (organization_service, user_service) => { + const organization_id = user_service.get_organization().id; + return organization_service.get_descendant_access_level_tree(organization_id); + } + ], + all_columns: [ + '$stateParams', + 'inventory_service', + ($stateParams, inventory_service) => { + if ($stateParams.inventory_type === 'properties') { + return inventory_service.get_property_columns(); + } + return inventory_service.get_taxlot_columns(); + } + ], + cycles: ['cycle_service', (cycle_service) => cycle_service.get_cycles()], + profiles: [ + '$stateParams', + 'inventory_service', + ($stateParams, inventory_service) => { + const inventory_type = $stateParams.inventory_type === 'properties' ? 'Property' : 'Tax Lot'; + return inventory_service.get_column_list_profiles('List View Profile', inventory_type); + } + ], + organization_payload: ['user_service', 'organization_service', (user_service, organization_service) => organization_service.get_organization(user_service.get_organization().id)] + } + }) .state({ name: 'inventory_cycles', url: '/{inventory_type:properties|taxlots}/cycles', diff --git a/seed/static/seed/js/services/inventory_service.js b/seed/static/seed/js/services/inventory_service.js index a698015265..a4fadbc95f 100644 --- a/seed/static/seed/js/services/inventory_service.js +++ b/seed/static/seed/js/services/inventory_service.js @@ -1294,6 +1294,14 @@ angular.module('SEED.service.inventory', []).factory('inventory_service', [ taxlot_view_ids }).then((response) => response.data); + inventory_service.create_inventory = (data, inventory_type) => { + return $http.post(`/api/v3/${inventory_type}/form_create/`, data, { + params: { + organization_id: user_service.get_organization().id + } + }); + } + return inventory_service; } ]); diff --git a/seed/static/seed/partials/inventory_create.html b/seed/static/seed/partials/inventory_create.html new file mode 100644 index 0000000000..ea2257e10f --- /dev/null +++ b/seed/static/seed/partials/inventory_create.html @@ -0,0 +1,135 @@ + + +
+ +
+ +
+
+
+
+ +
+
+
+

Create a {$ inventory_type === 'properties' ? 'Property' : 'Tax Lot'$}

+
+
+ +
+
+
+ +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Inventory TypeColumnValueMatching CriteriaExtra DataData Type
{$ column.table_name.slice(0, -5) $} + + + + {$ column.is_matching_criteria $}{$ column.is_extra_data $}{$ column.data_type $} + +
+ +
+
+
\ No newline at end of file diff --git a/seed/static/seed/partials/inventory_nav.html b/seed/static/seed/partials/inventory_nav.html index eca1a717ae..81c698a6f6 100644 --- a/seed/static/seed/partials/inventory_nav.html +++ b/seed/static/seed/partials/inventory_nav.html @@ -6,3 +6,4 @@ Map Data Summary +Create a {$ inventory_type == 'taxlots' ? 'Tax Lot' : 'Property' $} diff --git a/seed/static/seed/scss/style.scss b/seed/static/seed/scss/style.scss index 415b693897..328e68e9a8 100755 --- a/seed/static/seed/scss/style.scss +++ b/seed/static/seed/scss/style.scss @@ -1668,6 +1668,10 @@ a:not([href]) { border-collapse: separate; border-spacing: 5px; } + .content-row { + display: flex; + margin: 10px 0; + } } } diff --git a/seed/templates/seed/_scripts.html b/seed/templates/seed/_scripts.html index 242b7c30c7..37c107d9ed 100644 --- a/seed/templates/seed/_scripts.html +++ b/seed/templates/seed/_scripts.html @@ -85,6 +85,7 @@ + From d063dd9e5fa50d81bdbb1707efa0aeb1a345934a Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 18 Dec 2024 16:11:35 -0700 Subject: [PATCH 02/14] backend dev testing --- .../inventory_create_controller.js | 1 + seed/tests/test_property_views.py | 48 ++++++++++++++++ seed/views/v3/organizations.py | 8 +++ seed/views/v3/properties.py | 56 ++++++++++++++++++- 4 files changed, 112 insertions(+), 1 deletion(-) diff --git a/seed/static/seed/js/controllers/inventory_create_controller.js b/seed/static/seed/js/controllers/inventory_create_controller.js index 3ffd91c167..40b4db5147 100644 --- a/seed/static/seed/js/controllers/inventory_create_controller.js +++ b/seed/static/seed/js/controllers/inventory_create_controller.js @@ -45,6 +45,7 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre if (!c.is_extra_data && !c.derived_column) $scope.canonical_columns.push(c); } }); + // create a copy, not a reference $scope.inventory.form_columns =[...$scope.matching_columns]; $scope.form_values = []; diff --git a/seed/tests/test_property_views.py b/seed/tests/test_property_views.py index 08a85747b4..48bdf9bb7b 100644 --- a/seed/tests/test_property_views.py +++ b/seed/tests/test_property_views.py @@ -47,6 +47,7 @@ TaxLotProperty, TaxLotView, ) +from seed.serializers.columns import ColumnSerializer from seed.serializers.properties import PropertyStatePromoteWritableSerializer, PropertyStateSerializer from seed.test_helpers.fake import ( FakeColumnFactory, @@ -658,6 +659,52 @@ def test_meters_exist(self): false_result = self.client.post(url, false_post_params, content_type="application/json") self.assertEqual(b"false", false_result.content) + def test_property_form_create(self): + original_state_count = PropertyState.objects.count() + original_view_count = PropertyView.objects.count() + original_property_count = Property.objects.count() + + url = reverse("api:v3:properties-form-create") + f"?organization_id={self.org.pk}" + pm_property_id = Column.objects.get(column_name="pm_property_id", organization=self.org) + pm_property_id = ColumnSerializer(pm_property_id).data + custom_id_1 = Column.objects.get(column_name="custom_id_1", table_name="PropertyState", organization=self.org) + custom_id_1 = ColumnSerializer(custom_id_1).data + extra = {"displayName": "Extra Data Column", "value": "XYZ"} + data = { + "access_level_instance": self.org.root.id, + "cycle": self.cycle.id, + "form_columns": [ + {**pm_property_id, "value": "123"}, + {**custom_id_1, "value": "ABC"}, + extra + ] + } + + self.client.post(url, json.dumps(data), content_type="application/json") + assert PropertyState.objects.count() == original_state_count + 1 + assert PropertyView.objects.count() == original_view_count + 1 + assert Property.objects.count() == original_property_count + 1 + + # # verify with existing matches + cycle2 = self.cycle_factory.get_cycle(start=datetime(2000, 10, 10, tzinfo=get_current_timezone())) + cycle3 = self.cycle_factory.get_cycle(start=datetime(2020, 10, 10, tzinfo=get_current_timezone())) + view = self.property_view_factory.get_property_view(cycle=cycle2, pm_property_id="456", custom_id_1="DEF") + + data = { + "access_level_instance": self.org.root.id, + "cycle": self.cycle.id, + "form_columns": [ + {**pm_property_id, "value": "456"}, + {**custom_id_1, "value": "DEF"}, + {"displayName": "Column2", "value": "GHI"} + ] + } + + self.client.post(url, json.dumps(data), content_type="application/json") + + + assert True + class PropertyViewTestsPermissions(AccessLevelBaseTestCase): def setUp(self): @@ -2708,3 +2755,4 @@ def test_update_property_view_with_espm(self): # verify that the property has meters too, which came from the XLSX file self.assertEqual(pv.property.meters.count(), 2) + diff --git a/seed/views/v3/organizations.py b/seed/views/v3/organizations.py index 9646ea3971..588472bf58 100644 --- a/seed/views/v3/organizations.py +++ b/seed/views/v3/organizations.py @@ -159,6 +159,14 @@ def _dict_org(request, organizations): "audit_template_sync_enabled": o.audit_template_sync_enabled, "salesforce_enabled": o.salesforce_enabled, "ubid_threshold": o.ubid_threshold, + "unit_options": { + "area_options": o.MEASUREMENT_CHOICES_AREA, + "eui_options": o.MEASUREMENT_CHOICES_EUI, + "ghg_options": o.MEASUREMENT_CHOICES_GHG, + "ghgi_options": o.MEASUREMENT_CHOICES_GHG_INTENSITY, + "wui_options": o.MEASUREMENT_CHOICES_WUI, + "water_use_options": o.MEASUREMENT_CHOICES_WATER_USE, + }, "inventory_count": o.property_set.count() + o.taxlot_set.count(), "access_level_names": o.access_level_names, "public_feed_enabled": o.public_feed_enabled, diff --git a/seed/views/v3/properties.py b/seed/views/v3/properties.py index 4acae95453..5d1cd48535 100644 --- a/seed/views/v3/properties.py +++ b/seed/views/v3/properties.py @@ -90,7 +90,7 @@ from seed.utils.api_schema import AutoSchemaHelper, swagger_auto_schema_org_query_param from seed.utils.inventory_filter import get_filtered_results from seed.utils.labels import get_labels -from seed.utils.match import MergeLinkPairError, match_merge_link +from seed.utils.match import MergeLinkPairError, match_merge_link, get_matching_criteria_column_names from seed.utils.merge import merge_properties from seed.utils.meters import PropertyMeterReadingsExporter from seed.utils.properties import get_changed_fields, pair_unpair_property_taxlot, properties_across_cycles, update_result_with_master @@ -1950,6 +1950,60 @@ def facility_bps_export_to_cts(self, request): return response + @api_endpoint_class + @ajax_request_class + @has_perm_class("requires_member") + @action(detail=False, methods=["POST"]) + def form_create(self, request): + org_id = self.get_organization(request) + data = request.data + access_level_instance = data.get("access_level_instance") + cycle = data.get("cycle") + columns = data.get("form_columns") + matching_columns = get_matching_criteria_column_names(org_id, "PropertyState") + + + state_data = { "organization_id": org_id, "extra_data": {} } + for col in columns: + if not col.get("id"): + Column.objects.create( + is_extra_data=True, + column_name=col["displayName"], + organization_id=org_id, + table_name="PropertyState", + ) + state_data["extra_data"][col["displayName"]] = col["value"] + else: + column_name = col.get("column_name", col.get("displayName")) + state_data[column_name] = col["value"] + + # find matching states + filter_query = {col.get("column_name"): col.get("value") for col in columns if col.get("column_name") in matching_columns} + matching_states = list(PropertyState.objects.filter(**filter_query, propertyview__isnull=False)) + + # always create a new state + state = PropertyState.objects.create(**state_data) + + # create a new property view + if not matching_states: + property = Property.objects.create(organization_id=org_id, access_level_instance_id=access_level_instance) + PropertyView.objects.create(property=property, cycle_id=cycle, state=state) + else: + parent_property = matching_states[0].propertyview_set.first().property + views = parent_property.views.all() + + # out of cycle views + views_out = parent_property.views.exclude(cycle=cycle) + # in cycle + views_in = parent_property.views.filter(cycle=cycle) + + breakpoint() + + # create new extra data columns + # is it new or existing view? + + # breakpoint() + return JsonResponse({"status": "success"}) def _row_from_views(views): data = pd.Series() From 48d1019fa15582fb760c4e71f538a286ea424f92 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Thu, 19 Dec 2024 16:08:15 -0700 Subject: [PATCH 03/14] functional with audit log issues --- .../inventory_create_controller.js | 85 ++++++++++--------- seed/static/seed/js/seed.js | 1 - .../seed/partials/inventory_create.html | 30 +++---- seed/tests/test_property_views.py | 43 +++++----- seed/views/v3/properties.py | 75 +++++++--------- 5 files changed, 109 insertions(+), 125 deletions(-) diff --git a/seed/static/seed/js/controllers/inventory_create_controller.js b/seed/static/seed/js/controllers/inventory_create_controller.js index 40b4db5147..c0055160ad 100644 --- a/seed/static/seed/js/controllers/inventory_create_controller.js +++ b/seed/static/seed/js/controllers/inventory_create_controller.js @@ -12,7 +12,6 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre 'all_columns', 'cycles', 'profiles', - 'organization_payload', // eslint-disable-next-line func-names function ( $scope, @@ -24,9 +23,8 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre all_columns, cycles, profiles, - organization_payload, ) { - $scope.inventory = {}; + $scope.data = { state: { extra_data: {} } }; $scope.inventory_type = $stateParams.inventory_type; $scope.inventory_types = ['Property', 'TaxLot']; const table_name = $scope.inventory_type === 'taxlots' ? 'TaxLotState' : 'PropertyState'; @@ -45,15 +43,15 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre if (!c.is_extra_data && !c.derived_column) $scope.canonical_columns.push(c); } }); - // create a copy, not a reference - $scope.inventory.form_columns =[...$scope.matching_columns]; + // create a copy, not a reference. from_column contents is a list of columns dicts + $scope.form_columns =[...$scope.matching_columns]; + // form_values allows value persistance $scope.form_values = []; - $scope.remove_column = (index) => { - $scope.inventory.form_columns.splice(index, 1) - $scope.form_values[index] = null; - }; - $scope.add_column = () => $scope.inventory.form_columns.push({displayName: '', table_name: table_name}); + $scope.$watch('data', () => { + console.log('watch') + $scope.valid = $scope.data.cycle && $scope.data.access_level_instance && !_.isEqual($scope.data.state, { extra_data: {} }); + }, true) // ACCESS LEVEL TREE @@ -64,49 +62,56 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre })); const access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth($scope.access_level_tree); $scope.change_selected_level_index = () => { - const new_level_instance_depth = parseInt($scope.inventory.level_name_index, 10) + 1; + const new_level_instance_depth = parseInt($scope.data.level_name_index, 10) + 1; $scope.potential_level_instances = access_level_instances_by_depth[new_level_instance_depth]; }; - $scope.inventory.level_name_index = $scope.level_names.at(-1).index; + $scope.data.level_name_index = $scope.level_names.at(-1).index; $scope.change_selected_level_index(); - $scope.inventory.access_level_instance = $scope.potential_level_instances.at(0).id; + $scope.data.access_level_instance = $scope.potential_level_instances.at(0).id; // COLUMN LIST PROFILES $scope.set_columns = (type) => { remove_empty_last_column(); switch (type) { case 'canonical': - $scope.inventory.form_columns = Array.from(new Set([...$scope.inventory.form_columns, ...$scope.canonical_columns])); + $scope.form_columns = Array.from(new Set([...$scope.form_columns, ...$scope.canonical_columns])); break; case 'extra': - $scope.inventory.form_columns = Array.from(new Set([...$scope.inventory.form_columns, ...$scope.extra_columns])); + $scope.form_columns = Array.from(new Set([...$scope.form_columns, ...$scope.extra_columns])); break; default: - $scope.inventory.form_columns = [...$scope.matching_columns]; - $scope.inventory.form_columns = $scope.inventory.form_columns.map(c => ({...c, value: null})); + $scope.form_columns = [...$scope.matching_columns]; + $scope.form_columns = $scope.form_columns.map(c => ({...c, value: null})); $scope.form_values = []; } } + // FORM LOGIC + $scope.remove_column = (column, index) => { + $scope.form_columns.splice(index, 1) + $scope.form_values[index] = null; + set_column_value(column, null); + }; + + $scope.add_column = () => $scope.form_columns.push({ displayName: '', table_name: table_name }); + const remove_empty_last_column = () => { - if (!_.isEmpty($scope.inventory.form_columns) && $scope.inventory.form_columns.at(-1).displayName === '') { - $scope.inventory.form_columns.pop(); + if (!_.isEmpty($scope.form_columns) && $scope.form_columns.at(-1).displayName === '') { + $scope.form_columns.pop(); } } - - // FORM LOGIC $scope.change_profile = () => { const profile_column_names = $scope.profile.columns.map(p => p.column_name); - $scope.inventory.form_columns = $scope.columns.filter(c => profile_column_names.includes(c.column_name)) + $scope.form_columns = $scope.columns.filter(c => profile_column_names.includes(c.column_name)) } - $scope.select_column = (column, idx) => { - column.value = $scope.form_values.at(idx); - $scope.inventory.form_columns[idx] = column; + $scope.select_column = (column, index) => { + column.value = $scope.form_values.at(index); + $scope.form_columns[index] = column; } - $scope.change_column = (displayName, idx) => { + $scope.change_column = (displayName, index) => { const defaults = { table_name: table_name, is_extra_data: true, @@ -116,24 +121,28 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre let column = $scope.columns.find(c => c.displayName === displayName) || {displayName: displayName}; column = {...defaults, ...column}; - column.value = $scope.form_values.at(idx); - $scope.inventory.form_columns[idx] = column; + column.value = $scope.form_values.at(index); + $scope.form_columns[index] = column; }; - $scope.change_value = (value, idx) => { - $scope.form_values[idx] = value; + $scope.change_value = (column, index) => { + $scope.form_values[index] = column.value; + set_column_value(column, column.value) } - $scope.save_inventory = () => { - console.log('save_inventory', $scope.inventory) + const set_column_value = (column, value) => { + const column_name = column.column_name || column.displayName; + if (!column_name) return + const target = column.is_extra_data ? $scope.data.state.extra_data : $scope.data.state; + target[column_name] = value; } - - - - - - + $scope.save_inventory = () => { + console.log($scope.data.state) + inventory_service.create_inventory($scope.data, $scope.inventory_type).then((data) => { + console.log('>>>', data) + }) + } } ]); diff --git a/seed/static/seed/js/seed.js b/seed/static/seed/js/seed.js index 1f07f79071..6535761b12 100644 --- a/seed/static/seed/js/seed.js +++ b/seed/static/seed/js/seed.js @@ -2276,7 +2276,6 @@ return inventory_service.get_column_list_profiles('List View Profile', inventory_type); } ], - organization_payload: ['user_service', 'organization_service', (user_service, organization_service) => organization_service.get_organization(user_service.get_organization().id)] } }) .state({ diff --git a/seed/static/seed/partials/inventory_create.html b/seed/static/seed/partials/inventory_create.html index ea2257e10f..9783c2b8f4 100644 --- a/seed/static/seed/partials/inventory_create.html +++ b/seed/static/seed/partials/inventory_create.html @@ -6,26 +6,16 @@
-
-
+
@@ -34,7 +24,7 @@

{$:: (group_id? 'Group - ': '')$}{$:: (inventory_type === 'taxlots' ? 'Tax L

Create a {$ inventory_type === 'properties' ? 'Property' : 'Tax Lot'$}

- +
@@ -46,13 +36,13 @@

Create a {$ inventory_type === ' + ng-model="data.level_name_index">
+ ng-model="data.access_level_instance">
@@ -60,7 +50,7 @@

Create a {$ inventory_type === ' + ng-model="data.cycle">

@@ -91,7 +81,7 @@

Create a {$ inventory_type === ' - + {$ column.table_name.slice(0, -5) $} @@ -116,13 +106,13 @@

Create a {$ inventory_type === ' aria-label="New column name" /> - + {$ column.is_matching_criteria $} {$ column.is_extra_data $} {$ column.data_type $} - diff --git a/seed/tests/test_property_views.py b/seed/tests/test_property_views.py index 48bdf9bb7b..d7d514f59d 100644 --- a/seed/tests/test_property_views.py +++ b/seed/tests/test_property_views.py @@ -665,45 +665,46 @@ def test_property_form_create(self): original_property_count = Property.objects.count() url = reverse("api:v3:properties-form-create") + f"?organization_id={self.org.pk}" - pm_property_id = Column.objects.get(column_name="pm_property_id", organization=self.org) - pm_property_id = ColumnSerializer(pm_property_id).data - custom_id_1 = Column.objects.get(column_name="custom_id_1", table_name="PropertyState", organization=self.org) - custom_id_1 = ColumnSerializer(custom_id_1).data - extra = {"displayName": "Extra Data Column", "value": "XYZ"} + + state_data = { + "pm_property_id": "123", + "custom_id_1": "ABC", + "extra_data": {"Extra Data Column": "456"}, + } + data = { "access_level_instance": self.org.root.id, "cycle": self.cycle.id, - "form_columns": [ - {**pm_property_id, "value": "123"}, - {**custom_id_1, "value": "ABC"}, - extra - ] + "state": state_data } self.client.post(url, json.dumps(data), content_type="application/json") + # For a new property, counts should only increase by 1 assert PropertyState.objects.count() == original_state_count + 1 assert PropertyView.objects.count() == original_view_count + 1 assert Property.objects.count() == original_property_count + 1 - # # verify with existing matches + + # verify with existing matches cycle2 = self.cycle_factory.get_cycle(start=datetime(2000, 10, 10, tzinfo=get_current_timezone())) - cycle3 = self.cycle_factory.get_cycle(start=datetime(2020, 10, 10, tzinfo=get_current_timezone())) - view = self.property_view_factory.get_property_view(cycle=cycle2, pm_property_id="456", custom_id_1="DEF") + self.property_view_factory.get_property_view(cycle=cycle2, pm_property_id="456", custom_id_1="DEF") + state_data = { + "pm_property_id": "456", + "custom_id_1": "DEF", + "extra_data": {"Extra Data Column": "GHI"}, + } data = { "access_level_instance": self.org.root.id, "cycle": self.cycle.id, - "form_columns": [ - {**pm_property_id, "value": "456"}, - {**custom_id_1, "value": "DEF"}, - {"displayName": "Column2", "value": "GHI"} - ] + "state": state_data } self.client.post(url, json.dumps(data), content_type="application/json") - - - assert True + # For property merges, counts should increase by 2 (new property, and merged property) + assert PropertyState.objects.count() == original_state_count + 3 + assert PropertyView.objects.count() == original_view_count + 3 + assert Property.objects.count() == original_property_count + 3 class PropertyViewTestsPermissions(AccessLevelBaseTestCase): diff --git a/seed/views/v3/properties.py b/seed/views/v3/properties.py index 5d1cd48535..53149241ac 100644 --- a/seed/views/v3/properties.py +++ b/seed/views/v3/properties.py @@ -1194,7 +1194,7 @@ def update(self, request, pk=None): log = PropertyAuditLog.objects.select_related().filter(state=property_view.state).order_by("-id").first() - if log.name in {"Manual Edit", "Manual Match", "System Match", "Merge current state in migration"}: + if log.name in {"Manual Edit", "Manual Match", "System Match", "Merge current state in migration", "Form Creation"}: # Convert this to using the serializer to save the data. This will override the previous values # in the state object. @@ -1957,55 +1957,40 @@ def facility_bps_export_to_cts(self, request): def form_create(self, request): org_id = self.get_organization(request) data = request.data - access_level_instance = data.get("access_level_instance") - cycle = data.get("cycle") - columns = data.get("form_columns") + access_level_instance_id = data.get("access_level_instance") + cycle_id = data.get("cycle") + new_state_data = data.get("state") + + # Create extra data columns if necessary + extra_data = new_state_data.get("extra_data", {}) + for column in extra_data: + Column.objects.get_or_create( + is_extra_data=True, + column_name=column, + organization_id=org_id, + table_name="PropertyState", + ) + + # Create stub state  + state = PropertyState.objects.create(organization_id=org_id) + # get_or_create existing property and view matching_columns = get_matching_criteria_column_names(org_id, "PropertyState") - - - state_data = { "organization_id": org_id, "extra_data": {} } - for col in columns: - if not col.get("id"): - Column.objects.create( - is_extra_data=True, - column_name=col["displayName"], - organization_id=org_id, - table_name="PropertyState", - ) - state_data["extra_data"][col["displayName"]] = col["value"] - else: - column_name = col.get("column_name", col.get("displayName")) - state_data[column_name] = col["value"] - - # find matching states - filter_query = {col.get("column_name"): col.get("value") for col in columns if col.get("column_name") in matching_columns} - matching_states = list(PropertyState.objects.filter(**filter_query, propertyview__isnull=False)) - - # always create a new state - state = PropertyState.objects.create(**state_data) - - # create a new property view - if not matching_states: - property = Property.objects.create(organization_id=org_id, access_level_instance_id=access_level_instance) - PropertyView.objects.create(property=property, cycle_id=cycle, state=state) + filter_query = {col: new_state_data.get(col) for col in matching_columns if col in new_state_data} + matching_state = PropertyState.objects.filter(**filter_query, organization=org_id, propertyview__cycle=cycle_id).first() + if matching_state: + view = matching_state.propertyview_set.first() + property = view.property else: - parent_property = matching_states[0].propertyview_set.first().property - views = parent_property.views.all() + property = Property.objects.create(organization_id=org_id, access_level_instance_id=access_level_instance_id) + view = PropertyView.objects.create(cycle_id=cycle_id, property=property, state=state) - # out of cycle views - views_out = parent_property.views.exclude(cycle=cycle) - # in cycle - views_in = parent_property.views.filter(cycle=cycle) - - breakpoint() - - # create new extra data columns - # is it new or existing view? - - # breakpoint() - return JsonResponse({"status": "success"}) + PropertyAuditLog.objects.create(organization_id=org_id, state=state, view=view, name="Form Creation") + # Use existing view endpoint to ensure matching, merging, and linking. + return self.update(request, pk=view.id) def _row_from_views(views): + + data = pd.Series() def mode(field, extra_data=False): From 6c4b6f580d11342fd51d8968b7e749c653369f9a Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Fri, 20 Dec 2024 12:43:23 -0700 Subject: [PATCH 04/14] modal actions --- .../inventory_create_controller.js | 312 ++++++++++-------- seed/static/seed/js/seed.js | 2 +- .../seed/js/services/inventory_service.js | 12 +- .../seed/partials/inventory_create.html | 219 ++++++------ seed/static/seed/scss/style.scss | 1 + seed/tests/test_property_views.py | 17 +- seed/views/v3/properties.py | 9 +- 7 files changed, 297 insertions(+), 275 deletions(-) diff --git a/seed/static/seed/js/controllers/inventory_create_controller.js b/seed/static/seed/js/controllers/inventory_create_controller.js index c0055160ad..967a6c231c 100644 --- a/seed/static/seed/js/controllers/inventory_create_controller.js +++ b/seed/static/seed/js/controllers/inventory_create_controller.js @@ -3,146 +3,176 @@ * See also https://github.com/SEED-platform/seed/blob/main/LICENSE.md */ angular.module('SEED.controller.inventory_create', []).controller('inventory_create_controller', [ - '$scope', - '$window', - '$stateParams', - 'ah_service', - 'inventory_service', - 'access_level_tree', - 'all_columns', - 'cycles', - 'profiles', - // eslint-disable-next-line func-names - function ( - $scope, - $window, - $stateParams, - ah_service, - inventory_service, - access_level_tree, - all_columns, - cycles, - profiles, - ) { - $scope.data = { state: { extra_data: {} } }; - $scope.inventory_type = $stateParams.inventory_type; - $scope.inventory_types = ['Property', 'TaxLot']; - const table_name = $scope.inventory_type === 'taxlots' ? 'TaxLotState' : 'PropertyState'; - $scope.cycles = cycles.cycles; - $scope.profiles = profiles; - $scope.profile = []; - $scope.columns = all_columns; - - $scope.matching_columns = []; - $scope.extra_columns = []; - $scope.canonical_columns = []; - $scope.columns.forEach((c) => { - if (c.table_name == table_name) { - if (c.is_matching_criteria) $scope.matching_columns.push(c); - if (c.is_extra_data) $scope.extra_columns.push(c); - if (!c.is_extra_data && !c.derived_column) $scope.canonical_columns.push(c); - } + '$scope', + '$state', + '$stateParams', + 'ah_service', + 'inventory_service', + 'Notification', + 'simple_modal_service', + 'spinner_utility', + 'access_level_tree', + 'all_columns', + 'cycles', + 'profiles', + // eslint-disable-next-line func-names + function ( + $scope, + $state, + $stateParams, + ah_service, + inventory_service, + Notification, + simple_modal_service, + spinner_utility, + access_level_tree, + all_columns, + cycles, + profiles + ) { + $scope.data = { state: { extra_data: {} } }; + $scope.inventory_type = $stateParams.inventory_type; + $scope.inventory_types = ['Property', 'TaxLot']; + const table_name = $scope.inventory_type === 'taxlots' ? 'TaxLotState' : 'PropertyState'; + $scope.cycles = cycles.cycles; + $scope.profiles = profiles; + $scope.profile = []; + $scope.columns = all_columns; + + $scope.matching_columns = []; + $scope.extra_columns = []; + $scope.canonical_columns = []; + $scope.columns.forEach((c) => { + if (c.table_name === table_name) { + if (c.is_matching_criteria) $scope.matching_columns.push(c); + if (c.is_extra_data) $scope.extra_columns.push(c); + if (!c.is_extra_data && !c.derived_column) $scope.canonical_columns.push(c); + } + }); + // create a copy, not a reference. from_column contents is a list of columns dicts + $scope.form_columns = [...$scope.matching_columns]; + // form_values allows value persistance + $scope.form_values = []; + + $scope.$watch('data', () => { + $scope.valid = $scope.data.cycle && $scope.data.access_level_instance && !_.isEqual($scope.data.state, { extra_data: {} }); + }, true); + + // ACCESS LEVEL TREE + $scope.access_level_tree = access_level_tree.access_level_tree; + $scope.level_names = access_level_tree.access_level_names.map((level, i) => ({ + index: i, + name: level + })); + const access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth($scope.access_level_tree); + $scope.change_selected_level_index = () => { + const new_level_instance_depth = parseInt($scope.data.level_name_index, 10) + 1; + $scope.potential_level_instances = access_level_instances_by_depth[new_level_instance_depth]; + }; + $scope.data.level_name_index = $scope.level_names.at(-1).index; + $scope.change_selected_level_index(); + $scope.data.access_level_instance = $scope.potential_level_instances.at(0).id; + + // COLUMN LIST PROFILES + $scope.set_columns = (type) => { + remove_empty_last_column(); + switch (type) { + case 'canonical': + $scope.form_columns = Array.from(new Set([...$scope.form_columns, ...$scope.canonical_columns])); + break; + case 'extra': + $scope.form_columns = Array.from(new Set([...$scope.form_columns, ...$scope.extra_columns])); + break; + default: + $scope.form_columns = [...$scope.matching_columns]; + $scope.form_columns = $scope.form_columns.map((c) => ({ ...c, value: null })); + $scope.form_values = []; + } + }; + + // FORM LOGIC + $scope.remove_column = (column, index) => { + $scope.form_columns.splice(index, 1); + $scope.form_values[index] = null; + set_column_value(column, null); + }; + + $scope.add_column = () => $scope.form_columns.push({ displayName: '', table_name }); + + const remove_empty_last_column = () => { + if (!_.isEmpty($scope.form_columns) && $scope.form_columns.at(-1).displayName === '') { + $scope.form_columns.pop(); + } + }; + + $scope.change_profile = () => { + const profile_column_names = $scope.profile.columns.map((p) => p.column_name); + $scope.form_columns = $scope.columns.filter((c) => profile_column_names.includes(c.column_name)); + }; + + $scope.select_column = (column, index) => { + column.value = $scope.form_values.at(index); + $scope.form_columns[index] = column; + }; + + $scope.change_column = (displayName, index) => { + const defaults = { + table_name, + is_extra_data: true, + is_matching_criteria: false, + data_type: 'string' + }; + let column = $scope.columns.find((c) => c.displayName === displayName) || { displayName }; + column = { ...defaults, ...column }; + + column.value = $scope.form_values.at(index); + $scope.form_columns[index] = column; + }; + + $scope.change_value = (column, index) => { + $scope.form_values[index] = column.value; + set_column_value(column, column.value); + }; + + const set_column_value = (column, value) => { + const column_name = column.column_name || column.displayName; + if (!column_name) return; + const target = column.is_extra_data ? $scope.data.state.extra_data : $scope.data.state; + target[column_name] = value; + }; + + $scope.save_inventory = () => { + const type_name = $scope.inventory_type === 'taxlots' ? 'Tax Lot' : 'Property'; + const cycle_name = $scope.cycles.find((c) => c.id === $scope.data.cycle).name; + const ali_name = $scope.access_level_tree.find((ali) => ali.id === $scope.data.access_level_instance).name; + const modalOptions = { + type: 'default', + okButtonText: 'Confirm', + headerText: `Create new ${type_name}`, + bodyText: `Create ${ali_name} ${type_name} in Cycle ${cycle_name}?` + }; + const successOptions = { + type: 'default', + okButtonText: `View ${type_name}`, + headerText: 'Success', + bodyText: `Successfully created ${type_name}` + }; + simple_modal_service.showModal(modalOptions).then(() => { + spinner_utility.show(); + inventory_service.create_inventory($scope.data, $scope.inventory_type).then((response) => { + Notification.success(`Successfully created ${type_name}`); + spinner_utility.hide(); + return response.data.view_id; + }).then((view_id) => { + simple_modal_service.showModal(successOptions).then(() => { + window.location.href = `/app/#/${$scope.inventory_type}/${view_id}`; + }).catch(() => { + $state.reload(); + }); + }).catch(() => { + Notification.error(`Failed to create ${type_name}`); }); - // create a copy, not a reference. from_column contents is a list of columns dicts - $scope.form_columns =[...$scope.matching_columns]; - // form_values allows value persistance - $scope.form_values = []; - - $scope.$watch('data', () => { - console.log('watch') - $scope.valid = $scope.data.cycle && $scope.data.access_level_instance && !_.isEqual($scope.data.state, { extra_data: {} }); - }, true) - - - // ACCESS LEVEL TREE - $scope.access_level_tree = access_level_tree.access_level_tree; - $scope.level_names = access_level_tree.access_level_names.map((level, i) => ({ - index: i, - name: level - })); - const access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth($scope.access_level_tree); - $scope.change_selected_level_index = () => { - const new_level_instance_depth = parseInt($scope.data.level_name_index, 10) + 1; - $scope.potential_level_instances = access_level_instances_by_depth[new_level_instance_depth]; - }; - $scope.data.level_name_index = $scope.level_names.at(-1).index; - $scope.change_selected_level_index(); - $scope.data.access_level_instance = $scope.potential_level_instances.at(0).id; - - // COLUMN LIST PROFILES - $scope.set_columns = (type) => { - remove_empty_last_column(); - switch (type) { - case 'canonical': - $scope.form_columns = Array.from(new Set([...$scope.form_columns, ...$scope.canonical_columns])); - break; - case 'extra': - $scope.form_columns = Array.from(new Set([...$scope.form_columns, ...$scope.extra_columns])); - break; - default: - $scope.form_columns = [...$scope.matching_columns]; - $scope.form_columns = $scope.form_columns.map(c => ({...c, value: null})); - $scope.form_values = []; - } - } - - // FORM LOGIC - $scope.remove_column = (column, index) => { - $scope.form_columns.splice(index, 1) - $scope.form_values[index] = null; - set_column_value(column, null); - }; - - $scope.add_column = () => $scope.form_columns.push({ displayName: '', table_name: table_name }); - - const remove_empty_last_column = () => { - if (!_.isEmpty($scope.form_columns) && $scope.form_columns.at(-1).displayName === '') { - $scope.form_columns.pop(); - } - } - - $scope.change_profile = () => { - const profile_column_names = $scope.profile.columns.map(p => p.column_name); - $scope.form_columns = $scope.columns.filter(c => profile_column_names.includes(c.column_name)) - } - - $scope.select_column = (column, index) => { - column.value = $scope.form_values.at(index); - $scope.form_columns[index] = column; - } - - $scope.change_column = (displayName, index) => { - const defaults = { - table_name: table_name, - is_extra_data: true, - is_matching_criteria: false, - data_type: 'string', - } - let column = $scope.columns.find(c => c.displayName === displayName) || {displayName: displayName}; - column = {...defaults, ...column}; - - column.value = $scope.form_values.at(index); - $scope.form_columns[index] = column; - }; - - $scope.change_value = (column, index) => { - $scope.form_values[index] = column.value; - set_column_value(column, column.value) - } - - const set_column_value = (column, value) => { - const column_name = column.column_name || column.displayName; - if (!column_name) return - const target = column.is_extra_data ? $scope.data.state.extra_data : $scope.data.state; - target[column_name] = value; - } - - $scope.save_inventory = () => { - console.log($scope.data.state) - inventory_service.create_inventory($scope.data, $scope.inventory_type).then((data) => { - console.log('>>>', data) - }) - } - - } + }); + }; + } ]); diff --git a/seed/static/seed/js/seed.js b/seed/static/seed/js/seed.js index 6535761b12..ab0c49e16a 100644 --- a/seed/static/seed/js/seed.js +++ b/seed/static/seed/js/seed.js @@ -2275,7 +2275,7 @@ const inventory_type = $stateParams.inventory_type === 'properties' ? 'Property' : 'Tax Lot'; return inventory_service.get_column_list_profiles('List View Profile', inventory_type); } - ], + ] } }) .state({ diff --git a/seed/static/seed/js/services/inventory_service.js b/seed/static/seed/js/services/inventory_service.js index a4fadbc95f..f683f461a5 100644 --- a/seed/static/seed/js/services/inventory_service.js +++ b/seed/static/seed/js/services/inventory_service.js @@ -1294,13 +1294,11 @@ angular.module('SEED.service.inventory', []).factory('inventory_service', [ taxlot_view_ids }).then((response) => response.data); - inventory_service.create_inventory = (data, inventory_type) => { - return $http.post(`/api/v3/${inventory_type}/form_create/`, data, { - params: { - organization_id: user_service.get_organization().id - } - }); - } + inventory_service.create_inventory = (data, inventory_type) => $http.post(`/api/v3/${inventory_type}/form_create/`, data, { + params: { + organization_id: user_service.get_organization().id + } + }); return inventory_service; } diff --git a/seed/static/seed/partials/inventory_create.html b/seed/static/seed/partials/inventory_create.html index 9783c2b8f4..d82f180398 100644 --- a/seed/static/seed/partials/inventory_create.html +++ b/seed/static/seed/partials/inventory_create.html @@ -5,121 +5,126 @@
-
-
+
-
-
-

Create a {$ inventory_type === 'properties' ? 'Property' : 'Tax Lot'$}

-
-
- -
+
+
+

Create a {$ inventory_type === 'properties' ? 'Property' : 'Tax Lot'$}

+
+ +
+
-
-
-
-
- - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
Inventory TypeColumnValueMatching CriteriaExtra DataData Type
{$ column.table_name.slice(0, -5) $} - - - - {$ column.is_matching_criteria $}{$ column.is_extra_data $}{$ column.data_type $} - -
- -
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
-
\ No newline at end of file +
+
+ + +
+
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Inventory TypeColumnValueMatching CriteriaExtra DataData Type
{$ column.table_name.slice(0, -5) $} + + + + {$ column.is_matching_criteria $}{$ column.is_extra_data $}{$ column.data_type $} + +
+ +
+
+
diff --git a/seed/static/seed/scss/style.scss b/seed/static/seed/scss/style.scss index 328e68e9a8..bf83e93021 100755 --- a/seed/static/seed/scss/style.scss +++ b/seed/static/seed/scss/style.scss @@ -1668,6 +1668,7 @@ a:not([href]) { border-collapse: separate; border-spacing: 5px; } + .content-row { display: flex; margin: 10px 0; diff --git a/seed/tests/test_property_views.py b/seed/tests/test_property_views.py index d7d514f59d..4302595cb2 100644 --- a/seed/tests/test_property_views.py +++ b/seed/tests/test_property_views.py @@ -47,7 +47,6 @@ TaxLotProperty, TaxLotView, ) -from seed.serializers.columns import ColumnSerializer from seed.serializers.properties import PropertyStatePromoteWritableSerializer, PropertyStateSerializer from seed.test_helpers.fake import ( FakeColumnFactory, @@ -672,11 +671,7 @@ def test_property_form_create(self): "extra_data": {"Extra Data Column": "456"}, } - data = { - "access_level_instance": self.org.root.id, - "cycle": self.cycle.id, - "state": state_data - } + data = {"access_level_instance": self.org.root.id, "cycle": self.cycle.id, "state": state_data} self.client.post(url, json.dumps(data), content_type="application/json") # For a new property, counts should only increase by 1 @@ -684,8 +679,7 @@ def test_property_form_create(self): assert PropertyView.objects.count() == original_view_count + 1 assert Property.objects.count() == original_property_count + 1 - - # verify with existing matches + # verify with existing matches cycle2 = self.cycle_factory.get_cycle(start=datetime(2000, 10, 10, tzinfo=get_current_timezone())) self.property_view_factory.get_property_view(cycle=cycle2, pm_property_id="456", custom_id_1="DEF") @@ -694,11 +688,7 @@ def test_property_form_create(self): "custom_id_1": "DEF", "extra_data": {"Extra Data Column": "GHI"}, } - data = { - "access_level_instance": self.org.root.id, - "cycle": self.cycle.id, - "state": state_data - } + data = {"access_level_instance": self.org.root.id, "cycle": self.cycle.id, "state": state_data} self.client.post(url, json.dumps(data), content_type="application/json") # For property merges, counts should increase by 2 (new property, and merged property) @@ -2756,4 +2746,3 @@ def test_update_property_view_with_espm(self): # verify that the property has meters too, which came from the XLSX file self.assertEqual(pv.property.meters.count(), 2) - diff --git a/seed/views/v3/properties.py b/seed/views/v3/properties.py index 53149241ac..3bd2a9c323 100644 --- a/seed/views/v3/properties.py +++ b/seed/views/v3/properties.py @@ -90,7 +90,7 @@ from seed.utils.api_schema import AutoSchemaHelper, swagger_auto_schema_org_query_param from seed.utils.inventory_filter import get_filtered_results from seed.utils.labels import get_labels -from seed.utils.match import MergeLinkPairError, match_merge_link, get_matching_criteria_column_names +from seed.utils.match import MergeLinkPairError, get_matching_criteria_column_names, match_merge_link from seed.utils.merge import merge_properties from seed.utils.meters import PropertyMeterReadingsExporter from seed.utils.properties import get_changed_fields, pair_unpair_property_taxlot, properties_across_cycles, update_result_with_master @@ -1970,8 +1970,8 @@ def form_create(self, request): organization_id=org_id, table_name="PropertyState", ) - - # Create stub state  + + # Create stub state state = PropertyState.objects.create(organization_id=org_id) # get_or_create existing property and view matching_columns = get_matching_criteria_column_names(org_id, "PropertyState") @@ -1988,9 +1988,8 @@ def form_create(self, request): # Use existing view endpoint to ensure matching, merging, and linking. return self.update(request, pk=view.id) -def _row_from_views(views): - +def _row_from_views(views): data = pd.Series() def mode(field, extra_data=False): From ad478bd9c7889214e48dd98b1771bc7d58ced2fd Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Fri, 20 Dec 2024 13:20:31 -0700 Subject: [PATCH 05/14] styles remove units --- seed/static/seed/partials/inventory_create.html | 4 ++-- seed/views/v3/organizations.py | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/seed/static/seed/partials/inventory_create.html b/seed/static/seed/partials/inventory_create.html index d82f180398..c6764283a2 100644 --- a/seed/static/seed/partials/inventory_create.html +++ b/seed/static/seed/partials/inventory_create.html @@ -59,7 +59,7 @@

Create a {$ inventory_type === '

-
+
@@ -70,7 +70,7 @@

Create a {$ inventory_type === '

-
+
diff --git a/seed/views/v3/organizations.py b/seed/views/v3/organizations.py index 588472bf58..9646ea3971 100644 --- a/seed/views/v3/organizations.py +++ b/seed/views/v3/organizations.py @@ -159,14 +159,6 @@ def _dict_org(request, organizations): "audit_template_sync_enabled": o.audit_template_sync_enabled, "salesforce_enabled": o.salesforce_enabled, "ubid_threshold": o.ubid_threshold, - "unit_options": { - "area_options": o.MEASUREMENT_CHOICES_AREA, - "eui_options": o.MEASUREMENT_CHOICES_EUI, - "ghg_options": o.MEASUREMENT_CHOICES_GHG, - "ghgi_options": o.MEASUREMENT_CHOICES_GHG_INTENSITY, - "wui_options": o.MEASUREMENT_CHOICES_WUI, - "water_use_options": o.MEASUREMENT_CHOICES_WATER_USE, - }, "inventory_count": o.property_set.count() + o.taxlot_set.count(), "access_level_names": o.access_level_names, "public_feed_enabled": o.public_feed_enabled, From 99b4ffc2c88e829ed7aa036a73f46f04788580c4 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Mon, 30 Dec 2024 13:41:42 -0700 Subject: [PATCH 06/14] check duplicates and matching refactors refactors --- .../inventory_create_controller.js | 41 +++++++++++++++++-- .../seed/partials/inventory_create.html | 13 +++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/seed/static/seed/js/controllers/inventory_create_controller.js b/seed/static/seed/js/controllers/inventory_create_controller.js index 967a6c231c..284307ec91 100644 --- a/seed/static/seed/js/controllers/inventory_create_controller.js +++ b/seed/static/seed/js/controllers/inventory_create_controller.js @@ -30,6 +30,7 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre cycles, profiles ) { + // INIT $scope.data = { state: { extra_data: {} } }; $scope.inventory_type = $stateParams.inventory_type; $scope.inventory_types = ['Property', 'TaxLot']; @@ -38,6 +39,7 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre $scope.profiles = profiles; $scope.profile = []; $scope.columns = all_columns; + $scope.form_errors = [] $scope.matching_columns = []; $scope.extra_columns = []; @@ -54,10 +56,41 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre // form_values allows value persistance $scope.form_values = []; + // DATA VALIDATION $scope.$watch('data', () => { $scope.valid = $scope.data.cycle && $scope.data.access_level_instance && !_.isEqual($scope.data.state, { extra_data: {} }); + check_form_errors(); }, true); + $scope.$watch('form_columns', () => { + check_form_errors(); + }, true); + + const check_form_errors = () => { + $scope.form_errors = []; + if (!$scope.data.cycle) $scope.form_errors.push('Cycle is required'); + check_duplicates(); + check_matching_criteria(); + }; + + const check_duplicates = () => { + const display_name_counts = {}; + let has_duplicates = false; + $scope.form_columns.forEach((col) => { + display_name_counts[col.displayName] = (display_name_counts[col.displayName] || 0) + 1; + }); + $scope.form_columns.forEach(col => { + col.is_duplicate = display_name_counts[col.displayName] > 1; + if (col.is_duplicate) has_duplicates = true + }); + has_duplicates && $scope.form_errors.push('Duplicate columns are not allowed'); + }; + + const check_matching_criteria = () => { + const error = !$scope.form_columns.some(c => c.is_matching_criteria && c.value !== undefined) + if (error) $scope.form_errors.push('At least one matching criteria must have a value'); + } + // ACCESS LEVEL TREE $scope.access_level_tree = access_level_tree.access_level_tree; $scope.level_names = access_level_tree.access_level_names.map((level, i) => ({ @@ -85,16 +118,17 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre break; default: $scope.form_columns = [...$scope.matching_columns]; - $scope.form_columns = $scope.form_columns.map((c) => ({ ...c, value: null })); + $scope.form_columns = $scope.form_columns.map((c) => ({ ...c, value: 'null', is_duplicate: false })); $scope.form_values = []; } }; // FORM LOGIC - $scope.remove_column = (column, index) => { + $scope.remove_column = (index, column) => { $scope.form_columns.splice(index, 1); $scope.form_values[index] = null; set_column_value(column, null); + check_form_errors(); }; $scope.add_column = () => $scope.form_columns.push({ displayName: '', table_name }); @@ -111,6 +145,8 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre }; $scope.select_column = (column, index) => { + // preserve value if column is duplicate + if ($scope.form_columns.includes(column)) return; column.value = $scope.form_values.at(index); $scope.form_columns[index] = column; }; @@ -124,7 +160,6 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre }; let column = $scope.columns.find((c) => c.displayName === displayName) || { displayName }; column = { ...defaults, ...column }; - column.value = $scope.form_values.at(index); $scope.form_columns[index] = column; }; diff --git a/seed/static/seed/partials/inventory_create.html b/seed/static/seed/partials/inventory_create.html index c6764283a2..2439f32416 100644 --- a/seed/static/seed/partials/inventory_create.html +++ b/seed/static/seed/partials/inventory_create.html @@ -24,13 +24,16 @@

{$:: (inventory_type === 'taxlots' ? 'Tax Lots' : 'Properties') | translate

Create a {$ inventory_type === 'properties' ? 'Property' : 'Tax Lot'$}

- +
+
+
  • {$ message $}
+
@@ -70,7 +73,7 @@

Create a {$ inventory_type === '

-
+
@@ -84,7 +87,7 @@

Create a {$ inventory_type === '

- + - - + + - +
{$ column.table_name.slice(0, -5) $} @@ -113,8 +116,8 @@

Create a {$ inventory_type === '

{$ column.is_matching_criteria $}{$ column.is_extra_data $} {$ column.data_type $} {$ column.data_type $} diff --git a/seed/tests/test_property_views.py b/seed/tests/test_property_views.py index ff237147bf..07946455ba 100644 --- a/seed/tests/test_property_views.py +++ b/seed/tests/test_property_views.py @@ -662,23 +662,24 @@ def test_property_form_create(self): original_state_count = PropertyState.objects.count() original_view_count = PropertyView.objects.count() original_property_count = Property.objects.count() + original_taxlot_view_count = TaxLotView.objects.count() url = reverse("api:v3:properties-form-create") + f"?organization_id={self.org.pk}" state_data = { "state": { - "pm_property_id": "123", - "custom_id_1": "ABC", - "extra_data": {"Extra Data Column": "456"}, + "pm_property_id": "1", + "custom_id_1": "2", + "extra_data": {"Extra Data Column": "3"}, }, - "taxlot_state": {} + "taxlot_state": {}, } data = {"access_level_instance": self.org.root.id, "cycle": self.cycle.id, **state_data} response = self.client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 200 - assert response.json().get('view_id') + assert response.json().get("view_id") # For a new property, counts should only increase by 1 assert PropertyState.objects.count() == original_state_count + 1 assert PropertyView.objects.count() == original_view_count + 1 @@ -686,15 +687,15 @@ def test_property_form_create(self): # verify with existing matches cycle2 = self.cycle_factory.get_cycle(start=datetime(2000, 10, 10, tzinfo=get_current_timezone())) - self.property_view_factory.get_property_view(cycle=cycle2, pm_property_id="456", custom_id_1="DEF") + self.property_view_factory.get_property_view(cycle=cycle2, pm_property_id="4", custom_id_1="5") state_data = { "state": { - "pm_property_id": "456", - "custom_id_1": "DEF", - "extra_data": {"Extra Data Column": "GHI"}, + "pm_property_id": "4", + "custom_id_1": "5", + "extra_data": {"Extra Data Column": "6"}, }, - "taxlot_state": {} + "taxlot_state": {}, } data = {"access_level_instance": self.org.root.id, "cycle": self.cycle.id, **state_data} @@ -704,15 +705,38 @@ def test_property_form_create(self): assert PropertyView.objects.count() == original_view_count + 3 assert Property.objects.count() == original_property_count + 3 - # # RP - how to pass taxlot data? - # state_data = { - # "pm_property_id": "888", - # "jursidiction_tax_lot_id": "999", - # "extra_data": {"Extra Data Column": "GHI"}, - # } - # data = {"access_level_instance": self.org.root.id, "cycle": self.cycle.id, "state": state_data} - # self.client.post(url, json.dumps(data), content_type="application/json") - # breakpoint() + # property associated with a taxlot + property_data = { + "acess_level_instance": self.org.root.id, + "cycle": self.cycle.id, + "state": {"pm_property_id": "7", "custom_id_1": "8", "extra_data": {"Extra Data Column": "9"}, "lot_number": "10"}, + } + taxlot_data = { + "acess_level_instance": self.org.root.id, + "cycle": self.cycle.id, + "state": {"jurisdiction_tax_lot_id": "10", "address_line_1": "A"}, + } + + p_response = self.client.post(url, json.dumps(property_data), content_type="application/json") + assert p_response.status_code == 200 + view_id = p_response.json().get("view_id") + taxlot_url = reverse("api:v3:taxlots-form-create") + f"?organization_id={self.org.pk}&related_view_id={view_id}" + t_response = self.client.post(taxlot_url, json.dumps(taxlot_data), content_type="application/json") + assert t_response.status_code == 200 + + property_view = PropertyView.objects.get(pk=view_id) + property_state = property_view.state + taxlot_view = property_view.taxlotproperty_set.first().taxlot_view + taxlot_state = taxlot_view.state + assert property_state.pm_property_id == "7" + assert property_state.lot_number == "10" + assert taxlot_state.jurisdiction_tax_lot_id == "10" + assert taxlot_state.address_line_1 == "A" + + assert PropertyState.objects.count() == original_state_count + 4 + assert PropertyView.objects.count() == original_view_count + 4 + assert Property.objects.count() == original_property_count + 4 + assert TaxLotView.objects.count() == original_taxlot_view_count + 1 class PropertyViewTestsPermissions(AccessLevelBaseTestCase): diff --git a/seed/tests/test_taxlot_views.py b/seed/tests/test_taxlot_views.py index 127d748b53..204cd157ea 100644 --- a/seed/tests/test_taxlot_views.py +++ b/seed/tests/test_taxlot_views.py @@ -11,7 +11,18 @@ from seed.data_importer.tasks import geocode_and_match_buildings_task from seed.landing.models import SEEDUser as User -from seed.models import DATA_STATE_MAPPING, VIEW_LIST_TAXLOT, Column, Note, PropertyView, StatusLabel, TaxLot, TaxLotProperty, TaxLotState, TaxLotView +from seed.models import ( + DATA_STATE_MAPPING, + VIEW_LIST_TAXLOT, + Column, + Note, + PropertyView, + StatusLabel, + TaxLot, + TaxLotProperty, + TaxLotState, + TaxLotView, +) from seed.test_helpers.fake import ( FakeColumnListProfileFactory, FakeCycleFactory, @@ -334,28 +345,60 @@ def test_taxlots_cycles_list(self): def test_taxlot_form_create(self): original_state_count = TaxLotState.objects.count() original_view_count = TaxLotView.objects.count() - original_property_count = TaxLot.objects.count() + original_taxlot_count = TaxLot.objects.count() + original_property_view_count = PropertyView.objects.count() url = reverse("api:v3:taxlots-form-create") + f"?organization_id={self.org.pk}" state_data = { - "jurisdiction_tax_lot_id": "123", - "custom_id_1": "ABC", - "extra_data": {"Extra Data Column": "456"}, + "jurisdiction_tax_lot_id": "1", + "custom_id_1": "2", + "extra_data": {"Extra Data Column": "3"}, } data = {"access_level_instance": self.org.root.id, "cycle": self.cycle.id, "state": state_data} response = self.client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 200 - assert response.json().get('view_id') + assert response.json().get("view_id") # For a new property, counts should only increase by 1 assert TaxLotState.objects.count() == original_state_count + 1 assert TaxLotView.objects.count() == original_view_count + 1 - assert TaxLot.objects.count() == original_property_count + 1 + assert TaxLot.objects.count() == original_taxlot_count + 1 + # taxlot associated with a property + taxlot_data = { + "acess_level_instance": self.org.root.id, + "cycle": self.cycle.id, + "state": {"jurisdiction_tax_lot_id": "10", "address_line_1": "A"}, + } + property_data = { + "acess_level_instance": self.org.root.id, + "cycle": self.cycle.id, + "state": {"pm_property_id": "7", "custom_id_1": "8", "extra_data": {"Extra Data Column": "9"}, "lot_number": "10"}, + } + t_response = self.client.post(url, json.dumps(taxlot_data), content_type="application/json") + assert t_response.status_code == 200 + view_id = t_response.json().get("view_id") + property_url = reverse("api:v3:properties-form-create") + f"?organization_id={self.org.pk}&related_view_id={view_id}" + p_response = self.client.post(property_url, json.dumps(property_data), content_type="application/json") + assert p_response.status_code == 200 + + taxlot_view = TaxLotView.objects.get(pk=view_id) + taxlot_state = taxlot_view.state + property_view = taxlot_view.taxlotproperty_set.first().property_view + property_state = property_view.state + assert property_state.pm_property_id == "7" + assert property_state.lot_number == "10" + assert taxlot_state.jurisdiction_tax_lot_id == "10" + assert taxlot_state.address_line_1 == "A" + + assert TaxLotState.objects.count() == original_state_count + 2 + assert TaxLotView.objects.count() == original_view_count + 2 + assert TaxLot.objects.count() == original_taxlot_count + 2 + assert PropertyView.objects.count() == original_property_view_count + 1 class TaxLotViewTestPermissions(AccessLevelBaseTestCase): diff --git a/seed/utils/inventory.py b/seed/utils/inventory.py new file mode 100644 index 0000000000..f2c8845cdf --- /dev/null +++ b/seed/utils/inventory.py @@ -0,0 +1,42 @@ +from seed.models import Column, Property, PropertyAuditLog, PropertyState, PropertyView, TaxLot, TaxLotAuditLog, TaxLotState, TaxLotView +from seed.utils.match import get_matching_criteria_column_names + + +def create_inventory(inventory_type, org_id, cycle_id, access_level_instance_id, new_state_data, related=False): + inventory_class, view_class, state_class, auditlog_class, cycle_query, viewset = _inventorty_config(inventory_type) + + # Create extra data columns if necessary + extra_data = new_state_data.get("extra_data", {}) + for column in extra_data: + Column.objects.get_or_create(is_extra_data=True, column_name=column, organization_id=org_id, table_name=state_class.__name__) + + # Create stub state + if related: + state = state_class.objects.create(organization_id=org_id, **new_state_data) + else: + state = state_class.objects.create(organization_id=org_id) + # get_or_create existing inventory and view + matching_columns = get_matching_criteria_column_names(org_id, state_class.__name__) + filter_query = {col: new_state_data.get(col) for col in matching_columns if col in new_state_data} + filter_query.update({"organization": org_id, cycle_query: cycle_id, f"{inventory_type}view__isnull": False}) + matching_state = state_class.objects.filter(**filter_query).first() + if matching_state: + view = getattr(matching_state, viewset).first() + inventory = getattr(view, inventory_type) + else: + inventory = inventory_class.objects.create(organization_id=org_id, access_level_instance_id=access_level_instance_id) + view_data = {"cycle_id": cycle_id, inventory_type: inventory, "state": state} + view = view_class.objects.create(**view_data) + + auditlog_class.objects.create(organization_id=org_id, state=state, view=view, name="Form Creation") + + return view + + +def _inventorty_config(inventory_type): + if inventory_type == "property": + return Property, PropertyView, PropertyState, PropertyAuditLog, "propertyview__cycle", "propertyview_set" + elif inventory_type == "taxlot": + return TaxLot, TaxLotView, TaxLotState, TaxLotAuditLog, "taxlotview__cycle", "taxlotview_set" + else: + raise ValueError(f"Invalid inventory type: {inventory_type}") diff --git a/seed/views/v3/properties.py b/seed/views/v3/properties.py index a1d30a8e53..9e1e261e6d 100644 --- a/seed/views/v3/properties.py +++ b/seed/views/v3/properties.py @@ -33,7 +33,7 @@ from seed.building_sync.building_sync import BuildingSync from seed.data_importer import tasks -from seed.data_importer.match import save_state_match, get_matching_criteria_column_names +from seed.data_importer.match import save_state_match from seed.data_importer.meters_parser import MetersParser from seed.data_importer.models import ImportFile, ImportRecord from seed.data_importer.tasks import _save_pm_meter_usage_data_task @@ -88,6 +88,7 @@ from seed.tasks import update_state_derived_data from seed.utils.api import OrgMixin, ProfileIdMixin, api_endpoint_class from seed.utils.api_schema import AutoSchemaHelper, swagger_auto_schema_org_query_param +from seed.utils.inventory import create_inventory from seed.utils.inventory_filter import get_filtered_results from seed.utils.labels import get_labels from seed.utils.match import MergeLinkPairError, match_merge_link @@ -1959,35 +1960,16 @@ def form_create(self, request): data = request.data access_level_instance_id = data.get("access_level_instance") cycle_id = data.get("cycle") - property_data = data.get("state") - taxlot_data = data.get("taxlot_state") # rp - need to tie in. - - # Create extra data columns if necessary - extra_data = property_data.get("extra_data", {}) - for column in extra_data: - Column.objects.get_or_create( - is_extra_data=True, - column_name=column, - organization_id=org_id, - table_name="PropertyState", - ) + new_state_data = data.get("state") - # Create stub state - rp - why? why cant we assign data - state = PropertyState.objects.create(organization_id=org_id) - # get_or_create existing property and view - matching_columns = get_matching_criteria_column_names(org_id, "PropertyState") - filter_query = {col: property_data.get(col) for col in matching_columns if col in property_data} - matching_state = PropertyState.objects.filter(**filter_query, organization=org_id, propertyview__cycle=cycle_id).first() - if matching_state: - view = matching_state.propertyview_set.first() - property = view.property - else: - property = Property.objects.create(organization_id=org_id, access_level_instance_id=access_level_instance_id) - view = PropertyView.objects.create(cycle_id=cycle_id, property=property, state=state) - + if new_state_data == {"extra_data": {}}: + return JsonResponse({"status": "error", "message": "No data provided"}, status=status.HTTP_400_BAD_REQUEST) + + view = create_inventory("property", org_id, cycle_id, access_level_instance_id, new_state_data) + # if taxlot_view_id passed, link property and taxlot + if taxlot_view_id := request.query_params.get("related_view_id"): + TaxLotProperty.objects.get_or_create(property_view_id=view.id, taxlot_view_id=taxlot_view_id, cycle_id=cycle_id) - PropertyAuditLog.objects.create(organization_id=org_id, state=state, view=view, name="Form Creation") - # Use existing view endpoint to ensure matching, merging, and linking. return self.update(request, pk=view.id) diff --git a/seed/views/v3/taxlots.py b/seed/views/v3/taxlots.py index e11623bb31..f2b2515ff7 100644 --- a/seed/views/v3/taxlots.py +++ b/seed/views/v3/taxlots.py @@ -40,9 +40,10 @@ from seed.tasks import update_state_derived_data from seed.utils.api import OrgMixin, ProfileIdMixin, api_endpoint_class from seed.utils.api_schema import AutoSchemaHelper, swagger_auto_schema_org_query_param +from seed.utils.inventory import create_inventory from seed.utils.inventory_filter import get_filtered_results from seed.utils.labels import get_labels -from seed.utils.match import MergeLinkPairError, get_matching_criteria_column_names, match_merge_link +from seed.utils.match import MergeLinkPairError, match_merge_link from seed.utils.merge import merge_taxlots from seed.utils.properties import get_changed_fields, pair_unpair_property_taxlot, update_result_with_master from seed.utils.taxlots import taxlots_across_cycles @@ -730,7 +731,7 @@ def update(self, request, pk): return JsonResponse(result, status=status.HTTP_204_NO_CONTENT) else: return JsonResponse(result, status=status.HTTP_404_NOT_FOUND) - + @api_endpoint_class @ajax_request_class @has_perm_class("requires_member") @@ -741,33 +742,15 @@ def form_create(self, request): access_level_instance_id = data.get("access_level_instance") cycle_id = data.get("cycle") new_state_data = data.get("state") - property_data = data.get("PropertyState") # rp - need to tie in - - # Create extra data columns if necessary - extra_data = new_state_data.get("extra_data", {}) - for column in extra_data: - Column.objects.get_or_create( - is_extra_data=True, - column_name=column, - organization_id=org_id, - table_name="TaxLotState", - ) - - # Create stub state - state = TaxLotState.objects.create(organization_id=org_id) - # get_or_create existing taxlot and view - matching_columns = get_matching_criteria_column_names(org_id, "TaxLotState") - filter_query = {col: new_state_data.get(col) for col in matching_columns if col in new_state_data} - matching_state = TaxLotState.objects.filter(**filter_query, organization=org_id, taxlotview__cycle=cycle_id).first() - if matching_state: - view = matching_state.taxlotview_set.first() - taxlot = view.taxlot - else: - taxlot = TaxLot.objects.create(organization_id=org_id, access_level_instance_id=access_level_instance_id) - view = TaxLotView.objects.create(cycle_id=cycle_id, taxlot=taxlot, state=state) - TaxLotAuditLog.objects.create(organization_id=org_id, state=state, view=view, name="Form Creation") - # Use existing view endpoint to ensure matching, merging, and linking. + if new_state_data == {"extra_data": {}}: + return JsonResponse({"status": "error", "message": "No data provided"}, status=status.HTTP_400_BAD_REQUEST) + + view = create_inventory("taxlot", org_id, cycle_id, access_level_instance_id, new_state_data) + # if property_view_id passed, link property and taxlot + if property_view_id := request.query_params.get("related_view_id"): + TaxLotProperty.objects.get_or_create(property_view_id=property_view_id, taxlot_view_id=view.id, cycle_id=cycle_id) + return self.update(request, pk=view.id) From 2f619bc8cf312e02e8adfad04f1736b56d4fea2b Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 8 Jan 2025 10:09:28 -0700 Subject: [PATCH 10/14] translations --- locale/en_US/LC_MESSAGES/django.mo | Bin 154446 -> 154762 bytes locale/en_US/LC_MESSAGES/django.po | 12 ++++++++ locale/es/LC_MESSAGES/django.mo | Bin 16986 -> 17241 bytes locale/es/LC_MESSAGES/django.po | 14 ++++++++- locale/fr_CA/LC_MESSAGES/django.mo | Bin 170326 -> 170592 bytes locale/fr_CA/LC_MESSAGES/django.po | 12 ++++++++ seed/static/seed/locales/en_US.json | 4 +++ seed/static/seed/locales/es.json | 4 +++ seed/static/seed/locales/fr_CA.json | 4 +++ .../seed/partials/inventory_create.html | 28 +++++++++--------- 10 files changed, 63 insertions(+), 15 deletions(-) diff --git a/locale/en_US/LC_MESSAGES/django.mo b/locale/en_US/LC_MESSAGES/django.mo index 0a30fd0abc2bde37d2a20275369a7c51b2d4c461..ac1cfba9a6b484d349fd6323ebce94f39b9d26a9 100644 GIT binary patch delta 35444 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 diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index a39abe07bd..125d94ddd2 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: lokalise.com\n" "Project-Id-Version: SEED Platform\n" -"PO-Revision-Date: 2024-12-14 00:26\n" +"PO-Revision-Date: 2025-01-08 18:08\n" "Last-Translator: lokalise.com\n" "Language-Team: lokalise.com\n\n" "Language: es\n" @@ -461,6 +461,12 @@ msgstr "Configuración avanzada" msgid "Alias" msgstr "Alias" +msgid "All Canonical Fields" +msgstr "Todos los campos canónicos" + +msgid "All Extra Data Fields" +msgstr "Todos los campos de datos adicionales" + #, fuzzy msgid "An Audit Template organization token, user email and password are required" msgstr "Se requiere un token de organización de Plantilla de Auditoría, un correo electrónico de usuario y una contraseña" @@ -1245,6 +1251,12 @@ msgstr "Crear una nueva suborganización" msgid "Create a Program to get started!" msgstr "Cree un programa para empezar" +msgid "Create a Property" +msgstr "Crear una propiedad" + +msgid "Create a Tax Lot" +msgstr "Crear un lote de impuestos" + #, fuzzy msgid "Create a new rule" msgstr "Crear una nueva regla" diff --git a/locale/fr_CA/LC_MESSAGES/django.mo b/locale/fr_CA/LC_MESSAGES/django.mo index 805cb30b4000d38a1d5728a866aaea42c6477fb7..9ce4a1e48e074ebb42062c8b024f98ec5950efa8 100644 GIT binary patch delta 34590 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 diff --git a/locale/fr_CA/LC_MESSAGES/django.po b/locale/fr_CA/LC_MESSAGES/django.po index 1e29ab3360..4ec6eb18f5 100644 --- a/locale/fr_CA/LC_MESSAGES/django.po +++ b/locale/fr_CA/LC_MESSAGES/django.po @@ -364,6 +364,12 @@ msgstr "Réglages avancés" msgid "Alias" msgstr "Alias" +msgid "All Canonical Fields" +msgstr "Tous les champs canoniques" + +msgid "All Extra Data Fields" +msgstr "Tous les champs de données supplémentaires" + msgid "An Audit Template organization token, user email and password are required" msgstr "Un jeton d'organisation Audit Template, un e-mail d'utilisateur et un mot de passe sont requis" @@ -980,6 +986,12 @@ msgstr "Créer une nouvelle sous-organisation" msgid "Create a Program to get started!" msgstr "Créez un programme pour commencer !" +msgid "Create a Property" +msgstr "Créer une propriété" + +msgid "Create a Tax Lot" +msgstr "Créer un lot fiscal" + msgid "Create a new rule" msgstr "Créer une nouvelle règle" diff --git a/seed/static/seed/locales/en_US.json b/seed/static/seed/locales/en_US.json index 4af53e0abe..00d34e0890 100644 --- a/seed/static/seed/locales/en_US.json +++ b/seed/static/seed/locales/en_US.json @@ -114,6 +114,8 @@ "Admin": "Admin", "Advanced Settings": "Advanced Settings", "Alias": "Alias", + "All Canonical Fields": "All Canonical Fields", + "All Extra Data Fields": "All Extra Data Fields", "An Audit Template organization token, user email and password are required": "An Audit Template organization token, user email and password are required", "An error occurred while processing the file. Please ensure that your file meets the required specifications.": "An error occurred while processing the file. Please ensure that your file meets the required specifications.", "Analyses": "Analyses", @@ -313,6 +315,8 @@ "Create a New Project": "Create a New Project", "Create a New Sub-Organization": "Create a New Sub-Organization", "Create a Program to get started!": "Create a Program to get started!", + "Create a Property": "Create a Property", + "Create a Tax Lot": "Create a Tax Lot", "Create a new rule": "Create a new rule", "Create a new sub-organization": "Create a new sub-organization", "Create a new...": "Create a new...", diff --git a/seed/static/seed/locales/es.json b/seed/static/seed/locales/es.json index 0f09adadf4..6c4f2f34f0 100644 --- a/seed/static/seed/locales/es.json +++ b/seed/static/seed/locales/es.json @@ -114,6 +114,8 @@ "Admin": "Admin", "Advanced Settings": "Configuración avanzada", "Alias": "Alias", + "All Canonical Fields": "Todos los campos canónicos", + "All Extra Data Fields": "Todos los campos de datos adicionales", "An Audit Template organization token, user email and password are required": "Se requiere un token de organización de Plantilla de Auditoría, un correo electrónico de usuario y una contraseña", "An error occurred while processing the file. Please ensure that your file meets the required specifications.": "Se ha producido un error al procesar el archivo. Asegúrese de que su archivo cumple las especificaciones requeridas.", "Analyses": "Análisis", @@ -313,6 +315,8 @@ "Create a New Project": "Crear un nuevo proyecto", "Create a New Sub-Organization": "Crear una nueva suborganización", "Create a Program to get started!": "Cree un programa para empezar", + "Create a Property": "Crear una propiedad", + "Create a Tax Lot": "Crear un lote de impuestos", "Create a new rule": "Crear una nueva regla", "Create a new sub-organization": "Crear una nueva suborganización", "Create a new...": "Crear un nuevo...", diff --git a/seed/static/seed/locales/fr_CA.json b/seed/static/seed/locales/fr_CA.json index 85bec50011..d1971f4902 100644 --- a/seed/static/seed/locales/fr_CA.json +++ b/seed/static/seed/locales/fr_CA.json @@ -114,6 +114,8 @@ "Admin": "Admin", "Advanced Settings": "Réglages avancés", "Alias": "Alias", + "All Canonical Fields": "Tous les champs canoniques", + "All Extra Data Fields": "Tous les champs de données supplémentaires", "An Audit Template organization token, user email and password are required": "Un jeton d'organisation Audit Template, un e-mail d'utilisateur et un mot de passe sont requis", "An error occurred while processing the file. Please ensure that your file meets the required specifications.": "Une erreur s'est produite lors du traitement du fichier. Assurez-vous que votre fichier respecte les spécifications requises.", "Analyses": "Analyses", @@ -313,6 +315,8 @@ "Create a New Project": "Créer un nouveau projet", "Create a New Sub-Organization": "Créer une nouvelle sous-organisation", "Create a Program to get started!": "Créez un programme pour commencer !", + "Create a Property": "Créer une propriété", + "Create a Tax Lot": "Créer un lot fiscal", "Create a new rule": "Créer une nouvelle règle", "Create a new sub-organization": "Créer une nouvelle sous-organisation", "Create a new...": "Créer un nouveau...", diff --git a/seed/static/seed/partials/inventory_create.html b/seed/static/seed/partials/inventory_create.html index bc20d79d7b..8db6d7077b 100644 --- a/seed/static/seed/partials/inventory_create.html +++ b/seed/static/seed/partials/inventory_create.html @@ -21,10 +21,10 @@

{$:: (inventory_type === 'taxlots' ? 'Tax Lots' : 'Properties') | translate
-

Create a {$ inventory_type === 'properties' ? 'Property' : 'Tax Lot'$}

+

{$ inventory_type === 'taxlots' ? 'Create a Tax Lot' : 'Create a Property' | translate $}

- +
@@ -70,27 +70,27 @@

Create a {$ inventory_type === '
- - - + + +
- - - - - - + + + + + + - + - +
Inventory TypeColumnValueMatching CriteriaExtra DataData TypeInventory TypeColumnValueMatching CriteriaExtra DataData Type
{$ column.table_name.slice(0, -5) $}{$ column.table_name.slice(0, -5) | translate $} Create a {$ inventory_type === ' {$ column.data_type $}{$ column.data_type | translate $}
- +
From 1bc75c67be32da3ee9d306b513ee065355c016a1 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 8 Jan 2025 10:22:46 -0700 Subject: [PATCH 11/14] cleanup --- seed/static/seed/partials/inventory_nav.html | 3 ++- seed/tests/test_taxlot_views.py | 2 +- seed/utils/inventory.py | 7 ++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/seed/static/seed/partials/inventory_nav.html b/seed/static/seed/partials/inventory_nav.html index 81c698a6f6..b03ee26a0e 100644 --- a/seed/static/seed/partials/inventory_nav.html +++ b/seed/static/seed/partials/inventory_nav.html @@ -6,4 +6,5 @@ Map Data Summary -Create a {$ inventory_type == 'taxlots' ? 'Tax Lot' : 'Property' $} +Create a Tax Lot +Create a Property diff --git a/seed/tests/test_taxlot_views.py b/seed/tests/test_taxlot_views.py index 204cd157ea..803bbde0f8 100644 --- a/seed/tests/test_taxlot_views.py +++ b/seed/tests/test_taxlot_views.py @@ -362,7 +362,7 @@ def test_taxlot_form_create(self): assert response.status_code == 200 assert response.json().get("view_id") - # For a new property, counts should only increase by 1 + # For a new tax lot, counts should only increase by 1 assert TaxLotState.objects.count() == original_state_count + 1 assert TaxLotView.objects.count() == original_view_count + 1 assert TaxLot.objects.count() == original_taxlot_count + 1 diff --git a/seed/utils/inventory.py b/seed/utils/inventory.py index f2c8845cdf..9f3d3cd713 100644 --- a/seed/utils/inventory.py +++ b/seed/utils/inventory.py @@ -2,7 +2,7 @@ from seed.utils.match import get_matching_criteria_column_names -def create_inventory(inventory_type, org_id, cycle_id, access_level_instance_id, new_state_data, related=False): +def create_inventory(inventory_type, org_id, cycle_id, access_level_instance_id, new_state_data): inventory_class, view_class, state_class, auditlog_class, cycle_query, viewset = _inventorty_config(inventory_type) # Create extra data columns if necessary @@ -11,10 +11,7 @@ def create_inventory(inventory_type, org_id, cycle_id, access_level_instance_id, Column.objects.get_or_create(is_extra_data=True, column_name=column, organization_id=org_id, table_name=state_class.__name__) # Create stub state - if related: - state = state_class.objects.create(organization_id=org_id, **new_state_data) - else: - state = state_class.objects.create(organization_id=org_id) + state = state_class.objects.create(organization_id=org_id) # get_or_create existing inventory and view matching_columns = get_matching_criteria_column_names(org_id, state_class.__name__) filter_query = {col: new_state_data.get(col) for col in matching_columns if col in new_state_data} From f91d4a2623ef9db03994dda4fa5d057ba82f22c8 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 8 Jan 2025 13:10:09 -0700 Subject: [PATCH 12/14] testing --- .../static/seed/js/controllers/inventory_create_controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/seed/static/seed/js/controllers/inventory_create_controller.js b/seed/static/seed/js/controllers/inventory_create_controller.js index 0346547e4c..ec16c9decd 100644 --- a/seed/static/seed/js/controllers/inventory_create_controller.js +++ b/seed/static/seed/js/controllers/inventory_create_controller.js @@ -144,7 +144,7 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre break; default: $scope.form_columns = [...$scope.matching_columns]; - $scope.form_columns = $scope.form_columns.map((c) => ({ ...c, value: 'null', is_duplicate: false })); + $scope.form_columns = $scope.form_columns.map((c) => ({ ...c, value: null, is_duplicate: false })); $scope.form_values = []; } }; @@ -179,7 +179,7 @@ angular.module('SEED.controller.inventory_create', []).controller('inventory_cre $scope.change_column = (displayName, index) => { const defaults = { - primary, + table_name: primary, is_extra_data: true, is_matching_criteria: false, data_type: 'string' From f83dacc83b06b22ac6510a7756b1214f7181d01b Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Tue, 14 Jan 2025 11:40:31 -0700 Subject: [PATCH 13/14] http error catching --- seed/views/v3/properties.py | 38 +++++++++++++++++++++++++++++-------- seed/views/v3/taxlots.py | 35 +++++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/seed/views/v3/properties.py b/seed/views/v3/properties.py index 9e1e261e6d..20df1f207a 100644 --- a/seed/views/v3/properties.py +++ b/seed/views/v3/properties.py @@ -91,7 +91,7 @@ from seed.utils.inventory import create_inventory from seed.utils.inventory_filter import get_filtered_results from seed.utils.labels import get_labels -from seed.utils.match import MergeLinkPairError, match_merge_link +from seed.utils.match import MergeLinkPairError, match_merge_link, get_matching_criteria_column_names from seed.utils.merge import merge_properties from seed.utils.meters import PropertyMeterReadingsExporter from seed.utils.properties import get_changed_fields, pair_unpair_property_taxlot, properties_across_cycles, update_result_with_master @@ -1951,24 +1951,46 @@ def facility_bps_export_to_cts(self, request): return response + @swagger_auto_schema( + manual_parameters=[AutoSchemaHelper.query_org_id_field()], + request_body=AutoSchemaHelper.schema_factory( + { + "access_level_instance": "integer", + "cycle": "integer", + "state": "object", + }, + description="state object represpents a PropertySstate's data", + ), + ) @api_endpoint_class @ajax_request_class @has_perm_class("requires_member") @action(detail=False, methods=["POST"]) def form_create(self, request): + """Manaully create a property""" org_id = self.get_organization(request) data = request.data - access_level_instance_id = data.get("access_level_instance") - cycle_id = data.get("cycle") new_state_data = data.get("state") - if new_state_data == {"extra_data": {}}: - return JsonResponse({"status": "error", "message": "No data provided"}, status=status.HTTP_400_BAD_REQUEST) - - view = create_inventory("property", org_id, cycle_id, access_level_instance_id, new_state_data) + try: + access_level_instance = AccessLevelInstance.objects.get(pk=data.get("access_level_instance"), organization_id=org_id) + cycle = Cycle.objects.get(pk=data.get("cycle"), organization_id=org_id) + except AccessLevelInstance.DoesNotExist: + return JsonResponse({"status": "error", "message": "Access Level Instance does not exist"}, status=status.HTTP_404_NOT_FOUND) + except Cycle.DoesNotExist: + return JsonResponse({"status": "error", "message": "Cycle does not exist"}, status=status.HTTP_404_NOT_FOUND) + + matching_columns = get_matching_criteria_column_names(org_id, "PropertyState") + if not (matching_columns & new_state_data.keys()): + return JsonResponse( + {"status": "error", "message": f"At least one of the following matching fields are required: {matching_columns}"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + view = create_inventory("property", org_id, cycle.id, access_level_instance.id, new_state_data) # if taxlot_view_id passed, link property and taxlot if taxlot_view_id := request.query_params.get("related_view_id"): - TaxLotProperty.objects.get_or_create(property_view_id=view.id, taxlot_view_id=taxlot_view_id, cycle_id=cycle_id) + TaxLotProperty.objects.get_or_create(property_view_id=view.id, taxlot_view_id=taxlot_view_id, cycle_id=cycle.id) return self.update(request, pk=view.id) diff --git a/seed/views/v3/taxlots.py b/seed/views/v3/taxlots.py index f2b2515ff7..1471e78dc3 100644 --- a/seed/views/v3/taxlots.py +++ b/seed/views/v3/taxlots.py @@ -24,6 +24,7 @@ MERGE_STATE_MERGED, MERGE_STATE_NEW, Column, + Cycle, DerivedColumn, InventoryGroupMapping, Note, @@ -43,7 +44,7 @@ from seed.utils.inventory import create_inventory from seed.utils.inventory_filter import get_filtered_results from seed.utils.labels import get_labels -from seed.utils.match import MergeLinkPairError, match_merge_link +from seed.utils.match import MergeLinkPairError, match_merge_link, get_matching_criteria_column_names from seed.utils.merge import merge_taxlots from seed.utils.properties import get_changed_fields, pair_unpair_property_taxlot, update_result_with_master from seed.utils.taxlots import taxlots_across_cycles @@ -732,6 +733,17 @@ def update(self, request, pk): else: return JsonResponse(result, status=status.HTTP_404_NOT_FOUND) + @swagger_auto_schema( + manual_parameters=[AutoSchemaHelper.query_org_id_field()], + request_body=AutoSchemaHelper.schema_factory( + { + "access_level_instance": "integer", + "cycle": "integer", + "state": "object", + }, + description="state object represpents a TaxLotState's data", + ), + ) @api_endpoint_class @ajax_request_class @has_perm_class("requires_member") @@ -739,17 +751,30 @@ def update(self, request, pk): def form_create(self, request): org_id = self.get_organization(request) data = request.data - access_level_instance_id = data.get("access_level_instance") - cycle_id = data.get("cycle") new_state_data = data.get("state") + try: + access_level_instance = AccessLevelInstance.objects.get(pk=data.get("access_level_instance"), organization_id=org_id) + cycle = Cycle.objects.get(pk=data.get("cycle"), organization_id=org_id) + except AccessLevelInstance.DoesNotExist: + return JsonResponse({"status": "error", "message": "Access Level Instance does not exist"}, status=status.HTTP_404_NOT_FOUND) + except Cycle.DoesNotExist: + return JsonResponse({"status": "error", "message": "Cycle does not exist"}, status=status.HTTP_404_NOT_FOUND) + + matching_columns = get_matching_criteria_column_names(org_id, "TaxLotState") + if not (matching_columns & new_state_data.keys()): + return JsonResponse( + {"status": "error", "message": f"At least one of the following matching fields are required: {matching_columns}"}, + status=status.HTTP_400_BAD_REQUEST, + ) + if new_state_data == {"extra_data": {}}: return JsonResponse({"status": "error", "message": "No data provided"}, status=status.HTTP_400_BAD_REQUEST) - view = create_inventory("taxlot", org_id, cycle_id, access_level_instance_id, new_state_data) + view = create_inventory("taxlot", org_id, cycle.id, access_level_instance.id, new_state_data) # if property_view_id passed, link property and taxlot if property_view_id := request.query_params.get("related_view_id"): - TaxLotProperty.objects.get_or_create(property_view_id=property_view_id, taxlot_view_id=view.id, cycle_id=cycle_id) + TaxLotProperty.objects.get_or_create(property_view_id=property_view_id, taxlot_view_id=view.id, cycle_id=cycle.id) return self.update(request, pk=view.id) From 532435f5c147599aa6c32563a657ea59bb3fdc11 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Fri, 17 Jan 2025 13:09:11 -0700 Subject: [PATCH 14/14] precommit --- seed/tests/test_property_views.py | 4 ++-- seed/tests/test_taxlot_views.py | 4 ++-- seed/views/v3/properties.py | 6 +++--- seed/views/v3/taxlots.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/seed/tests/test_property_views.py b/seed/tests/test_property_views.py index 07946455ba..61e3784ec0 100644 --- a/seed/tests/test_property_views.py +++ b/seed/tests/test_property_views.py @@ -707,12 +707,12 @@ def test_property_form_create(self): # property associated with a taxlot property_data = { - "acess_level_instance": self.org.root.id, + "access_level_instance": self.org.root.id, "cycle": self.cycle.id, "state": {"pm_property_id": "7", "custom_id_1": "8", "extra_data": {"Extra Data Column": "9"}, "lot_number": "10"}, } taxlot_data = { - "acess_level_instance": self.org.root.id, + "access_level_instance": self.org.root.id, "cycle": self.cycle.id, "state": {"jurisdiction_tax_lot_id": "10", "address_line_1": "A"}, } diff --git a/seed/tests/test_taxlot_views.py b/seed/tests/test_taxlot_views.py index 803bbde0f8..80d651046b 100644 --- a/seed/tests/test_taxlot_views.py +++ b/seed/tests/test_taxlot_views.py @@ -369,12 +369,12 @@ def test_taxlot_form_create(self): # taxlot associated with a property taxlot_data = { - "acess_level_instance": self.org.root.id, + "access_level_instance": self.org.root.id, "cycle": self.cycle.id, "state": {"jurisdiction_tax_lot_id": "10", "address_line_1": "A"}, } property_data = { - "acess_level_instance": self.org.root.id, + "access_level_instance": self.org.root.id, "cycle": self.cycle.id, "state": {"pm_property_id": "7", "custom_id_1": "8", "extra_data": {"Extra Data Column": "9"}, "lot_number": "10"}, } diff --git a/seed/views/v3/properties.py b/seed/views/v3/properties.py index 20df1f207a..05764a15da 100644 --- a/seed/views/v3/properties.py +++ b/seed/views/v3/properties.py @@ -91,7 +91,7 @@ from seed.utils.inventory import create_inventory from seed.utils.inventory_filter import get_filtered_results from seed.utils.labels import get_labels -from seed.utils.match import MergeLinkPairError, match_merge_link, get_matching_criteria_column_names +from seed.utils.match import MergeLinkPairError, get_matching_criteria_column_names, match_merge_link from seed.utils.merge import merge_properties from seed.utils.meters import PropertyMeterReadingsExporter from seed.utils.properties import get_changed_fields, pair_unpair_property_taxlot, properties_across_cycles, update_result_with_master @@ -1977,7 +1977,7 @@ def form_create(self, request): cycle = Cycle.objects.get(pk=data.get("cycle"), organization_id=org_id) except AccessLevelInstance.DoesNotExist: return JsonResponse({"status": "error", "message": "Access Level Instance does not exist"}, status=status.HTTP_404_NOT_FOUND) - except Cycle.DoesNotExist: + except Cycle.DoesNotExist: return JsonResponse({"status": "error", "message": "Cycle does not exist"}, status=status.HTTP_404_NOT_FOUND) matching_columns = get_matching_criteria_column_names(org_id, "PropertyState") @@ -1986,7 +1986,7 @@ def form_create(self, request): {"status": "error", "message": f"At least one of the following matching fields are required: {matching_columns}"}, status=status.HTTP_400_BAD_REQUEST, ) - + view = create_inventory("property", org_id, cycle.id, access_level_instance.id, new_state_data) # if taxlot_view_id passed, link property and taxlot if taxlot_view_id := request.query_params.get("related_view_id"): diff --git a/seed/views/v3/taxlots.py b/seed/views/v3/taxlots.py index 1471e78dc3..f5634524a0 100644 --- a/seed/views/v3/taxlots.py +++ b/seed/views/v3/taxlots.py @@ -44,7 +44,7 @@ from seed.utils.inventory import create_inventory from seed.utils.inventory_filter import get_filtered_results from seed.utils.labels import get_labels -from seed.utils.match import MergeLinkPairError, match_merge_link, get_matching_criteria_column_names +from seed.utils.match import MergeLinkPairError, get_matching_criteria_column_names, match_merge_link from seed.utils.merge import merge_taxlots from seed.utils.properties import get_changed_fields, pair_unpair_property_taxlot, update_result_with_master from seed.utils.taxlots import taxlots_across_cycles @@ -758,7 +758,7 @@ def form_create(self, request): cycle = Cycle.objects.get(pk=data.get("cycle"), organization_id=org_id) except AccessLevelInstance.DoesNotExist: return JsonResponse({"status": "error", "message": "Access Level Instance does not exist"}, status=status.HTTP_404_NOT_FOUND) - except Cycle.DoesNotExist: + except Cycle.DoesNotExist: return JsonResponse({"status": "error", "message": "Cycle does not exist"}, status=status.HTTP_404_NOT_FOUND) matching_columns = get_matching_criteria_column_names(org_id, "TaxLotState")