Skip to content

Commit

Permalink
Updates to meter connection modal (#4868)
Browse files Browse the repository at this point in the history
* Add modal to update meter connections

* Create update system meter connection endpoint

* modal development

* hookup group meter update

* Fix tests

* add config to serailized meter

* precommit

* comments

* fix test

* Add dashbard endpoint

* group header styling

* readable keys

* sort options

precommit

precommit

* migration

* precommit

* fix test

* hookup group meter delete

---------

Co-authored-by: Hannah Eslinger <[email protected]>
Co-authored-by: Ross Perry <[email protected]>
  • Loading branch information
3 people authored Nov 1, 2024
1 parent dfc39be commit e5df2f4
Show file tree
Hide file tree
Showing 31 changed files with 741 additions and 109 deletions.
3 changes: 2 additions & 1 deletion seed/api/v3/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from seed.views.v3.historical_notes import HistoricalNoteViewSet
from seed.views.v3.import_files import ImportFileViewSet
from seed.views.v3.inventory_group_mappings import InventoryGroupMappingViewSet
from seed.views.v3.inventory_groups import InventoryGroupViewSet
from seed.views.v3.inventory_groups import InventoryGroupMetersViewSet, InventoryGroupViewSet
from seed.views.v3.label_inventories import LabelInventoryViewSet
from seed.views.v3.labels import LabelViewSet
from seed.views.v3.measures import MeasureViewSet
Expand Down Expand Up @@ -149,6 +149,7 @@

inventory_group_router = nested_routers.NestedSimpleRouter(api_v3_router, r"inventory_groups", lookup="inventory_group")
inventory_group_router.register(r"systems", SystemViewSet, basename="inventory_group-systems")
inventory_group_router.register(r"meters", InventoryGroupMetersViewSet, basename="inventory_group-meters")

system_router = nested_routers.NestedSimpleRouter(inventory_group_router, r"systems", lookup="system")
system_router.register(r"services", ServiceViewSet, basename="system-services")
Expand Down
20 changes: 20 additions & 0 deletions seed/migrations/0236_auto_20241030_1434.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.2.25 on 2024-10-30 21:34

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("seed", "0235_meter_systems"),
]

operations = [
migrations.AlterModelOptions(
name="service",
options={"ordering": ["name"]},
),
migrations.AlterModelOptions(
name="system",
options={"ordering": ["name"]},
),
]
43 changes: 27 additions & 16 deletions seed/models/meters.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,35 +212,46 @@ def presave_meter(sender, instance, **kwargs):
system = instance.system
connection_type = instance.connection_type
service = instance.service
connection_string = dict(Meter.CONNECTION_TYPES).get(connection_type)

# must be connected to either a system or a property
if property is not None and system is not None:
raise IntegrityError(f"Meter {instance.id} has both a property and a system. It must only have one.")

# Only systems have connection type "total"
system_connections = connection_type in [Meter.TOTAL_FROM_PATRON, Meter.TOTAL_TO_PATRON]
if system_connections and instance.system is None:
raise IntegrityError(f"Meter {instance.id} has connection_type {connection_type}, but is not connected to a system")

outside_connection = connection_type in [Meter.FROM_OUTSIDE, Meter.TO_OUTSIDE]
if outside_connection:
# outside connections don't have services
if instance.service is not None:
raise IntegrityError(f"Meter {instance.id} has connection_type {connection_type}, but also is connected to a service")
raise IntegrityError(f"Meter {instance.id} has connection_type '{connection_string}', but also is connected to a service")
else:
# inside connections _do_ have services
if service is None:
raise IntegrityError(f"Meter {instance.id} has connection_type {connection_type}, but is not connected to a service")
raise IntegrityError(f"Meter {instance.id} has connection_type '{connection_string}', but is not connected to a service")

total_connections = connection_type in [Meter.TOTAL_FROM_PATRON, Meter.TOTAL_TO_PATRON]
if total_connections:
# Only systems have connection type "total"
if system is None:
raise IntegrityError(f"Meter {instance.id} has connection_type '{connection_string}', but is not connected to a system")

# Total connections must have a service owned by system
if system.id != service.system_id:
raise IntegrityError(
f"Meters with connection_type '{connection_string}' must have a service on the system the meter is connected to"
# f"Meter {instance.id} on system {system.name} has connection_type '{connection_string}', but is also connected to service {service.name}, which is on a different system, {service.system.name}. Meters with connection_type '{connection_string}' must have a service on the system the meter is connected to"
)

# inside connections must be within the group
if property is not None:
meter_groups = InventoryGroupMapping.objects.filter(property=property).values_list("group_id", flat=True)
else:
meter_groups = [system.group]
if service is not None and service.system.group.id not in meter_groups:
raise IntegrityError(
f"Meter {instance.id} on property {property.id} and has service {service.id}, but meter is not in the services group"
)
# Service should only have one meter of each "total" connection type
if Meter.objects.filter(service=service, connection_type=connection_type).exclude(pk=instance.pk).exists():
raise IntegrityError(f"Service {service.id} already has a meter with connection type '{connection_string}'")

elif property: # Meter.FROM_PATRON_TO_SERVICE and Meter.FROM_SERVICE_TO_PATRON
# service must be within the meter's property's group
property_groups = InventoryGroupMapping.objects.filter(property=property).values_list("group_id", flat=True)
if service is not None and service.system.group.id not in property_groups:
raise IntegrityError(
f"Meter {instance.id} on property {property.id} and has service {service.name}, but meter and property are not in the service's group"
)


class MeterReading(models.Model):
Expand Down
2 changes: 2 additions & 0 deletions seed/models/systems.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class System(models.Model):
objects = InheritanceManager()

class Meta:
ordering = ["name"]
constraints = [
models.UniqueConstraint(fields=["group", "name"], name="unique_name_for_group"),
]
Expand Down Expand Up @@ -64,6 +65,7 @@ class Service(models.Model):
objects = InheritanceManager()

class Meta:
ordering = ["name"]
constraints = [
models.UniqueConstraint(fields=["system", "name"], name="unique_name_for_system"),
]
11 changes: 10 additions & 1 deletion seed/serializers/inventory_groups.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# !/usr/bin/env python
import logging

from django.core.exceptions import ValidationError
from rest_framework import serializers

from seed.models import VIEW_LIST_INVENTORY_TYPE, InventoryGroup, InventoryGroupMapping, PropertyView, TaxLotView
from seed.serializers.access_level_instances import AccessLevelInstanceSerializer
from seed.serializers.base import ChoiceField
from seed.serializers.systems import SystemSerializer

logger = logging.getLogger()


class InventoryGroupMappingSerializer(serializers.ModelSerializer):
Expand All @@ -21,6 +26,10 @@ def get_group_name(self, obj):
class InventoryGroupSerializer(serializers.ModelSerializer):
inventory_type = ChoiceField(choices=VIEW_LIST_INVENTORY_TYPE)
access_level_instance_data = AccessLevelInstanceSerializer(source="access_level_instance", many=False, read_only=True)
systems = serializers.SerializerMethodField()

def get_systems(self, obj):
return SystemSerializer(obj.systems.all().select_subclasses(), many=True).data

def __init__(self, *args, **kwargs):
if "inventory" not in kwargs:
Expand All @@ -33,7 +42,7 @@ def __init__(self, *args, **kwargs):

class Meta:
model = InventoryGroup
fields = ("id", "name", "inventory_type", "access_level_instance", "access_level_instance_data", "organization")
fields = ("id", "name", "inventory_type", "access_level_instance", "access_level_instance_data", "organization", "systems")

def to_representation(self, obj):
result = super().to_representation(obj)
Expand Down
26 changes: 25 additions & 1 deletion seed/serializers/meters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
class MeterSerializer(serializers.ModelSerializer, OrgMixin):
type = ChoiceField(choices=Meter.ENERGY_TYPES, required=True)
connection_type = ChoiceField(choices=Meter.CONNECTION_TYPES, required=True)
service_id = serializers.IntegerField(source="service.id", allow_null=True, read_only=True)
service_name = serializers.CharField(source="service.name", allow_null=True, read_only=True)
service_group = serializers.IntegerField(source="service.system.group_id", allow_null=True, read_only=True)
alias = serializers.CharField(required=False, allow_blank=True)
source = ChoiceField(choices=Meter.SOURCES)
source_id = serializers.CharField(required=False, allow_blank=True)
property_id = serializers.IntegerField(required=False, allow_null=True)
system_id = serializers.IntegerField(required=False, allow_null=True)
system_id = serializers.IntegerField(source="system.id", required=False, allow_null=True)
system_name = serializers.CharField(source="system.name", required=False, allow_null=True)
scenario_id = serializers.IntegerField(required=False, allow_null=True)

Expand Down Expand Up @@ -55,4 +56,27 @@ def to_representation(self, obj):
if obj.alias is None or obj.alias == "":
result["alias"] = f"{obj.get_type_display()} - {obj.get_source_display()} - {result['source_id']}"

self.set_config(obj, result)

return result

def set_config(self, obj, result):
# generate config for meter modal
connection_lookup = {
1: {"direction": "inflow", "use": "outside", "connection": "outside"},
2: {"direction": "outflow", "use": "outside", "connection": "outside"},
3: {"direction": "inflow", "use": "using", "connection": "service"},
4: {"direction": "outflow", "use": "using", "connection": "service"},
5: {"direction": "inflow", "use": "offering", "connection": "service"},
6: {"direction": "outflow", "use": "offering", "connection": "service"},
}

group_id, system_id = None, None
if obj.service:
system = obj.service.system
group_id, system_id = system.group.id, system.id
elif obj.system:
group_id, system_id = obj.system.group.id, obj.system.id

config = {"group_id": group_id, "system_id": system_id, "service_id": obj.service_id, **connection_lookup[obj.connection_type]}
result["config"] = config
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ angular.module('SEED.controller.inventory_detail_meters', []).controller('invent
'cycles',
'dataset_service',
'inventory_service',
'inventory_group_service',
'inventory_payload',
'meters',
'property_meter_usage',
'spinner_utility',
'urls',
'organization_payload',
'group',
// eslint-disable-next-line func-names
function (
$state,
Expand All @@ -32,22 +32,22 @@ angular.module('SEED.controller.inventory_detail_meters', []).controller('invent
cycles,
dataset_service,
inventory_service,
inventory_group_service,
inventory_payload,
meters,
property_meter_usage,
spinner_utility,
urls,
organization_payload,
group
organization_payload
) {
spinner_utility.show();
$scope.inventory_type = $stateParams.inventory_type;
if (inventory_payload) {
$scope.item_state = inventory_payload.state;
} else if (group) {
$scope.group = group;
$scope.group_id = group.id;
}
$scope.item_state = inventory_payload.state;

inventory_group_service.get_groups_for_inventory('properties', [inventory_payload.property.id]).then((groups) => {
$scope.groups = groups;
});

$scope.organization = organization_payload.organization;
$scope.filler_cycle = cycles.cycles[0].id;

Expand All @@ -68,10 +68,12 @@ angular.module('SEED.controller.inventory_detail_meters', []).controller('invent

resetSelections();

// dont show edit if disabled?
const buttons = (
'<div style="display: flex; flex-direction=column">' +
' <button type="button" ng-show="grid.appScope.menu.user.organization.user_role !== \'viewer\'" class="btn-primary" style="border-radius: 4px;" ng-click="grid.appScope.open_meter_deletion_modal(row.entity)" translate>Delete</button>' +
' <button type="button" ng-show="grid.appScope.menu.user.organization.user_role !== \'viewer\'" class="btn-primary" style="border-radius: 4px;" ng-click="grid.appScope.open_meter_connection_edit_modal(row.entity)" translate>Edit Connection</button>' +
'<div class="meters-table-actions" style="display: flex; flex-direction=column">' +
' <button type="button" ng-show="grid.appScope.menu.user.organization.user_role !== \'viewer\'" class="btn-danger" style="border-radius: 4px;" ng-click="grid.appScope.open_meter_deletion_modal(row.entity)" title="Delete Meter"><i class="fa-solid fa-xmark"></i></button>' +
' <button type="button" ng-show="grid.appScope.menu.user.organization.user_role !== \'viewer\' && grid.appScope.groups.length" class="btn-primary" style="border-radius: 4px;" ng-click="grid.appScope.open_meter_connection_edit_modal(row.entity)" title="Edit Meter Connection"><i class="fa-solid fa-pencil"></i></button>' +
' <button type="button" ng-show="grid.appScope.menu.user.organization.user_role !== \'viewer\' && !grid.appScope.groups.length" class="btn-gray" style="border-radius: 4px;" ng-click="grid.appScope.open_meter_connection_edit_modal(row.entity)" title="To Edit Connection, a meter must be part of an inventory group" ng-disabled="true"><i class="fa-solid fa-pencil"></i></button>' +
'</div>'
);

Expand Down Expand Up @@ -164,6 +166,7 @@ angular.module('SEED.controller.inventory_detail_meters', []).controller('invent
controller: 'meter_deletion_modal_controller',
resolve: {
organization_id: () => $scope.organization.id,
group_id: () => null,
meter: () => meter,
view_id: () => $scope.inventory.view_id,
refresh_meters_and_readings: () => $scope.refresh_meters_and_readings
Expand All @@ -172,8 +175,18 @@ angular.module('SEED.controller.inventory_detail_meters', []).controller('invent
};

$scope.open_meter_connection_edit_modal = (meter) => {
// TODO
console.log(meter);
$uibModal.open({
templateUrl: `${urls.static_url}seed/partials/meter_edit_modal.html`,
controller: 'meter_edit_modal_controller',
resolve: {
organization_id: () => $scope.organization.id,
meter: () => meter,
property_id: () => inventory_payload.property.id,
system_id: () => null,
view_id: () => $scope.inventory.view_id,
refresh_meters_and_readings: () => $scope.refresh_meters_and_readings
}
});
};

$scope.apply_column_settings = () => {
Expand Down Expand Up @@ -291,11 +304,7 @@ angular.module('SEED.controller.inventory_detail_meters', []).controller('invent
});
};

if (inventory_payload) {
$scope.inventory_display_name = organization_service.get_inventory_display_value($scope.organization, $scope.inventory_type === 'properties' ? 'property' : 'taxlot', $scope.item_state);
} else if (group) {
$scope.inventory_display_name = group.name;
}
$scope.inventory_display_name = organization_service.get_inventory_display_value($scope.organization, $scope.inventory_type === 'properties' ? 'property' : 'taxlot', $scope.item_state);

$scope.updateHeight = () => {
let height = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,27 @@ angular.module('SEED.controller.inventory_group_detail_dashboard', [])
'$scope',
'$state',
'$stateParams',
'cycles',
'group',
'inventory_group_service',
// eslint-disable-next-line func-names
function (
$scope,
$state,
$stateParams
$stateParams,
cycles,
group,
inventory_group_service
) {
$scope.inventory_display_name = group.name;
$scope.inventory_type = $stateParams.inventory_type;
$scope.group_id = $stateParams.group_id;
$scope.cycles = cycles.cycles;
$scope.selectedCycle = $scope.cycles[0] ?? undefined;
$scope.data = {};
inventory_group_service.get_dashboard_info($scope.group_id, $scope.selectedCycle.id).then((data) => { $scope.data = data; });

$scope.changeCycle = () => {
inventory_group_service.get_dashboard_info($scope.group_id, $scope.selectedCycle.id).then((data) => { $scope.data = data; });
};
}]);
Loading

0 comments on commit e5df2f4

Please sign in to comment.