diff --git a/seed/api/v3/urls.py b/seed/api/v3/urls.py index be4d726ba1..edd74c6122 100644 --- a/seed/api/v3/urls.py +++ b/seed/api/v3/urls.py @@ -13,6 +13,7 @@ from seed.views.v3.data_quality_checks import DataQualityCheckViewSet from seed.views.v3.data_quality_check_rules import DataQualityCheckRuleViewSet from seed.views.v3.datasets import DatasetViewSet +from seed.views.v3.geocode import GeocodeViewSet from seed.views.v3.import_files import ImportFileViewSet from seed.views.v3.measures import MeasureViewSet from seed.views.v3.labels import LabelViewSet @@ -22,7 +23,9 @@ from seed.views.v3.organizations import OrganizationViewSet from seed.views.v3.organization_users import OrganizationUserViewSet from seed.views.v3.properties import PropertyViewSet +from seed.views.v3.property_scenarios import PropertyScenarioViewSet from seed.views.v3.taxlots import TaxlotViewSet +from seed.views.v3.ubid import UbidViewSet from seed.views.v3.uploads import UploadViewSet from seed.views.v3.users import UserViewSet @@ -32,6 +35,7 @@ api_v3_router.register(r'columns', ColumnViewSet, base_name='columns') api_v3_router.register(r'cycles', CycleViewSet, base_name='cycles') api_v3_router.register(r'datasets', DatasetViewSet, base_name='datasets') +api_v3_router.register(r'geocode', GeocodeViewSet, base_name='geocode') api_v3_router.register(r'labels', LabelViewSet, base_name='labels') api_v3_router.register(r'data_quality_checks', DataQualityCheckViewSet, base_name='data_quality_checks') api_v3_router.register(r'import_files', ImportFileViewSet, base_name='import_files') @@ -40,6 +44,7 @@ api_v3_router.register(r'organizations', OrganizationViewSet, base_name='organizations') api_v3_router.register(r'properties', PropertyViewSet, base_name='properties') api_v3_router.register(r'taxlots', TaxlotViewSet, base_name='taxlots') +api_v3_router.register(r'ubid', UbidViewSet, base_name='ubid') api_v3_router.register(r'upload', UploadViewSet, base_name='upload') api_v3_router.register(r'users', UserViewSet, base_name='user') @@ -48,9 +53,15 @@ organizations_router = nested_routers.NestedSimpleRouter(api_v3_router, r'organizations', lookup='organization') organizations_router.register(r'users', OrganizationUserViewSet, base_name='organization-users') -taxlots_router = nested_routers.NestedSimpleRouter(api_v3_router, r'taxlots', lookup='taxlots') + +properties_router = nested_routers.NestedSimpleRouter(api_v3_router, r'properties', lookup='property') +properties_router.register(r'notes', NoteViewSet, base_name='property-notes') +properties_router.register(r'scenarios', PropertyScenarioViewSet, base_name='property-scenarios') + +taxlots_router = nested_routers.NestedSimpleRouter(api_v3_router, r'taxlots', lookup='taxlot') taxlots_router.register(r'notes', NoteViewSet, base_name='taxlot-notes') + urlpatterns = [ url(r'^', include(api_v3_router.urls)), url(r'^', include(data_quality_checks_router.urls)), @@ -65,5 +76,6 @@ {'inventory_type': 'taxlot'}, ), url(r'^', include(organizations_router.urls)), + url(r'^', include(properties_router.urls)), url(r'^', include(taxlots_router.urls)), ] diff --git a/seed/utils/api_schema.py b/seed/utils/api_schema.py index 095374eb10..0130c9ace1 100644 --- a/seed/utils/api_schema.py +++ b/seed/utils/api_schema.py @@ -81,6 +81,26 @@ def query_boolean_field(name, required, description): type=openapi.TYPE_BOOLEAN ) + @staticmethod + def form_string_field(name, required, description): + return openapi.Parameter( + name, + openapi.IN_FORM, + description=description, + required=required, + type=openapi.TYPE_STRING + ) + + @staticmethod + def upload_file_field(name, required, description): + return openapi.Parameter( + name, + openapi.IN_FORM, + description=description, + required=required, + type=openapi.TYPE_FILE + ) + @staticmethod def path_id_field(description): return openapi.Parameter( diff --git a/seed/views/v3/geocode.py b/seed/views/v3/geocode.py new file mode 100644 index 0000000000..1827d17f0d --- /dev/null +++ b/seed/views/v3/geocode.py @@ -0,0 +1,152 @@ +# !/usr/bin/env python +# encoding: utf-8 + +from django.db.models import Subquery +from django.http import JsonResponse + +from drf_yasg.utils import swagger_auto_schema + +from rest_framework import viewsets +from rest_framework.decorators import action + +from seed.decorators import ajax_request_class + +from seed.lib.superperms.orgs.decorators import has_perm_class + +from seed.models.properties import PropertyState, PropertyView +from seed.models.tax_lots import TaxLotState, TaxLotView +from seed.utils.api import api_endpoint_class, OrgMixin +from seed.utils.api_schema import AutoSchemaHelper +from seed.utils.geocode import geocode_buildings + + +class GeocodeViewSet(viewsets.ViewSet, OrgMixin): + + @swagger_auto_schema( + manual_parameters=[AutoSchemaHelper.query_org_id_field()], + request_body=AutoSchemaHelper.schema_factory( + { + 'property_view_ids': ['integer'], + 'taxlot_view_ids': ['integer'], + }, + description='IDs by inventory type for records to be geocoded.' + ) + ) + @api_endpoint_class + @ajax_request_class + @has_perm_class('can_modify_data') + @action(detail=False, methods=['POST']) + def geocode_by_ids(self, request): + """ + Submit a request to geocode property and tax lot records. + """ + body = dict(request.data) + org_id = self.get_organization(request) + property_view_ids = body.get('property_view_ids') + taxlot_view_ids = body.get('taxlot_view_ids') + + if property_view_ids: + property_views = PropertyView.objects.filter( + id__in=property_view_ids, + cycle__organization_id=org_id + ) + properties = PropertyState.objects.filter( + id__in=Subquery(property_views.values('state_id')) + ) + geocode_buildings(properties) + + if taxlot_view_ids: + taxlot_views = TaxLotView.objects.filter( + id__in=taxlot_view_ids, + cycle__organization_id=org_id + ) + taxlots = TaxLotState.objects.filter( + id__in=Subquery(taxlot_views.values('state_id')) + ) + geocode_buildings(taxlots) + + return JsonResponse({'status': 'success'}) + + @swagger_auto_schema( + manual_parameters=[AutoSchemaHelper.query_org_id_field()], + request_body=AutoSchemaHelper.schema_factory( + { + 'property_view_ids': ['integer'], + 'taxlot_view_ids': ['integer'], + }, + description='IDs by inventory type for records to be used in building a geocoding summary.' + ) + ) + @api_endpoint_class + @ajax_request_class + @has_perm_class('can_view_data') + @action(detail=False, methods=['POST']) + def confidence_summary(self, request): + """ + Generate a summary of geocoding confidence values for property and + tax lot records. + """ + body = dict(request.data) + org_id = self.get_organization(request) + property_view_ids = body.get('property_view_ids') + taxlot_view_ids = body.get('taxlot_view_ids') + + result = {} + + if property_view_ids: + property_views = PropertyView.objects.filter( + id__in=property_view_ids, + cycle__organization_id=org_id + ) + result["properties"] = { + 'not_geocoded': PropertyState.objects.filter( + id__in=Subquery(property_views.values('state_id')), + geocoding_confidence__isnull=True + ).count(), + 'high_confidence': PropertyState.objects.filter( + id__in=Subquery(property_views.values('state_id')), + geocoding_confidence__startswith='High' + ).count(), + 'low_confidence': PropertyState.objects.filter( + id__in=Subquery(property_views.values('state_id')), + geocoding_confidence__startswith='Low' + ).count(), + 'manual': PropertyState.objects.filter( + id__in=Subquery(property_views.values('state_id')), + geocoding_confidence='Manually geocoded (N/A)' + ).count(), + 'missing_address_components': PropertyState.objects.filter( + id__in=Subquery(property_views.values('state_id')), + geocoding_confidence='Missing address components (N/A)' + ).count(), + } + + if taxlot_view_ids: + taxlot_views = TaxLotView.objects.filter( + id__in=taxlot_view_ids, + cycle__organization_id=org_id + ) + result["tax_lots"] = { + 'not_geocoded': TaxLotState.objects.filter( + id__in=Subquery(taxlot_views.values('state_id')), + geocoding_confidence__isnull=True + ).count(), + 'high_confidence': TaxLotState.objects.filter( + id__in=Subquery(taxlot_views.values('state_id')), + geocoding_confidence__startswith='High' + ).count(), + 'low_confidence': TaxLotState.objects.filter( + id__in=Subquery(taxlot_views.values('state_id')), + geocoding_confidence__startswith='Low' + ).count(), + 'manual': TaxLotState.objects.filter( + id__in=Subquery(taxlot_views.values('state_id')), + geocoding_confidence='Manually geocoded (N/A)' + ).count(), + 'missing_address_components': TaxLotState.objects.filter( + id__in=Subquery(taxlot_views.values('state_id')), + geocoding_confidence='Missing address components (N/A)' + ).count(), + } + + return result diff --git a/seed/views/v3/notes.py b/seed/views/v3/notes.py index 9bcec4f80e..eef620383c 100644 --- a/seed/views/v3/notes.py +++ b/seed/views/v3/notes.py @@ -44,22 +44,22 @@ class NoteViewSet(SEEDOrgNoPatchOrOrgCreateModelViewSet): def get_queryset(self): # check if the request is properties or taxlots org_id = self.get_organization(self.request) - if self.kwargs.get('properties_pk', None): - return Note.objects.filter(organization_id=org_id, property_view_id=self.kwargs.get('properties_pk')) - elif self.kwargs.get('taxlots_pk', None): - return Note.objects.filter(organization_id=org_id, taxlot_view_id=self.kwargs.get('taxlots_pk')) + if self.kwargs.get('property_pk', None): + return Note.objects.filter(organization_id=org_id, property_view_id=self.kwargs.get('property_pk')) + elif self.kwargs.get('taxlot_pk', None): + return Note.objects.filter(organization_id=org_id, taxlot_view_id=self.kwargs.get('taxlot_pk')) else: return Note.objects.filter(organization_id=org_id) def perform_create(self, serializer): org_id = self.get_organization(self.request) - if self.kwargs.get('properties_pk', None): + if self.kwargs.get('property_pk', None): serializer.save( - organization_id=org_id, user=self.request.user, property_view_id=self.kwargs.get('properties_pk', None) + organization_id=org_id, user=self.request.user, property_view_id=self.kwargs.get('property_pk', None) ) - elif self.kwargs.get('taxlots_pk', None): + elif self.kwargs.get('taxlot_pk', None): serializer.save( - organization_id=org_id, user=self.request.user, taxlot_view_id=self.kwargs.get('taxlots_pk', None) + organization_id=org_id, user=self.request.user, taxlot_view_id=self.kwargs.get('taxlot_pk', None) ) else: - _log.warn("Unable to create model without a property_pk or taxlots_pk") + _log.warn("Unable to create model without a property_pk or taxlot_pk") diff --git a/seed/views/v3/properties.py b/seed/views/v3/properties.py index c79fb90030..47e7e98334 100644 --- a/seed/views/v3/properties.py +++ b/seed/views/v3/properties.py @@ -2,26 +2,29 @@ :copyright (c) 2014 - 2020, The Regents of the University of California, through Lawrence Berkeley National Laboratory (subject to receipt of any required approvals from the U.S. Department of Energy) and contributors. All rights reserved. # NOQA :author """ +import os from collections import namedtuple from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator -from django.db.models import Q -from django.db.models import Subquery -from django.http import JsonResponse +from django.db.models import Q, Subquery +from django.http import HttpResponse, JsonResponse from drf_yasg.utils import no_body, swagger_auto_schema from rest_framework import status, viewsets from rest_framework.decorators import action -from rest_framework.parsers import JSONParser +from rest_framework.parsers import JSONParser, MultiPartParser from rest_framework.renderers import JSONRenderer +from seed.building_sync.building_sync import BuildingSync from seed.data_importer.utils import usage_point_id from seed.decorators import ajax_request_class +from seed.hpxml.hpxml import HPXML from seed.lib.superperms.orgs.decorators import has_perm_class from seed.lib.superperms.orgs.models import Organization from seed.models import (AUDIT_USER_EDIT, DATA_STATE_MATCHING, MERGE_STATE_DELETE, MERGE_STATE_MERGED, MERGE_STATE_NEW, VIEW_LIST, VIEW_LIST_PROPERTY, - Column, ColumnListSetting, ColumnListSettingColumn, - Cycle, Meter, Note, Property, PropertyAuditLog, + BuildingFile, Column, ColumnListSetting, + ColumnListSettingColumn, ColumnMappingPreset, Cycle, + Meter, Note, Property, PropertyAuditLog, PropertyMeasure, PropertyState, PropertyView, Simulation) from seed.models import StatusLabel as Label @@ -30,6 +33,7 @@ apply_display_unit_preferences) from seed.serializers.properties import (PropertySerializer, PropertyStateSerializer, + PropertyViewAsStateSerializer, PropertyViewSerializer, UpdatePropertyPayloadSerializer) from seed.serializers.taxlots import TaxLotViewSerializer @@ -1010,6 +1014,217 @@ def update(self, request, pk=None): else: return JsonResponse(result, status=status.HTTP_404_NOT_FOUND) + def _get_property_view_for_property(self, pk, cycle_pk): + """ + Return a property view based on the property id and cycle + :param pk: ID of property (not property view) + :param cycle_pk: ID of the cycle + :return: dict, propety view and status + """ + try: + property_view = PropertyView.objects.select_related( + 'property', 'cycle', 'state' + ).get( + property_id=pk, + cycle_id=cycle_pk, + property__organization_id=self.request.GET['organization_id'] + ) + result = { + 'status': 'success', + 'property_view': property_view + } + except PropertyView.DoesNotExist: + result = { + 'status': 'error', + 'message': 'property view with property id {} does not exist'.format(pk) + } + except PropertyView.MultipleObjectsReturned: + result = { + 'status': 'error', + 'message': 'Multiple property views with id {}'.format(pk) + } + return result + + @swagger_auto_schema( + manual_parameters=[ + AutoSchemaHelper.query_org_id_field(), + AutoSchemaHelper.query_integer_field( + 'preset_id', + required=True, + description='ID of a BuildingSync ColumnMappingPreset' + ), + ] + ) + @has_perm_class('can_view_data') + @action(detail=True, methods=['GET']) + def building_sync(self, request, pk): + """ + Return BuildingSync representation of the property + """ + preset_pk = request.GET.get('preset_id') + org_id = self.get_organization(self.request) + try: + preset_pk = int(preset_pk) + column_mapping_preset = ColumnMappingPreset.objects.get( + pk=preset_pk, + preset_type__in=[ColumnMappingPreset.BUILDINGSYNC_DEFAULT, ColumnMappingPreset.BUILDINGSYNC_CUSTOM]) + except TypeError: + return JsonResponse({ + 'success': False, + 'message': 'Query param `preset_id` is either missing or invalid' + }, status=status.HTTP_400_BAD_REQUEST) + except ColumnMappingPreset.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': f'Cannot find a BuildingSync ColumnMappingPreset with pk={preset_pk}' + }, status=status.HTTP_400_BAD_REQUEST) + + try: + property_view = (PropertyView.objects.select_related('state') + .get(pk=pk, cycle__organization_id=org_id)) + except PropertyView.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': 'Cannot match a PropertyView with pk=%s' % pk + }, status=status.HTTP_400_BAD_REQUEST) + + bs = BuildingSync() + # Check if there is an existing BuildingSync XML file to merge + bs_file = property_view.state.building_files.order_by('created').last() + if bs_file is not None and os.path.exists(bs_file.file.path): + bs.import_file(bs_file.file.path) + + try: + xml = bs.export_using_preset(property_view.state, column_mapping_preset.mappings) + return HttpResponse(xml, content_type='application/xml') + except Exception as e: + return JsonResponse({ + 'success': False, + 'message': str(e) + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @swagger_auto_schema( + manual_parameters=[AutoSchemaHelper.query_org_id_field()] + ) + @has_perm_class('can_view_data') + @action(detail=True, methods=['GET']) + def hpxml(self, request, pk): + """ + Return HPXML representation of the property + """ + org_id = self.get_organization(self.request) + try: + property_view = (PropertyView.objects.select_related('state') + .get(pk=pk, cycle__organization_id=org_id)) + except PropertyView.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': 'Cannot match a PropertyView with pk=%s' % pk + }) + + hpxml = HPXML() + # Check if there is an existing BuildingSync XML file to merge + hpxml_file = property_view.state.building_files.last() + if hpxml_file is not None and os.path.exists(hpxml_file.file.path): + hpxml.import_file(hpxml_file.file.path) + xml = hpxml.export(property_view.state) + return HttpResponse(xml, content_type='application/xml') + else: + # create a new XML from the record, do not import existing XML + xml = hpxml.export(property_view.state) + return HttpResponse(xml, content_type='application/xml') + + @swagger_auto_schema( + manual_parameters=[ + AutoSchemaHelper.path_id_field( + description='ID of the property view to update' + ), + AutoSchemaHelper.query_org_id_field(), + AutoSchemaHelper.query_integer_field( + 'cycle_id', + required=True, + description='ID of the cycle of the property view' + ), + AutoSchemaHelper.upload_file_field( + 'file', + required=True, + description='BuildingSync file to use', + ), + AutoSchemaHelper.form_string_field( + 'file_type', + required=True, + description='Either "Unknown" or "BuildingSync"', + ), + ], + request_body=no_body, + ) + @action(detail=True, methods=['PUT'], parser_classes=(MultiPartParser,)) + @has_perm_class('can_modify_data') + def update_with_building_sync(self, request, pk): + """ + Update an existing PropertyView with a building file. Currently only supports BuildingSync. + """ + if len(request.FILES) == 0: + return JsonResponse({ + 'success': False, + 'message': "Must pass file in as a Multipart/Form post" + }) + + the_file = request.data['file'] + file_type = BuildingFile.str_to_file_type(request.data.get('file_type', 'Unknown')) + organization_id = request.query_params.get('organization_id', None) + cycle_pk = request.query_params.get('cycle_id', None) + org_id = self.get_organization(self.request) + + try: + cycle = Cycle.objects.get(pk=cycle_pk, organization_id=org_id) + except Cycle.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': "Cycle ID is missing or Cycle does not exist" + }, status=status.HTTP_404_NOT_FOUND) + + try: + # note that this is a "safe" query b/c we should have already returned + # if the cycle was not within the user's organization + property_view = PropertyView.objects.select_related( + 'property', 'cycle', 'state' + ).get(pk=pk, cycle_id=cycle_pk) + except PropertyView.DoesNotExist: + return JsonResponse({ + 'status': 'error', + 'message': 'property view does not exist' + }, status=status.HTTP_404_NOT_FOUND) + + p_status = False + new_pv_state = None + building_file = BuildingFile.objects.create( + file=the_file, + filename=the_file.name, + file_type=file_type, + ) + + # passing in the existing propertyview allows it to process the buildingsync file and attach it to the + # existing propertyview. + p_status, new_pv_state, new_pv_view, messages = building_file.process( + organization_id, cycle, property_view=property_view + ) + + if p_status and new_pv_state: + return JsonResponse({ + 'success': True, + 'status': 'success', + 'message': 'successfully imported file', + 'data': { + 'property_view': PropertyViewAsStateSerializer(new_pv_view).data, + }, + }) + else: + return JsonResponse({ + 'status': 'error', + 'message': "Could not process building file with messages {}".format(messages) + }, status=status.HTTP_400_BAD_REQUEST) + def diffupdate(old, new): """Returns lists of fields changed""" diff --git a/seed/views/v3/property_scenarios.py b/seed/views/v3/property_scenarios.py new file mode 100644 index 0000000000..e3aa3931ad --- /dev/null +++ b/seed/views/v3/property_scenarios.py @@ -0,0 +1,36 @@ +# !/usr/bin/env python +# encoding: utf-8 +""" +:copyright (c) 2014 - 2020, The Regents of the University of California, through Lawrence Berkeley National Laboratory (subject to receipt of any required approvals from the U.S. Department of Energy) and contributors. All rights reserved. # NOQA +:author +""" +from rest_framework.parsers import JSONParser, FormParser +from rest_framework.renderers import JSONRenderer + +from seed.models import ( + Scenario, +) +from seed.serializers.scenarios import ScenarioSerializer +from seed.utils.viewsets import ( + SEEDOrgReadOnlyModelViewSet +) + + +class PropertyScenarioViewSet(SEEDOrgReadOnlyModelViewSet): + """ + API View for Scenarios. This only includes retrieve and list for now. + """ + serializer_class = ScenarioSerializer + parser_classes = (JSONParser, FormParser,) + renderer_classes = (JSONRenderer,) + pagination_class = None + orgfilter = 'property_state__organization_id' + + def get_queryset(self): + org_id = self.get_organization(self.request) + property_view_id = self.kwargs.get('property_pk') + + return Scenario.objects.filter( + property_state__organization_id=org_id, + property_state__propertyview=property_view_id, + ).order_by('id') diff --git a/seed/views/v3/ubid.py b/seed/views/v3/ubid.py new file mode 100644 index 0000000000..76967b3ee6 --- /dev/null +++ b/seed/views/v3/ubid.py @@ -0,0 +1,143 @@ +# !/usr/bin/env python +# encoding: utf-8 + +from django.db.models import Subquery +from django.http import JsonResponse + +from drf_yasg.utils import swagger_auto_schema + +from rest_framework import viewsets +from rest_framework.decorators import action + +from seed.decorators import ajax_request_class +from seed.lib.superperms.orgs.decorators import has_perm_class +from seed.models.properties import PropertyState, PropertyView +from seed.models.tax_lots import TaxLotState, TaxLotView +from seed.utils.api import api_endpoint_class, OrgMixin +from seed.utils.api_schema import AutoSchemaHelper +from seed.utils.ubid import decode_unique_ids + + +class UbidViewSet(viewsets.ViewSet, OrgMixin): + @swagger_auto_schema( + manual_parameters=[AutoSchemaHelper.query_org_id_field()], + request_body=AutoSchemaHelper.schema_factory( + { + 'property_view_ids': ['integer'], + 'taxlot_view_ids': ['integer'], + }, + description='IDs by inventory type for records to have their UBID decoded.' + ) + ) + @api_endpoint_class + @ajax_request_class + @has_perm_class('can_modify_data') + @action(detail=False, methods=['POST']) + def decode_by_ids(self, request): + """ + Submit a request to decode UBIDs for property and tax lot records. + """ + body = dict(request.data) + org_id = self.get_organization(request) + property_view_ids = body.get('property_view_ids') + taxlot_view_ids = body.get('taxlot_view_ids') + + if property_view_ids: + property_views = PropertyView.objects.filter( + id__in=property_view_ids, + cycle__organization_id=org_id + ) + properties = PropertyState.objects.filter( + id__in=Subquery(property_views.values('state_id')) + ) + decode_unique_ids(properties) + + if taxlot_view_ids: + taxlot_views = TaxLotView.objects.filter( + id__in=taxlot_view_ids, + cycle__organization_id=org_id + ) + taxlots = TaxLotState.objects.filter( + id__in=Subquery(taxlot_views.values('state_id')) + ) + decode_unique_ids(taxlots) + + return JsonResponse({'status': 'success'}) + + @swagger_auto_schema( + manual_parameters=[AutoSchemaHelper.query_org_id_field()], + request_body=AutoSchemaHelper.schema_factory( + { + 'property_view_ids': ['integer'], + 'taxlot_view_ids': ['integer'], + }, + description='IDs by inventory type for records to be used in building a UBID decoding summary.' + ) + ) + @ajax_request_class + @has_perm_class('can_view_data') + @action(detail=False, methods=['POST']) + def decode_results(self, request): + """ + Generate a summary of populated, unpopulated, and decoded UBIDs for + property and tax lot records. + """ + body = dict(request.data) + org_id = self.get_organization(request) + + ubid_unpopulated = 0 + ubid_successfully_decoded = 0 + ubid_not_decoded = 0 + ulid_unpopulated = 0 + ulid_successfully_decoded = 0 + ulid_not_decoded = 0 + property_view_ids = body.get('property_view_ids') + taxlot_view_ids = body.get('taxlot_view_ids') + if property_view_ids: + property_views = PropertyView.objects.filter( + id__in=property_view_ids, + cycle__organization_id=org_id + ) + property_states = PropertyState.objects.filter(id__in=Subquery(property_views.values('state_id'))) + + ubid_unpopulated = property_states.filter(ubid__isnull=True).count() + ubid_successfully_decoded = property_states.filter( + ubid__isnull=False, + bounding_box__isnull=False, + centroid__isnull=False + ).count() + # for ubid_not_decoded, bounding_box could be populated from a GeoJSON import + ubid_not_decoded = property_states.filter( + ubid__isnull=False, + centroid__isnull=True + ).count() + + if taxlot_view_ids: + taxlot_views = TaxLotView.objects.filter( + id__in=taxlot_view_ids, + cycle__organization_id=org_id + ) + taxlot_states = TaxLotState.objects.filter(id__in=Subquery(taxlot_views.values('state_id'))) + + ulid_unpopulated = taxlot_states.filter(ulid__isnull=True).count() + ulid_successfully_decoded = taxlot_states.filter( + ulid__isnull=False, + bounding_box__isnull=False, + centroid__isnull=False + ).count() + # for ulid_not_decoded, bounding_box could be populated from a GeoJSON import + ulid_not_decoded = taxlot_states.filter( + ulid__isnull=False, + centroid__isnull=True + ).count() + + result = { + "ubid_unpopulated": ubid_unpopulated, + "ubid_successfully_decoded": ubid_successfully_decoded, + "ubid_not_decoded": ubid_not_decoded, + "ulid_unpopulated": ulid_unpopulated, + "ulid_successfully_decoded": ulid_successfully_decoded, + "ulid_not_decoded": ulid_not_decoded, + } + + return result