diff --git a/seed/tests/test_meter_views.py b/seed/tests/test_meter_views.py index cc4be0f653..8ef2907ea5 100644 --- a/seed/tests/test_meter_views.py +++ b/seed/tests/test_meter_views.py @@ -7,12 +7,14 @@ import ast import copy import json +from datetime import datetime from django.urls import reverse +from django.utils import timezone as tz from seed.data_importer.utils import kbtu_thermal_conversion_factors as conversion_factors from seed.landing.models import SEEDUser as User -from seed.models import Meter, Property +from seed.models import Meter, MeterReading, Property from seed.models.scenarios import Scenario from seed.test_helpers.fake import FakePropertyViewFactory from seed.tests.util import AccessLevelBaseTestCase, DeleteModelsTestCase @@ -64,7 +66,7 @@ def setUp(self): def test_create_meter(self): property_view = self.property_view_factory.get_property_view() - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + f"?organization_id={self.org.id}" payload = { "type": "Electric - Grid", @@ -101,7 +103,7 @@ def test_create_meter(self): def test_create_meter_with_scenario(self): property_view = self.property_view_factory.get_property_view() - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + f"?organization_id={self.org.id}" # create a scenario and test again scenario = Scenario.objects.create( @@ -136,7 +138,7 @@ def test_create_meter_with_not_my_scenario(self): ) property_view = self.property_view_factory.get_property_view() - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + f"?organization_id={self.org.id}" payload = { "type": "Electric - Grid", @@ -152,7 +154,7 @@ def test_create_meter_with_not_my_scenario(self): def test_delete_meter(self): # create meter property_view = self.property_view_factory.get_property_view() - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + f"?organization_id={self.org.id}" payload = { "type": "Natural Gas", "source": "Portfolio Manager", @@ -171,8 +173,7 @@ def test_delete_meter(self): meter_id = response.data[0]["id"] meter_url = ( reverse("api:v3:property-meters-detail", kwargs={"property_pk": property_view.id, "pk": meter_id}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) response = self.client.delete(meter_url, content_type="application/json") self.assertEqual(response.status_code, 204) @@ -184,7 +185,7 @@ def test_delete_meter(self): def test_update_meter(self): # create meter property_view = self.property_view_factory.get_property_view() - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + f"?organization_id={self.org.id}" payload = { "type": "Natural Gas", "source": "Portfolio Manager", @@ -200,8 +201,7 @@ def test_update_meter(self): meter_id = response.data["id"] meter_url = ( reverse("api:v3:property-meters-detail", kwargs={"property_pk": property_view.id, "pk": meter_id}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) response = self.client.put(meter_url, data=json.dumps(new_payload), content_type="application/json") self.assertEqual(response.status_code, 200) @@ -215,12 +215,19 @@ def setUp(self): self.property = self.property_factory.get_property() self.property_view = self.property_view_factory.get_property_view(self.property) self.meter = Meter.objects.create(property=self.property) + self.meter_reading = MeterReading.objects.create( + meter=self.meter, + start_time=datetime(2024, 1, 1, 0, 0, tzinfo=tz.utc), + end_time=datetime(2024, 1, 2, 0, 0, tzinfo=tz.utc), + reading=12345, + source_unit="kWh", + conversion_factor=1, + ) def test_get_meters_detail_permissions(self): meter_url = ( reverse("api:v3:property-meters-detail", kwargs={"property_pk": self.property_view.id, "pk": self.meter.id}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) # root users can see meters in root @@ -235,7 +242,7 @@ def test_get_meters_detail_permissions(self): def test_get_meters_list_permissions(self): meter_url = ( - reverse("api:v3:property-meters-list", kwargs={"property_pk": self.property_view.id}) + "?organization_id=" + str(self.org.id) + reverse("api:v3:property-meters-list", kwargs={"property_pk": self.property_view.id}) + f"?organization_id={self.org.id}" ) # root users can see meters in root @@ -251,8 +258,7 @@ def test_get_meters_list_permissions(self): def test_get_meters_delete_permissions(self): meter_url = ( reverse("api:v3:property-meters-detail", kwargs={"property_pk": self.property_view.id, "pk": self.meter.id}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) # child user cannot delete meters in root @@ -266,7 +272,7 @@ def test_get_meters_delete_permissions(self): assert response.status_code == 204 def test_create_meter_permissions(self): - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": self.property_view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": self.property_view.id}) + f"?organization_id={self.org.id}" payload = {"type": "Electric", "source": "Manual Entry", "source_id": "1234567890"} # root users can create meters in root @@ -282,8 +288,7 @@ def test_create_meter_permissions(self): def test_update_meter_permissions(self): url = ( reverse("api:v3:property-meters-detail", kwargs={"property_pk": self.property_view.id, "pk": self.meter.id}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) payload = {"type": "Electric", "source": "Manual Entry", "source_id": "boo"} @@ -297,6 +302,133 @@ def test_update_meter_permissions(self): response = self.client.put(url, data=json.dumps(payload), content_type="application/json") assert response.status_code == 404 + def test_get_meter_reading_list_permissions(self): + meter_reading_url = ( + reverse( + "api:v3:property-meter-readings-list", + kwargs={ + "property_pk": self.property_view.id, + "meter_pk": self.meter.id, + }, + ) + + f"?organization_id={self.org.id}" + ) + + # root users can see meters in root + self.login_as_root_member() + response = self.client.get(meter_reading_url) + assert response.status_code == 200 + + # child user cannot + self.login_as_child_member() + response = self.client.get(meter_reading_url) + assert response.status_code == 404 + + def test_create_meter_reading_list_permissions(self): + meter_reading_url = ( + reverse( + "api:v3:property-meter-readings-list", + kwargs={ + "property_pk": self.property_view.id, + "meter_pk": self.meter.id, + }, + ) + + f"?organization_id={self.org.id}" + ) + payload = { + "start_time": "2024-01-01T00:00:00", + "end_time": "2024-01-02T00:00:00", + "reading": 12345, + "source_unit": "kWh", + "conversion_factor": 1, + } + + # root users can create meter readings in root + self.login_as_root_member() + response = self.client.post(meter_reading_url, data=json.dumps(payload), content_type="application/json") + assert response.status_code == 201 + + # child user cannot + self.login_as_child_member() + response = self.client.post(meter_reading_url, data=json.dumps(payload), content_type="application/json") + assert response.status_code == 404 + + def test_get_meter_reading_detail_permissions(self): + meter_reading_url = ( + reverse( + "api:v3:property-meter-readings-detail", + kwargs={ + "property_pk": self.property_view.id, + "meter_pk": self.meter.id, + "pk": "2024-01-01T00:00:00Z", + }, + ) + + f"?organization_id={self.org.id}" + ) + + # root users can see meter readings in root + self.login_as_root_member() + response = self.client.get(meter_reading_url) + assert response.status_code == 200 + + # child user cannot + self.login_as_child_member() + response = self.client.get(meter_reading_url) + assert response.status_code == 404 + + def test_update_meter_reading_detail_permissions(self): + meter_reading_url = ( + reverse( + "api:v3:property-meter-readings-detail", + kwargs={ + "property_pk": self.property_view.id, + "meter_pk": self.meter.id, + "pk": "2024-01-01T00:00:00Z", + }, + ) + + f"?organization_id={self.org.id}" + ) + payload = { + "start_time": "2024-01-01T00:00:00", + "end_time": "2024-01-02T00:00:00", + "reading": 1337, + "source_unit": "kWh", + "conversion_factor": 1, + } + + # root users can update meter readings in root + self.login_as_root_member() + response = self.client.put(meter_reading_url, data=json.dumps(payload), content_type="application/json") + assert response.status_code == 200 + + # child user cannot + self.login_as_child_member() + response = self.client.put(meter_reading_url, data=json.dumps(payload), content_type="application/json") + assert response.status_code == 404 + + def test_delete_meter_reading_detail_permissions(self): + meter_reading_url = ( + reverse( + "api:v3:property-meter-readings-detail", + kwargs={ + "property_pk": self.property_view.id, + "meter_pk": self.meter.id, + "pk": "2024-01-01T00:00:00Z", + }, + ) + + f"?organization_id={self.org.id}" + ) + + # child user cannot delete meter readings in root + self.login_as_child_member() + response = self.client.delete(meter_reading_url) + assert response.status_code == 404 + + # root users can + self.login_as_root_member() + response = self.client.delete(meter_reading_url) + assert response.status_code == 204 + class TestMeterReadingCRUD(DeleteModelsTestCase): def setUp(self): @@ -315,7 +447,7 @@ def setUp(self): def test_create_meter_readings(self): property_view = self.property_view_factory.get_property_view() - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + f"?organization_id={self.org.id}" payload = { "type": "Electric", @@ -329,8 +461,7 @@ def test_create_meter_readings(self): # create meter readings url = ( reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) # write a few values to the database @@ -361,7 +492,7 @@ def test_create_two_meters_readings(self): """Test edge case to make sure that data for two different meters don't overwrite each other""" property_view = self.property_view_factory.get_property_view() url_meters = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) - url_meters += "?organization_id=" + str(self.org.id) + url_meters += f"?organization_id={self.org.id}" payload = { "type": "Electric", @@ -380,8 +511,14 @@ def test_create_two_meters_readings(self): meter_pk_2 = response.json()["id"] # create meter readings - url_1 = reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk_1}) - url_2 = reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk_2}) + url_1 = ( + reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk_1}) + + f"?organization_id={self.org.id}" + ) + url_2 = ( + reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk_2}) + + f"?organization_id={self.org.id}" + ) readings_1 = { "start_time": "2022-01-05 05:00:00", @@ -418,7 +555,7 @@ def test_create_two_meters_bulk_readings(self): """Test edge case to make sure that data for two different meters don't overwrite each other""" property_view = self.property_view_factory.get_property_view() url_meters = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) - url_meters += "?organization_id=" + str(self.org.id) + url_meters += f"?organization_id={self.org.id}" payload = { "type": "Electric", @@ -437,8 +574,14 @@ def test_create_two_meters_bulk_readings(self): meter_pk_2 = response.json()["id"] # create meter readings - url_1 = reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk_1}) - url_2 = reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk_2}) + url_1 = ( + reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk_1}) + + f"?organization_id={self.org.id}" + ) + url_2 = ( + reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk_2}) + + f"?organization_id={self.org.id}" + ) # write a few values to the database for values in [ @@ -482,7 +625,7 @@ def test_create_two_meters_bulk_readings(self): def test_error_with_time_aware(self): property_view = self.property_view_factory.get_property_view() - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + f"?organization_id={self.org.id}" payload = { "type": "Electric", @@ -496,8 +639,7 @@ def test_error_with_time_aware(self): # create meter readings url = ( reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) # write a few values to the database @@ -532,7 +674,7 @@ def test_error_with_time_aware(self): def test_bulk_import(self): # create property property_view = self.property_view_factory.get_property_view() - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + f"?organization_id={self.org.id}" payload = { "type": "Electric", @@ -545,8 +687,7 @@ def test_bulk_import(self): url = ( reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) # prepare the data in bulk format @@ -582,7 +723,7 @@ def test_bulk_import_duplicate_dates(self): property_view = self.property_view_factory.get_property_view() url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) - url += "?organization_id=" + str(self.org.id) + url += f"?organization_id={self.org.id}" payload = { "type": "Electric", @@ -594,7 +735,7 @@ def test_bulk_import_duplicate_dates(self): meter_pk = response.json()["id"] url = reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk}) - url += "?organization_id=" + str(self.org.id) + url += f"?organization_id={self.org.id}" # prepare the data in bulk format reading1 = { @@ -623,9 +764,9 @@ def test_bulk_import_duplicate_dates(self): ) def test_delete_meter_readings(self): - # would be nice nice to make a factory out of the meter / meter reading requests + # would be nice to make a factory out of the meter / meter reading requests property_view = self.property_view_factory.get_property_view() - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": property_view.id}) + f"?organization_id={self.org.id}" payload = { "type": "Natural Gas", @@ -639,8 +780,7 @@ def test_delete_meter_readings(self): # create meter reading property-meter-readings-list url = ( reverse("api:v3:property-meter-readings-list", kwargs={"property_pk": property_view.id, "meter_pk": meter_pk}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) payload = { @@ -657,9 +797,12 @@ def test_delete_meter_readings(self): self.assertEqual(response.json()["reading"], 10) # now delete the item and verify that there are no more readings in the database - detail_url = reverse( - "api:v3:property-meter-readings-detail", - kwargs={"property_pk": property_view.id, "meter_pk": meter_pk, "pk": "2022-01-05 05:00:00"}, + detail_url = ( + reverse( + "api:v3:property-meter-readings-detail", + kwargs={"property_pk": property_view.id, "meter_pk": meter_pk, "pk": "2022-01-05T13:00:00Z"}, + ) + + f"?organization_id={self.org.id}" ) response = self.client.get(detail_url, content_type="application/json") self.assertEqual(response.status_code, 200) @@ -697,8 +840,7 @@ def test_meter_readings_list(self): def test_meter_readings_get(self): url = ( reverse("api:v3:property-meters-detail", kwargs={"property_pk": self.view.id, "pk": self.meter.id}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) # root member can @@ -714,8 +856,7 @@ def test_meter_readings_get(self): def test_meter_readings_update(self): url = ( reverse("api:v3:property-meters-detail", kwargs={"property_pk": self.view.id, "pk": self.meter.id}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) param = json.dumps({"type": "Electric", "source": "Manual Entry", "source_id": "boo"}) @@ -730,7 +871,7 @@ def test_meter_readings_update(self): assert resp.status_code == 404 def test_meter_readings_create(self): - url = reverse("api:v3:property-meters-list", kwargs={"property_pk": self.view.id}) + "?organization_id=" + str(self.org.id) + url = reverse("api:v3:property-meters-list", kwargs={"property_pk": self.view.id}) + f"?organization_id={self.org.id}" param = json.dumps({"type": "Electric", "source": "Manual Entry", "source_id": "boo"}) # root member can @@ -746,8 +887,7 @@ def test_meter_readings_create(self): def test_meter_readings_delete(self): url = ( reverse("api:v3:property-meters-detail", kwargs={"property_pk": self.view.id, "pk": self.meter.id}) - + "?organization_id=" - + str(self.org.id) + + f"?organization_id={self.org.id}" ) # child member cannot diff --git a/seed/utils/api_schema.py b/seed/utils/api_schema.py index e4c804d2b4..b6f8915c86 100644 --- a/seed/utils/api_schema.py +++ b/seed/utils/api_schema.py @@ -17,6 +17,7 @@ class AutoSchemaHelper(SwaggerAutoSchema): "integer": openapi.TYPE_INTEGER, "object": openapi.TYPE_OBJECT, "number": openapi.TYPE_NUMBER, + "datetime": openapi.TYPE_STRING, } @classmethod @@ -101,6 +102,8 @@ def schema_factory(cls, obj, **kwargs): if isinstance(obj, str): openapi_type = cls._openapi_type(obj) + if obj == "datetime": + return openapi.Schema(type=openapi_type, format=openapi.FORMAT_DATETIME, **kwargs) return openapi.Schema(type=openapi_type, **kwargs) if isinstance(obj, list): diff --git a/seed/utils/viewsets.py b/seed/utils/viewsets.py index c5f07a302f..b4e5ea37b9 100644 --- a/seed/utils/viewsets.py +++ b/seed/utils/viewsets.py @@ -92,7 +92,7 @@ class SEEDOrgReadOnlyModelViewSet( class SEEDOrgCreateUpdateModelViewSet(OrgCreateUpdateMixin, SEEDOrgModelViewSet): - """Extends SEEDModelViewset to add perform_create method to attach org. + """Extends SEEDOrgModelViewSet to add perform_create method to attach org. Provides the perform_create and update_create methods to save the Organization foreignkey relationship for models that have linked via an @@ -100,7 +100,7 @@ class SEEDOrgCreateUpdateModelViewSet(OrgCreateUpdateMixin, SEEDOrgModelViewSet) This viewset is not suitable for models using 'super_organization' or having additional foreign key relationships, such as user. Any such models - should instead extend SEEDOrgModelViewset and create perform_create + should instead extend SEEDOrgModelViewSet and create perform_create and/or perform_update overrides appropriate to the model's needs. """ diff --git a/seed/views/v3/building_files.py b/seed/views/v3/building_files.py index 8f850646ee..a3dc249586 100644 --- a/seed/views/v3/building_files.py +++ b/seed/views/v3/building_files.py @@ -41,7 +41,7 @@ def get_queryset(self): ) else: - return BuildingFile.objects.filter(pk=-1) + return BuildingFile.objects.none() def get_serializer_class(self): if self.action == "create": diff --git a/seed/views/v3/gbr_properties.py b/seed/views/v3/gbr_properties.py index 1e6376c0d9..311767400b 100644 --- a/seed/views/v3/gbr_properties.py +++ b/seed/views/v3/gbr_properties.py @@ -65,7 +65,7 @@ def get_queryset(self): ) else: - return PropertyModel.objects.filter(pk=-1) + return PropertyModel.objects.none() def create(self, request, *args, **kwargs): org_id = self.get_organization(self.request) diff --git a/seed/views/v3/meter_readings.py b/seed/views/v3/meter_readings.py index ff9c9e6fa7..044f517ca7 100644 --- a/seed/views/v3/meter_readings.py +++ b/seed/views/v3/meter_readings.py @@ -10,42 +10,115 @@ from rest_framework.parsers import FormParser, JSONParser from rest_framework.renderers import JSONRenderer +from seed.lib.superperms.orgs.decorators import has_hierarchy_access, has_perm_class from seed.models import MeterReading, PropertyView from seed.serializers.meter_readings import MeterReadingSerializer -from seed.utils.api_schema import AutoSchemaHelper -from seed.utils.viewsets import SEEDOrgModelViewSet +from seed.utils.api_schema import AutoSchemaHelper, swagger_auto_schema_org_query_param +from seed.utils.viewsets import SEEDOrgNoPatchOrOrgCreateModelViewSet +@method_decorator( + name="list", + decorator=[ + swagger_auto_schema_org_query_param, + has_perm_class("requires_viewer"), + has_hierarchy_access(property_view_id_kwarg="property_pk"), + ], +) +@method_decorator( + name="retrieve", + decorator=[ + swagger_auto_schema_org_query_param, + has_perm_class("requires_viewer"), + has_hierarchy_access(property_view_id_kwarg="property_pk"), + ], +) @method_decorator( name="create", - decorator=swagger_auto_schema( - manual_parameters=[ - AutoSchemaHelper.base_field( - name="property_pk", - location_attr="IN_PATH", - type_attr="TYPE_INTEGER", - required=True, - description="ID of the property view where the meter is associated.", - ), - AutoSchemaHelper.base_field( - name="meter_pk", - location_attr="IN_PATH", - type_attr="TYPE_INTEGER", - required=True, - description="ID of the meter to attached the meter readings.", + decorator=[ + has_perm_class("requires_member"), + has_hierarchy_access(property_view_id_kwarg="property_pk"), + swagger_auto_schema( + manual_parameters=[ + AutoSchemaHelper.query_org_id_field(), + AutoSchemaHelper.base_field( + name="property_pk", + location_attr="IN_PATH", + type_attr="TYPE_INTEGER", + required=True, + description="ID of the property view where the meter is associated.", + ), + AutoSchemaHelper.base_field( + name="meter_pk", + location_attr="IN_PATH", + type_attr="TYPE_INTEGER", + required=True, + description="ID of the meter to attached the meter readings.", + ), + ], + request_body=openapi.Schema( + type=openapi.TYPE_ARRAY, + items=AutoSchemaHelper.schema_factory( + { + "start_time": "datetime", + "end_time": "datetime", + "reading": "number", + "source_unit": "string", + "conversion_factor": "number", + }, + required=["start_time", "end_time", "reading", "source_unit", "conversion_factor"], + ), + description="Dictionary or list of dictionaries of meter readings to add.", ), - ], - request_body=openapi.Schema( - type=openapi.TYPE_ARRAY, - items=AutoSchemaHelper.schema_factory( - {"start_time": "string", "end_time": "string", "reading": "number", "source_unit": "string", "conversion_factor": "number"}, + ), + ], +) +@method_decorator( + name="update", + decorator=[ + has_perm_class("requires_member"), + has_hierarchy_access(property_view_id_kwarg="property_pk"), + swagger_auto_schema( + manual_parameters=[ + AutoSchemaHelper.query_org_id_field(), + AutoSchemaHelper.base_field( + name="property_pk", + location_attr="IN_PATH", + type_attr="TYPE_INTEGER", + required=True, + description="ID of the property view where the meter is associated.", + ), + AutoSchemaHelper.base_field( + name="meter_pk", + location_attr="IN_PATH", + type_attr="TYPE_INTEGER", + required=True, + description="ID of the meter to attached the meter readings.", + ), + ], + request_body=AutoSchemaHelper.schema_factory( + { + "start_time": "datetime", + "end_time": "datetime", + "reading": "number", + "source_unit": "string", + "conversion_factor": "number", + }, required=["start_time", "end_time", "reading", "source_unit", "conversion_factor"], ), - description="Dictionary or list of dictionaries of meter readings to add.", + description="Meter reading to update.", ), - ), + ], +) +@method_decorator( + name="destroy", + decorator=[ + swagger_auto_schema_org_query_param, + has_perm_class("requires_member"), + has_hierarchy_access(property_view_id_kwarg="property_pk"), + ], ) -class MeterReadingViewSet(SEEDOrgModelViewSet): +class MeterReadingViewSet(SEEDOrgNoPatchOrOrgCreateModelViewSet): """API endpoint for managing meters.""" serializer_class = MeterReadingSerializer @@ -53,7 +126,6 @@ class MeterReadingViewSet(SEEDOrgModelViewSet): pagination_class = None model = MeterReading parser_classes = (JSONParser, FormParser) - orgfilter = "property__organization" def get_queryset(self): # return the organization id from the request. This also check @@ -72,7 +144,7 @@ def get_queryset(self): except PropertyView.DoesNotExist: return MeterReading.objects.none() - self.property_pk = property_view.property.pk + self.property_pk = property_view.property_id # Grab the meter id meter_pk = self.kwargs.get("meter_pk", None) diff --git a/seed/views/v3/meters.py b/seed/views/v3/meters.py index 7e3ea5375b..fc45ed541e 100644 --- a/seed/views/v3/meters.py +++ b/seed/views/v3/meters.py @@ -12,13 +12,25 @@ from seed.lib.superperms.orgs.decorators import has_hierarchy_access, has_perm_class from seed.models import Meter, PropertyView from seed.serializers.meters import MeterSerializer -from seed.utils.api_schema import AutoSchemaHelper +from seed.utils.api_schema import AutoSchemaHelper, swagger_auto_schema_org_query_param from seed.utils.viewsets import SEEDOrgNoPatchOrOrgCreateModelViewSet -@method_decorator(name="list", decorator=[has_perm_class("requires_viewer"), has_hierarchy_access(property_view_id_kwarg="property_pk")]) @method_decorator( - name="retrieve", decorator=[has_perm_class("requires_viewer"), has_hierarchy_access(property_view_id_kwarg="property_pk")] + name="list", + decorator=[ + swagger_auto_schema_org_query_param, + has_perm_class("requires_viewer"), + has_hierarchy_access(property_view_id_kwarg="property_pk"), + ], +) +@method_decorator( + name="retrieve", + decorator=[ + swagger_auto_schema_org_query_param, + has_perm_class("requires_viewer"), + has_hierarchy_access(property_view_id_kwarg="property_pk"), + ], ) @method_decorator( name="create", @@ -27,6 +39,7 @@ has_hierarchy_access(property_view_id_kwarg="property_pk"), swagger_auto_schema( manual_parameters=[ + AutoSchemaHelper.query_org_id_field(), AutoSchemaHelper.base_field( name="property_pk", location_attr="IN_PATH", @@ -50,8 +63,22 @@ ), ], ) -@method_decorator(name="destroy", decorator=[has_perm_class("requires_member"), has_hierarchy_access(property_view_id_kwarg="property_pk")]) -@method_decorator(name="update", decorator=[has_perm_class("requires_member"), has_hierarchy_access(property_view_id_kwarg="property_pk")]) +@method_decorator( + name="update", + decorator=[ + swagger_auto_schema_org_query_param, + has_perm_class("requires_member"), + has_hierarchy_access(property_view_id_kwarg="property_pk"), + ], +) +@method_decorator( + name="destroy", + decorator=[ + swagger_auto_schema_org_query_param, + has_perm_class("requires_member"), + has_hierarchy_access(property_view_id_kwarg="property_pk"), + ], +) class MeterViewSet(SEEDOrgNoPatchOrOrgCreateModelViewSet): """API endpoint for managing meters.""" @@ -74,7 +101,7 @@ def get_queryset(self): return Meter.objects.none() property_view = PropertyView.objects.get(pk=property_view_pk) - self.property_pk = property_view.property.pk + self.property_pk = property_view.property_id return Meter.objects.filter(property__organization_id=org_id, property_id=self.property_pk) def perform_create(self, serializer): diff --git a/seed/views/v3/property_views.py b/seed/views/v3/property_views.py index 41c681062a..fcd28baa7e 100644 --- a/seed/views/v3/property_views.py +++ b/seed/views/v3/property_views.py @@ -13,15 +13,31 @@ from seed.lib.superperms.orgs.decorators import has_hierarchy_access, has_perm_class from seed.models import AccessLevelInstance, Organization, PropertyView from seed.serializers.properties import BriefPropertyViewSerializer -from seed.utils.api import api_endpoint_class +from seed.utils.api_schema import swagger_auto_schema_org_query_param from seed.utils.viewsets import SEEDOrgModelViewSet -@method_decorator(name="list", decorator=[has_perm_class("requires_viewer")]) -@method_decorator(name="retrieve", decorator=[has_perm_class("requires_viewer"), has_hierarchy_access(property_view_id_kwarg="pk")]) -@method_decorator(name="destroy", decorator=[has_perm_class("requires_viewer"), has_hierarchy_access(property_view_id_kwarg="pk")]) -@method_decorator(name="update", decorator=[has_perm_class("requires_viewer"), has_hierarchy_access(property_view_id_kwarg="pk")]) -@method_decorator(name="create", decorator=[has_perm_class("requires_viewer"), has_hierarchy_access(body_property_id="property_id")]) +@method_decorator(name="list", decorator=[swagger_auto_schema_org_query_param, has_perm_class("requires_viewer")]) +@method_decorator( + name="retrieve", + decorator=[swagger_auto_schema_org_query_param, has_perm_class("requires_viewer"), has_hierarchy_access(property_view_id_kwarg="pk")], +) +@method_decorator( + name="destroy", + decorator=[swagger_auto_schema_org_query_param, has_perm_class("requires_member"), has_hierarchy_access(property_view_id_kwarg="pk")], +) +@method_decorator( + name="update", + decorator=[swagger_auto_schema_org_query_param, has_perm_class("requires_member"), has_hierarchy_access(property_view_id_kwarg="pk")], +) +@method_decorator( + name="create", + decorator=[ + swagger_auto_schema_org_query_param, + has_perm_class("requires_member"), + has_hierarchy_access(body_property_id="property_id"), + ], +) class PropertyViewViewSet(SEEDOrgModelViewSet): """PropertyViews API Endpoint @@ -69,7 +85,7 @@ def get_queryset(self): ) else: - return PropertyView.objects.filter(pk=-1) + return PropertyView.objects.none() serializer_class = BriefPropertyViewSerializer pagination_class = None @@ -79,10 +95,7 @@ def get_queryset(self): data_name = "property_views" queryset = PropertyView.objects.all() - # Overwrite method to make it work - @api_endpoint_class - @has_perm_class("requires_viewer") - def create(self, request, organization_pk=None): + def create(self, request, *args, **kwargs): org_id = int(self.get_organization(request)) try: Organization.objects.get(pk=org_id)