From 7925a858a092322b1f16625927765233c7fe39a9 Mon Sep 17 00:00:00 2001 From: Katherine Fleming <2205659+kflemin@users.noreply.github.com> Date: Wed, 6 Sep 2023 15:13:02 -0600 Subject: [PATCH] Add new `create_property` and `update_with_espm` endpoint (#4012) * 179d create_property endpoint * precommit * adding property id, state id, and view id to create response * Partial fixes for handling bad data * Verify property exists and belongs to org * start of new serializer and controlling extra_data fields for 179d * fix property create endpoint * Formatting * fix org_id on column views * add report_format to PM download so spreadsheet can be downloaded * updating BAE and small tweak since assets are now typed * update BAE processing in reader * updating BAE to v0.1.14 * remove uneeded view * Add ESPM download for single property (via API) (#4158) * add single property download method * add viewset for downloading single espm report * fix BAE migration and update swagger schema * rename pm report single to download * update lokalise notes and pull from latest from website * initial commit with a new update with ESPM function. need to get the backend method finished * enable udpate_with_espm * add test and column mapping profile helper to load in the data * fix example file that was downloaded from espm * fix comment formats * file paths * style espm modal and fix both upload methods * put tmp files into the projects media dir * add column mapping profile param to update_with_espm * put tmp files into the projects media dir * add status to dresponse for pyseed * do not save in media_root * precommit * allow different urls for better and at during testing * return the mapping profile object when creating from file * add tests to ensure read_only fields are protected * Update models.py * Update models.py * - Removed broken `property_id` option - Fixed extra_data columns to only ones that actually apply - Changed `merge_state` from 0 to 1 - Added ValueError handling * Small fixes * Fix 179d ESPM update endpoint to run in foreground with Celery enabled (#4211) * Fixed bae object accessors * create two new methods for running save_raw and map to the foreground * fix type --------- Co-authored-by: Alex Swindler --------- Co-authored-by: Nicholas Long <1907354+nllong@users.noreply.github.com> Co-authored-by: Alex Swindler Co-authored-by: Nicholas Long --- .cspell/custom-dictionary-workspace.txt | 44 ++- .gitignore | 1 + docs/source/translation.rst | 11 +- locale/en_US/LC_MESSAGES/django.mo | Bin 111052 -> 112522 bytes locale/en_US/LC_MESSAGES/django.po | 42 +++ locale/fr_CA/LC_MESSAGES/django.mo | Bin 121740 -> 123440 bytes locale/fr_CA/LC_MESSAGES/django.po | 42 +++ lokalise.yml.example | 2 +- requirements/base.txt | 2 +- seed/building_sync/building_sync.py | 6 +- seed/data_importer/models.py | 5 + seed/data_importer/tasks.py | 109 +++++- seed/lib/xml_mapping/mapper.py | 20 +- seed/lib/xml_mapping/reader.py | 6 +- seed/models/column_mapping_profiles.py | 59 +++ seed/serializers/properties.py | 78 +++- .../data_upload_espm_modal_controller.js | 91 +++++ .../inventory_detail_controller.js | 37 +- seed/static/seed/js/directives/sdUploader.js | 145 ++++++++ seed/static/seed/js/seed.js | 2 + seed/static/seed/js/services/espm_service.js | 51 +++ seed/static/seed/locales/en_US.json | 19 +- seed/static/seed/locales/fr_CA.json | 27 +- .../seed/partials/data_upload_espm_modal.html | 80 ++++ .../seed/partials/inventory_detail.html | 5 + seed/static/seed/scss/style.scss | 13 + seed/templates/seed/_scripts.html | 2 + .../data/mappings/espm-single-mapping.csv | 25 ++ .../portfolio-manager-single-22482007.xlsx | Bin 0 -> 29921 bytes seed/tests/test_account_views.py | 27 +- seed/tests/test_api_mixins.py | 8 +- seed/tests/test_portfoliomanager.py | 87 ++++- seed/tests/test_property_views.py | 162 ++++++++- seed/tests/util.py | 8 +- seed/utils/api_schema.py | 1 + seed/views/v3/analyses.py | 20 +- seed/views/v3/columns.py | 5 +- seed/views/v3/portfolio_manager.py | 208 +++++++++-- seed/views/v3/properties.py | 341 ++++++++++++++++++ 39 files changed, 1697 insertions(+), 94 deletions(-) create mode 100644 seed/static/seed/js/controllers/data_upload_espm_modal_controller.js create mode 100644 seed/static/seed/js/services/espm_service.js create mode 100644 seed/static/seed/partials/data_upload_espm_modal.html create mode 100644 seed/tests/data/mappings/espm-single-mapping.csv create mode 100644 seed/tests/data/portfolio-manager-single-22482007.xlsx diff --git a/.cspell/custom-dictionary-workspace.txt b/.cspell/custom-dictionary-workspace.txt index 058b0ee7c4..c03f67d8f4 100644 --- a/.cspell/custom-dictionary-workspace.txt +++ b/.cspell/custom-dictionary-workspace.txt @@ -7,6 +7,7 @@ afterwards ajax Aleck Amongst +analysispropertyview Andriy angularjs api @@ -26,8 +27,10 @@ AWS backend backends badpass +basestring bashrc bedes +biffh Bool boolean BrowserDefinition @@ -36,7 +39,7 @@ bsyncr buildingsnapshot BuildingSnapshot BuildingSnapshots -BUILDINGSYNC +buildingsync bytestring calendarize canonicalbuilding @@ -55,6 +58,8 @@ cli cmp codebase collectstatic +columnlistprofile +columnmappingprofile comparators concat cond @@ -66,8 +71,11 @@ coparents crlf css csv +csvfile +csvreader Ctrl customizable +datacoercions dataset datasets datasource @@ -81,6 +89,7 @@ dest dev dict dicts +diffupdate django Django docker0 @@ -91,6 +100,7 @@ edgecase energystar enums env +ESPM eui eula fieldname @@ -111,6 +121,7 @@ geocode geocoded geocoding geocodings +geomodels getattr getenv getitem @@ -118,12 +129,14 @@ gis Github Google graphviz +greenbutton gte Gunter Gzip hardcoded Homebrew hotfix +hpxml href html iand @@ -140,11 +153,13 @@ iterable Iterable iteritems js +JSESSIONID jshint json JsonField JSONField kBtu +klass kubectl Kubectl Kubernetes @@ -170,9 +185,12 @@ mappable mapquest mcm metadata +meterdata +meterreading middleware MIDDLEWARE mixin +mmbtu ModelSerializer multipart Multipart @@ -186,12 +204,16 @@ nginx nlong nodejs noncanonicalprojectbuildings +nondefault nones noqa npm +nrows num Octant +officedocument onload +openxmlformats OperationalError OrderedDict OrderedDicts @@ -221,12 +243,14 @@ PrimaryKeyRelatedField projectbuilding propertystate propertyview +prprty py Pyright pytype pytz qqfile qs +quantityfield queryset querysets readthedocs @@ -247,26 +271,33 @@ scalable seeddb seedorg seeduser +selfvars sendmail serializable serializer serializers +servlet setUp setUpClass sha signup +spreadsheetml +springframework sqft Starke statuslabel str +strcmp strftime subclasses subdirectory Submodules +submonthly suborg Subpackages subtask sudo +superperms superset TableRows tastypie @@ -276,6 +307,7 @@ taxlots TaxLots taxlotstate taxlotview +taxview td tearDown templatetags @@ -305,13 +337,18 @@ Uncomment ungeocoded unicode unittest +unlinkable unmatch unmatching +unmerge unmerges +unpair unresolvable untracked uom uploader +uploaderfunc +ureg uri url urllist @@ -338,9 +375,14 @@ wildcards workflow wsgi xlarge +xlrd +xlsxwriter xml xmltodict xpath XPath XPATH +xpaths +XSLX +yasg yml diff --git a/.gitignore b/.gitignore index 6c49aab7ea..166547d8dc 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ seed/data_importer/tests/data/tmp_* seed/data_importer/tests/data/~* seed/tests/api/api_test_user.json seed/building_sync/tests/data/test_file.xml +seed/tests/output test.sqlite # Ignore all protractor coverage diff --git a/docs/source/translation.rst b/docs/source/translation.rst index e9236b3d94..b40ae3886b 100644 --- a/docs/source/translation.rst +++ b/docs/source/translation.rst @@ -3,7 +3,14 @@ Translating SEED 1. Update translations on `lokalise`_. -2. Copy lokalise.cfg.example to lokalise.cfg. Update API token. +2. Copy lokalise.yml.example to lokalise.yml. Update API token. + +3. Install lokalise locally + + .. code:: bash + + brew tap lokalise/cli-2 + brew install lokalise2 3. Run scripts if you have Lokalise CLI installed. If not, see scripts for manual steps. @@ -76,6 +83,6 @@ Compare:

{$:: inventory_type == 'taxlots' ? translations['INCLUDE_SHARED_TAXLOTS'] : - translations['INCLUDE_SHARED + translations['INCLUDE_SHARED'] .. _lokalise: https://lokalise.com/project/3537487659ca9b1dce98a7.36378626/?view=multi diff --git a/locale/en_US/LC_MESSAGES/django.mo b/locale/en_US/LC_MESSAGES/django.mo index adfb1f8cd640e8deca30ead4fcd2d89af051d2b5..c91468bfccf1a76581acd0fa726437f066f8d867 100644 GIT binary patch delta 28198 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 A@Bjb+ diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index 260b658ad4..2ca5de7e11 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -282,6 +282,9 @@ msgstr "Audit Template Organization Token" msgid "Audit Template Password" msgstr "Audit Template Password" +msgid "Audit Template Upload Results" +msgstr "Audit Template Upload Results" + #: seed/models/models.py:136 msgid "Auditing" msgstr "Auditing" @@ -570,6 +573,9 @@ msgstr "Configuration" msgid "Confirm" msgstr "Confirm" +msgid "Confirm Audit Template Building Import?" +msgstr "Confirm Audit Template Building Import?" + msgid "Confirm Save Mappings?" msgstr "Confirm Save Mappings?" @@ -991,6 +997,15 @@ msgstr "----Choose energy type----" msgid "ENERGY_UNIT_DISPLAY_CHOICE_PLACEHOLDER" msgstr "----Change display unit----" +msgid "ESPM Password" +msgstr "ESPM Password" + +msgid "ESPM Property ID" +msgstr "ESPM Property ID" + +msgid "ESPM Username" +msgstr "ESPM Username" + msgid "EUI" msgstr "EUI" @@ -1144,6 +1159,9 @@ msgstr "File types supported: .csv, .xls, ej@pVyf)EKAW;tr_y=srDy|p$aR_$3;6s^&s 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 dY(ILybJ2h1_LH6rGt-~)?AadYv?nC+{{S.csv, .xls dict: + """This method is used to map data synchronously and intended for only mapping a + small set of data. It is used in the Update with ESPM workflow, which runs on a + single property. Further, this method is a copy of the `map_data` method but simplified + to run only in the foreground. + + Args: + import_file_id (int): Database ID of the import file record + + Returns: + dict: Result of the progress data. + """ + import_file = ImportFile.objects.get(pk=import_file_id) + + # create a key, but this is just used to communicate the result + progress_data = ProgressData(func_name='map_data_only', unique_id=import_file_id) + progress_data.delete() + + # Check for duplicate column headers + column_headers = import_file.first_row_columns or [] + duplicate_tracker: dict = collections.defaultdict(lambda: 0) + for header in column_headers: + duplicate_tracker[header] += 1 + if duplicate_tracker[header] > 1: + raise Exception("Duplicate column found in file: %s" % (header)) + + source_type = SEED_DATA_SOURCES_MAPPING.get(import_file.source_type, ASSESSED_RAW) + + qs = PropertyState.objects.filter( + import_file=import_file, + source_type=source_type, + data_state=DATA_STATE_IMPORT, + ).only('id').iterator() + + # This version of the `map_data` should only be run when the data + # set is reasonably small because it will block operations and prevent + # reporting status updates. + id_chunks = [[obj.id for obj in chunk] for chunk in batch(qs, 100)] + for ids in id_chunks: + map_row_chunk(ids, import_file_id, source_type, progress_data.key) + + finish_mapping(import_file_id, True, progress_data.key) + + return progress_data.result() + + def map_data(import_file_id, remap=False, mark_as_done=True): """ Map data task. By default this method will run through the mapping and mark it as complete. @@ -603,7 +649,7 @@ def map_data(import_file_id, remap=False, mark_as_done=True): :param remap: bool, if remapping, then delete previous objects from the database :param mark_as_done: bool, if skip review then the mapping_done flag will be set to true at the end. - :return: JSON + :return: dict """ import_file = ImportFile.objects.get(pk=import_file_id) @@ -1256,6 +1302,67 @@ def _save_raw_data_create_tasks(file_pk, progress_key): return chord(tasks, interval=15)(finish_raw_save.s(file_pk, progress_data.key)) +def save_raw_espm_data_synchronous(file_pk: int) -> dict: + """This method is a one-off method for saving ESPM the raw data synchronously. This + is needed for the ESPM update method that runs on a single property. The `save_raw_data` + method is not used because it is asynchronous and the pieces of that method were + copied here. Technically, this method will work with a CSV or XLSX spreadsheet too, + but was only intended for ESPM. + + Args: + file_pk (int): Import file ID to import + + Returns: + dict: returns the result of the progress data + """ + progress_data = ProgressData(func_name='save_raw_data_synchronous', unique_id=file_pk) + try: + # Go get the tasks that need to be created, then call them in the chord here. + import_file = ImportFile.objects.get(pk=file_pk) + if import_file.raw_save_done: + return progress_data.finish_with_warning('Raw data already saved') + + try: + parser = reader.MCMParser(import_file.local_file) + except Exception as e: + _log.debug(f'Error reading XLSX file: {str(e)}') + return progress_data.finish_with_error('Failed to parse XLSX file. Please review your import file - all headers should be present and non-numeric.') + + import_file.has_generated_headers = False + if hasattr(parser, 'has_generated_headers'): + import_file.has_generated_headers = parser.has_generated_headers + + cache_first_rows(import_file, parser) + import_file.num_rows = 0 + import_file.num_columns = parser.num_columns() + + chunks = [] + for batch_chunk in batch(parser.data, 100): + import_file.num_rows += len(batch_chunk) + chunks.append(batch_chunk) + import_file.save() + + progress_data.total = len(chunks) + progress_data.save() + + # Save the raw data chunks. This should only happen + # on a small amount of data since it is running in the foreground + for chunk in chunks: + _save_raw_data_chunk(chunk, file_pk, progress_data.key) + + finish_raw_save(file_pk, progress_data.key) + except Error as e: + progress_data.finish_with_error('File Content Error: ' + str(e), traceback.format_exc()) + except KeyError as e: + progress_data.finish_with_error('Invalid Column Name: "' + str(e) + '"', traceback.format_exc()) + except TypeError: + progress_data.finish_with_error('TypeError Exception', traceback.format_exc()) + except Exception as e: + progress_data.finish_with_error('Unhandled Error: ' + str(e), traceback.format_exc()) + + return progress_data.result() + + def save_raw_data(file_pk): """ Simply report to the user that we have queued up the save_run_data to run. This is the entry diff --git a/seed/lib/xml_mapping/mapper.py b/seed/lib/xml_mapping/mapper.py index c50db4b23c..a07bad8413 100644 --- a/seed/lib/xml_mapping/mapper.py +++ b/seed/lib/xml_mapping/mapper.py @@ -56,20 +56,24 @@ def get_bae_mappings(): # units field name is the same with " Units" appended. bsync_assets = BAE.get_default_asset_defs() - for item in bsync_assets: + for asset in bsync_assets: + if isinstance(asset, dict): + asset_type, export_name, export_units = asset['type'], asset['export_name'], asset['export_units'] + else: + asset_type, export_name, export_units = asset.type, asset.export_name, asset.export_units - if item['type'] == 'sqft': + if asset_type == 'sqft': # these types need 2 different entries: 1 for "primary" and 1 for "secondary" for i in ['Primary', 'Secondary']: - results.append(make_bae_hash(i + ' ' + item['export_name'])) - if 'export_units' in item and item['export_units'] is True: + results.append(make_bae_hash(i + ' ' + export_name)) + if export_units is True: # also export units field - results.append(make_bae_hash(i + ' ' + item['export_name'] + " Units")) + results.append(make_bae_hash(i + ' ' + export_name + " Units")) else: - results.append(make_bae_hash(item['export_name'])) - if 'export_units' in item and item['export_units'] is True: - results.append(make_bae_hash(item['export_name'] + " Units")) + results.append(make_bae_hash(export_name)) + if export_units is True: + results.append(make_bae_hash(export_name + " Units")) return results diff --git a/seed/lib/xml_mapping/reader.py b/seed/lib/xml_mapping/reader.py index 89407ef5c4..e16f18ced2 100644 --- a/seed/lib/xml_mapping/reader.py +++ b/seed/lib/xml_mapping/reader.py @@ -64,10 +64,10 @@ def _add_property_to_data(self, bsync_file, file_name): # add to data and column headers for item in assets: - property_[item['name']] = item['value'] + property_[item.name] = item.value # only append if not already there (when processing a zip of xmls) - if item['name'] not in self.headers: - self.headers.append(item['name']) + if item.name not in self.headers: + self.headers.append(item.name) # When importing zip files, we need to be able to determine which .xml file # a certain PropertyState came from (because of the linked BuildingFile model). diff --git a/seed/models/column_mapping_profiles.py b/seed/models/column_mapping_profiles.py index f83e2343c9..95b8fe4d46 100644 --- a/seed/models/column_mapping_profiles.py +++ b/seed/models/column_mapping_profiles.py @@ -4,6 +4,9 @@ SEED Platform (TM), Copyright (c) Alliance for Sustainable Energy, LLC, and other contributors. See also https://github.com/seed-platform/seed/main/LICENSE.md """ +import csv +import os + from django.db import models from seed.lib.superperms.orgs.models import Organization @@ -23,6 +26,9 @@ class ColumnMappingProfile(models.Model): name = models.CharField(max_length=255, blank=False) mappings = models.JSONField(default=dict, blank=True) + # TODO: Need to verify that we want ManyToMany here. This might be needed for + # the BuildingSync related profiles, but the dev database appears to just + # have one org per profile. organizations = models.ManyToManyField(Organization) created = models.DateTimeField(auto_now_add=True) @@ -44,3 +50,56 @@ def get_profile_type(cls, profile_type): if profile_type in types_dict: return types_dict[profile_type] raise Exception(f'Invalid profile type "{profile_type}"') + + @classmethod + def create_from_file(cls, filename: str, org: Organization, profile_name: str, profile_type: int = NORMAL, overwrite_if_exists: bool = False): + """Generate a ColumnMappingProfile from a set of mappings in a file. The format of the file + is slightly different from the Column.create_mappings_from_file, but is the same format as + the file that you download from the column mappings page within SEED. + + Args: + filename (str): path to the file to create the mappings from. + org (Organization): Instance object of the organization + profile_name (str): Name of the new profile to create + profile_type (int, optional): Type of profile, will be NORMAL for most cases. Defaults to NORMAL. + overwrite_if_exists (bool, optional): If the mapping exists, then overwrite. Defaults to False. + + Raises: + Exception: If the file does not exist, mappings are empty, or the profile already exists and overwrite_if_exists is False. + """ + mappings = [] + if os.path.isfile(filename): + with open(filename, 'r', newline=None) as csvfile: + csvreader = csv.reader(csvfile) + next(csvreader) # skip header + for row in csvreader: + data = { + "from_field": row[0], + "from_units": row[1], + "to_table_name": row[2], + "to_field": row[3], + } + mappings.append(data) + else: + raise Exception(f"Mapping file does not exist: {filename}") + + if len(mappings) == 0: + raise Exception(f"No mappings in file: {filename}") + + # Because this object has a many to many on orgs (which I argue shouldn't), then + # first, get all the org's mapping profiles + profiles = org.columnmappingprofile_set.all() + + # second, get or create the profile now that we are only seeing my 'orgs' profiles + profile, created = profiles.get_or_create(name=profile_name, profile_type=profile_type) + if not created and not overwrite_if_exists: + raise Exception(f"ColumnMappingProfile already exists, not overwriting: {profile_name}") + + # Do I need to confirm that the mappings are defined in the Columns world? + profile.mappings = mappings + profile.save() + + # make sure that it is added to the org + org.columnmappingprofile_set.add(profile) + + return profile diff --git a/seed/serializers/properties.py b/seed/serializers/properties.py index 9dfec58c7c..985388f275 100644 --- a/seed/serializers/properties.py +++ b/seed/serializers/properties.py @@ -212,6 +212,83 @@ def to_representation(self, data): return result +class PropertyStatePromoteWritableSerializer(serializers.ModelSerializer): + """ + Used by Property create which takes in a state and promotes it to a PropertyView + Organization_id is set in view (not passed in directly by user) + """ + extra_data = serializers.JSONField(required=False) + measures = PropertyMeasureSerializer(source='propertymeasure_set', many=True, read_only=True) + scenarios = ScenarioSerializer(many=True, read_only=True) + files = BuildingFileSerializer(source='building_files', many=True, read_only=True) + + # to support the old state serializer method with the PROPERTY_STATE_FIELDS variables + import_file_id = serializers.IntegerField(allow_null=True, read_only=True) + organization_id = serializers.IntegerField() + + # read-only core fields + id = serializers.IntegerField(read_only=True) + data_state = serializers.IntegerField(read_only=True) + merge_state = serializers.IntegerField(allow_null=True, read_only=True) + source_type = serializers.IntegerField(allow_null=True, read_only=True) + hash_object = serializers.CharField(allow_null=True, read_only=True) + lot_number = serializers.CharField(allow_null=True, read_only=True) + normalized_address = serializers.CharField(allow_null=True, read_only=True) + created = serializers.DateTimeField(read_only=True) + updated = serializers.DateTimeField(read_only=True) + # read-only geo fields + bounding_box = serializers.CharField(allow_null=True, read_only=True) + centroid = serializers.CharField(allow_null=True, read_only=True) + geocoded_address = serializers.CharField(allow_null=True, read_only=True) + geocoding_confidence = serializers.CharField(allow_null=True, read_only=True) + geocoded_city = serializers.CharField(allow_null=True, read_only=True) + geocoded_county = serializers.CharField(allow_null=True, read_only=True) + geocoded_country = serializers.CharField(allow_null=True, read_only=True) + geocoded_neighborhood = serializers.CharField(allow_null=True, read_only=True) + geocoded_state = serializers.CharField(allow_null=True, read_only=True) + geocoded_postal_code = serializers.CharField(allow_null=True, read_only=True) + geocoded_side_of_street = serializers.CharField(allow_null=True, read_only=True) + long_lat = serializers.CharField(allow_null=True, read_only=True) + + # support naive datetime objects + generation_date = serializers.DateTimeField('%Y-%m-%dT%H:%M:%S', allow_null=True, required=False) + recent_sale_date = serializers.DateTimeField('%Y-%m-%dT%H:%M:%S', allow_null=True, required=False) + release_date = serializers.DateTimeField('%Y-%m-%dT%H:%M:%S', allow_null=True, required=False) + + # support the pint objects + conditioned_floor_area = PintQuantitySerializerField(allow_null=True, required=False) + gross_floor_area = PintQuantitySerializerField(allow_null=True, required=False) + occupied_floor_area = PintQuantitySerializerField(allow_null=True, required=False) + site_eui = PintQuantitySerializerField(allow_null=True, required=False) + site_eui_modeled = PintQuantitySerializerField(allow_null=True, required=False) + source_eui_weather_normalized = PintQuantitySerializerField(allow_null=True, required=False) + source_eui = PintQuantitySerializerField(allow_null=True, required=False) + source_eui_modeled = PintQuantitySerializerField(allow_null=True, required=False) + site_eui_weather_normalized = PintQuantitySerializerField(allow_null=True, required=False) + total_ghg_emissions = PintQuantitySerializerField(allow_null=True, required=False) + total_marginal_ghg_emissions = PintQuantitySerializerField(allow_null=True, required=False) + total_ghg_emissions_intensity = PintQuantitySerializerField(allow_null=True, required=False) + total_marginal_ghg_emissions_intensity = PintQuantitySerializerField(allow_null=True, required=False) + + # old fields that are no longer used and should not be updated + conditioned_floor_area_orig = serializers.FloatField(allow_null=True, read_only=True) + gross_floor_area_orig = serializers.FloatField(allow_null=True, read_only=True) + occupied_floor_area_orig = serializers.FloatField(allow_null=True, read_only=True) + site_eui_orig = serializers.FloatField(allow_null=True, read_only=True) + site_eui_modeled_orig = serializers.FloatField(allow_null=True, read_only=True) + site_eui_weather_normalized_orig = serializers.FloatField(allow_null=True, read_only=True) + source_eui_orig = serializers.FloatField(allow_null=True, read_only=True) + source_eui_weather_normalized_orig = serializers.FloatField(allow_null=True, read_only=True) + source_eui_modeled_orig = serializers.FloatField(allow_null=True, read_only=True) + + class Meta: + fields = '__all__' + model = PropertyState + extra_kwargs = { + 'organization': {'read_only': True} + } + + class PropertyStateWritableSerializer(serializers.ModelSerializer): """ Used by PropertyViewAsState as a nested serializer @@ -229,7 +306,6 @@ class PropertyStateWritableSerializer(serializers.ModelSerializer): import_file_id = serializers.IntegerField(allow_null=True, read_only=True) organization_id = serializers.IntegerField(read_only=True) - # support naive datetime objects # support naive datetime objects generation_date = serializers.DateTimeField('%Y-%m-%dT%H:%M:%S', allow_null=True, required=False) recent_sale_date = serializers.DateTimeField('%Y-%m-%dT%H:%M:%S', allow_null=True, required=False) diff --git a/seed/static/seed/js/controllers/data_upload_espm_modal_controller.js b/seed/static/seed/js/controllers/data_upload_espm_modal_controller.js new file mode 100644 index 0000000000..f0159ae7b5 --- /dev/null +++ b/seed/static/seed/js/controllers/data_upload_espm_modal_controller.js @@ -0,0 +1,91 @@ +/** + * SEED Platform (TM), Copyright (c) Alliance for Sustainable Energy, LLC, and other contributors. + * See also https://github.com/seed-platform/seed/main/LICENSE.md + */ +angular.module('BE.seed.controller.data_upload_espm_modal', []) + .controller('data_upload_espm_modal_controller', [ + '$scope', + '$uibModalInstance', + 'spinner_utility', + 'organization', + 'cycle_id', + 'upload_from_file', + 'espm_service', + 'view_id', + 'pm_property_id', + 'column_mapping_profiles', + function ( + $scope, + $uibModalInstance, + spinner_utility, + organization, + cycle_id, + upload_from_file, + espm_service, + view_id, + pm_property_id, + column_mapping_profiles + ) { + $scope.organization = organization; + $scope.view_id = view_id; + $scope.cycle_id = cycle_id; + $scope.upload_from_file = upload_from_file; + $scope.error = ''; + $scope.busy = false; + $scope.mapping_profiles = column_mapping_profiles; + let profile = $scope.mapping_profiles.length ? $scope.mapping_profiles[0].id : null; + + $scope.fields = { + pm_property_id: pm_property_id, + espm_username: '', + espm_password: '', + mapping_profile: profile + }; + + // password field + $scope.secret = 'password'; + $scope.toggle_secret = function () { + $scope.secret = ($scope.secret == 'password') ? 'text' : 'password'; + }; + + $scope.upload_from_file_and_close = function (event_message, file, progress) { + $scope.close(); + $scope.upload_from_file(event_message, file, progress); + }; + + $scope.confirm_import = function () { + if (!$scope.fields.pm_property_id) { + $scope.error = "An ESPM Property ID is required."; + } else { + $scope.submit_request(); + } + }; + + $scope.submit_request = function () { + $scope.error = ''; + $scope.busy = true; + spinner_utility.show(); + return espm_service.get_espm_building_xlsx($scope.organization.id, $scope.fields.pm_property_id, $scope.fields.espm_username, $scope.fields.espm_password).then(file_result => { + spinner_utility.hide(); + if (typeof (result) == 'object' && !result.success) { + $scope.error = 'Error: ' + result.message; + $scope.busy = false; + } else { + return espm_service.update_building_with_espm_xlsx($scope.organization.id, $scope.cycle_id, $scope.view_id, $scope.fields.mapping_profile, file_result).then(result => { + if (typeof (result) == 'object' && !result.success) { + $scope.error = 'Error: ' + result.message; + $scope.busy = false; + } else { + $scope.close(); + $scope.upload_from_file('upload_complete', null, null) + $scope.busy = false; + } + }); + } + }); + }; + + $scope.close = function () { + $uibModalInstance.dismiss(); + }; + }]); diff --git a/seed/static/seed/js/controllers/inventory_detail_controller.js b/seed/static/seed/js/controllers/inventory_detail_controller.js index 70176820fb..64bb8072e0 100644 --- a/seed/static/seed/js/controllers/inventory_detail_controller.js +++ b/seed/static/seed/js/controllers/inventory_detail_controller.js @@ -36,7 +36,6 @@ angular.module('BE.seed.controller.inventory_detail', []) 'current_profile', 'labels_payload', 'organization_payload', - 'audit_template_service', 'cycle_service', 'simple_modal_service', 'property_measure_service', @@ -74,7 +73,6 @@ angular.module('BE.seed.controller.inventory_detail', []) current_profile, labels_payload, organization_payload, - audit_template_service, cycle_service, simple_modal_service, property_measure_service, @@ -82,6 +80,7 @@ angular.module('BE.seed.controller.inventory_detail', []) ) { $scope.inventory_type = $stateParams.inventory_type; $scope.organization = organization_payload.organization; + // WARNING: $scope.org is used by "child" controller - analysis_details_controller $scope.org = {id: organization_payload.organization.id}; $scope.static_url = urls.static_url; @@ -118,6 +117,7 @@ angular.module('BE.seed.controller.inventory_detail', []) return !_.isEmpty(label.is_applied); }); $scope.audit_template_building_id = inventory_payload.state.audit_template_building_id; + $scope.pm_property_id = inventory_payload.state.pm_property_id; /** See service for structure of returned payload */ $scope.historical_items = inventory_payload.history; @@ -655,6 +655,33 @@ angular.module('BE.seed.controller.inventory_detail', []) }); }; + $scope.open_data_upload_espm_modal = function () { + var modalInstance = $uibModal.open({ + templateUrl: urls.static_url + 'seed/partials/data_upload_espm_modal.html', + controller: 'data_upload_espm_modal_controller', + resolve: { + pm_property_id: () => $scope.pm_property_id, + organization: () => $scope.organization, + cycle_id: () => $scope.cycle.id, + upload_from_file: () => $scope.uploaderfunc, + view_id: () => $stateParams.view_id, + column_mapping_profiles: [ + 'column_mappings_service', + function ( + column_mappings_service + ) { + return column_mappings_service.get_column_mapping_profiles_for_org( + $scope.organization.id, [] + ).then(function (response) { + return response.data; + }); + }] + } + }); + modalInstance.result.then(function () { + }); + }; + $scope.export_building_sync = function () { var modalInstance = $uibModal.open({ templateUrl: urls.static_url + 'seed/partials/export_buildingsync_modal.html', @@ -792,6 +819,7 @@ angular.module('BE.seed.controller.inventory_detail', []) $scope.uploader = { invalid_xml_extension_alert: false, + invalid_xlsx_extension_alert: false, in_progress: false, progress: 0, complete: false, @@ -804,9 +832,14 @@ angular.module('BE.seed.controller.inventory_detail', []) $scope.uploader.invalid_xml_extension_alert = true; break; + case 'invalid_extension': + $scope.uploader.invalid_xlsx_extension_alert = true; + break; + case 'upload_submitted': $scope.uploader.filename = file.filename; $scope.uploader.invalid_xml_extension_alert = false; + $scope.uploader.invalid_xlsx_extension_alert = false; $scope.uploader.in_progress = true; $scope.uploader.status_message = 'uploading file'; break; diff --git a/seed/static/seed/js/directives/sdUploader.js b/seed/static/seed/js/directives/sdUploader.js index dac67ed207..23b25af373 100644 --- a/seed/static/seed/js/directives/sdUploader.js +++ b/seed/static/seed/js/directives/sdUploader.js @@ -303,6 +303,148 @@ var makeBuildingSyncUpdater = function (scope, element, allowed_extensions) { return uploader; }; +var makeESPMUpdater = function (scope, element, allowed_extensions) { + var uploader = new qq.FineUploaderBasic({ + button: element[0], + request: { + method: 'PUT', + endpoint: '/api/v3/properties/' + scope.importrecord + '/update_with_espm/?cycle_id=' + scope.cycleId + '&organization_id=' + scope.organizationId + '&mapping_profile_id=' + scope.mappingProfileId, + inputName: 'file', + paramsInBody: true, + forceMultipart: true, + customHeaders: { + 'X-CSRFToken': BE.csrftoken + }, + params: { + } + }, + validation: { + allowedExtensions: allowed_extensions + }, + text: { + fileInputTitle: '', + uploadButton: scope.buttontext + }, + retry: { + enableAuto: false + }, + iframeSupport: { + localBlankPathPage: '/success.html' + }, + /** + * multiple: only allow one file to be uploaded at a time + */ + multiple: false, + maxConnections: 20, + callbacks: { + /** + * onSubmitted: overloaded callback that calls the callback defined + * in the element attribute. Passes as arguments to the callback + * a message indicating upload has started, "upload_submitted", and + * the filename. + */ + onSubmitted: function (id, fileName) { + scope.eventfunc({ + message: 'upload_submitted', + file: { + filename: fileName, + source_type: scope.sourcetype + } + }); + var params = { + csrf_token: BE.csrftoken, + csrf_name: 'csrfmiddlewaretoken', + csrf_xname: 'X-CSRFToken', + file_type: 1, + organization_id: scope.organizationId, + cycle_id: scope.cycleId + }; + + uploader.setParams(params); + }, + /** + * onComplete: overloaded callback that calls the callback defined + * in the element attribute unless the upload failed, which will + * fire a window alert. Passes as arguments to the callback + * a message indicating upload has completed, "upload_complete", and + * the filename. + */ + onComplete: function (id, fileName, responseJSON) { + + // Only handle success because error transition is in onError event handler + if (responseJSON.status === 'success') { + scope.eventfunc({ + message: 'upload_complete', + file: { + filename: fileName, + view_id: _.get(responseJSON, 'data.property_view.id'), + source_type: scope.sourcetype + } + }); + } + }, + /** + * onProgress: overloaded callback that calls the callback defined + * in the element attribute. Passes as arguments to the callback + * a message indicating upload is in progress, "upload_in_progress", + * the filename, and a progress object with two keys: loaded - the + * bytes of the file loaded, and total - the total number of bytes + * for the file. + */ + onProgress: function (id, fileName, loaded, total) { + scope.eventfunc({ + message: 'upload_in_progress', + file: { + filename: fileName, + source_type: scope.sourcetype + }, + progress: { + loaded: loaded, + total: total + } + }); + }, + /** + * onError: overloaded callback that calls the callback defined + * in the element attribute. Primarily for non-conforming files + * that return 400 from the backend and invalid file extensions. + */ + onError: function (id, fileName, errorReason, xhr) { + if (_.includes(errorReason, ' has an invalid extension.')) { + scope.eventfunc({message: 'invalid_extension'}); + return; + } + + // Ignore this error handler if the network request hasn't taken place yet (e.g., invalid file extension) + if (!xhr) { + alert(errorReason); + return; + } + + var error = errorReason; + try { + var json = JSON.parse(xhr.responseText); + if (_.has(json, 'message')) { + error = json.message; + } + } catch (e) { + // no-op + } + + scope.eventfunc({ + message: 'upload_error', + file: { + filename: fileName, + source_type: scope.sourcetype, + error: error + } + }); + } + } + }); + return uploader; +}; + /* Inventory Document Uploader for files to attach to a property */ var makeDocumentUploader = function (scope, element, allowed_extensions) { @@ -451,6 +593,8 @@ var sdUploaderFineUploader = function (scope, element/*, attrs, filename*/) { var uploader; if (scope.sourcetype === 'BuildingSyncUpdate') { uploader = makeBuildingSyncUpdater(scope, element, ['xml']); + } else if (scope.sourcetype === 'ESPMUpdate') { + uploader = makeESPMUpdater(scope, element, ['xlsx']); } else if (scope.sourcetype === 'GreenButton') { uploader = makeFileSystemUploader(scope, element, ['xml']); } else if (scope.sourcetype === 'SensorMetaData') { @@ -474,6 +618,7 @@ angular.module('sdUploader', []).directive('sdUploader', function () { eventfunc: '&', importrecord: '=', organizationId: '=', + mappingProfileId: '=?', sourceprog: '@', sourcetype: '@', sourcever: '=' diff --git a/seed/static/seed/js/seed.js b/seed/static/seed/js/seed.js index dce6dd9e85..dbbf4cd0e9 100644 --- a/seed/static/seed/js/seed.js +++ b/seed/static/seed/js/seed.js @@ -55,6 +55,7 @@ angular.module('BE.seed.controllers', [ 'BE.seed.controller.data_quality_labels_modal', 'BE.seed.controller.data_quality_modal', 'BE.seed.controller.data_upload_audit_template_modal', + 'BE.seed.controller.data_upload_espm_modal', 'BE.seed.controller.data_upload_modal', 'BE.seed.controller.data_view', 'BE.seed.controller.dataset', @@ -170,6 +171,7 @@ angular.module('BE.seed.services', [ 'BE.seed.service.data_view', 'BE.seed.service.dataset', 'BE.seed.service.derived_columns', + 'BE.seed.service.espm', 'BE.seed.service.event', 'BE.seed.service.filter_groups', 'BE.seed.service.flippers', diff --git a/seed/static/seed/js/services/espm_service.js b/seed/static/seed/js/services/espm_service.js new file mode 100644 index 0000000000..ebde12b025 --- /dev/null +++ b/seed/static/seed/js/services/espm_service.js @@ -0,0 +1,51 @@ +/** + * SEED Platform (TM), Copyright (c) Alliance for Sustainable Energy, LLC, and other contributors. + * See also https://github.com/seed-platform/seed/main/LICENSE.md + */ +angular.module('BE.seed.service.espm', []).factory('espm_service', [ + '$http', + '$log', + function ( + $http + ) { + + const get_espm_building_xlsx = function (org_id, pm_property_id, espm_username, espm_password) { + return $http.post(['/api/v3/portfolio_manager/', pm_property_id, '/download/?organization_id=', org_id].join(''), { + username: espm_username, + password: espm_password + }, { + responseType: 'arraybuffer' + }).then(function (response) { + return response.data; + }).catch(function (response) { + console.log('Could not get ESPM building from service with status:' + response.status); + return response.data; + }); + }; + + const update_building_with_espm_xlsx = function (org_id, cycle_id, property_view_id, mapping_profile, file_data) { + let body = new FormData(); + let blob = new Blob([file_data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + body.append('file', blob, ['espm_', new Date().getTime(), '.xlsx'].join('')); + let headers = {'Content-Type': undefined}; + + return $http.put([ + '/api/v3/properties/', property_view_id, '/update_with_espm/?', + 'cycle_id=', cycle_id, '&', + 'organization_id=', org_id, '&', + 'mapping_profile_id=', mapping_profile + ].join(''), body, { headers: headers }, + ).then(function (response) { + return response.data; + }).catch(function (response) { + return response.data; + }); + }; + + const analyses_factory = { + get_espm_building_xlsx: get_espm_building_xlsx, + update_building_with_espm_xlsx: update_building_with_espm_xlsx + }; + + return analyses_factory; + }]); diff --git a/seed/static/seed/locales/en_US.json b/seed/static/seed/locales/en_US.json index 24b04c47fd..3851bda4f6 100644 --- a/seed/static/seed/locales/en_US.json +++ b/seed/static/seed/locales/en_US.json @@ -88,6 +88,7 @@ "Audit Template Email": "Audit Template Email", "Audit Template Organization Token": "Audit Template Organization Token", "Audit Template Password": "Audit Template Password", + "Audit Template Upload Results": "Audit Template Upload Results", "Auditing": "Auditing", "Auto Matching": "Auto Matching", "BEFORE_GEOCODING": "Before geocoding", @@ -180,6 +181,7 @@ "Conditioned Floor Area": "Conditioned Floor Area", "Configuration": "Configuration", "Confirm": "Confirm", + "Confirm Audit Template Building Import?": "Confirm Audit Template Building Import?", "Confirm Save Mappings?": "Confirm Save Mappings?", "Confirm delete": "Confirm delete", "Confirm new password": "Confirm new password", @@ -319,6 +321,11 @@ "ENERGY STAR Score": "ENERGY STAR Score", "ENERGY_TYPE_DISPLAY_CHOICE_PLACEHOLDER": "----Choose energy type----", "ENERGY_UNIT_DISPLAY_CHOICE_PLACEHOLDER": "----Change display unit----", + "ESPM_FILE_UPLOAD": "ESPM Spreadsheet Upload (xlsx). The spreadsheet should contain a single property.", + "ESPM_IMPORT_TEXT": "Choose an EnergyStar Portfolio Manager (ESPM) data importing method below: you can either upload a property spreadsheet previously downloaded from ESPM, or you can connect to ESPM directly to access the data.", + "ESPM Password": "ESPM Password", + "ESPM Property ID": "ESPM Property ID", + "ESPM Username": "ESPM Username", "EUI": "EUI", "EXCLUDE": "EXCLUDE", "EXTRA_DATA_COL_TYPE_CHANGE": "For “extra data” fields, this allows the user to set the type, such as Text, Number, Date, etc.", @@ -369,6 +376,7 @@ "FIELD_NAMES_FOR_MATCHING": "Field names for matching", "FILE_TYPES_SUPPORTED": "File types supported: .csv<\/strong>, .xls<\/strong>, .xlsx<\/strong>, .xml<\/strong>, .zip<\/strong>, .geojson<\/strong>, and .json<\/strong>.", "Failed to delete inventory": "Failed to delete inventory", + "Fetching your buildings from Audit Template...": "Fetching your buildings from Audit Template...", "Field": "Field", "Field Name": "Field Name", "Fields that have a \"Must Contain\" or \"Must Not Contain\" Condition Check rule cannot have a \"Range\" Condition Check rule.": "Fields that have a \"Must Contain\" or \"Must Not Contain\" Condition Check rule cannot have a \"Range\" Condition Check rule.", @@ -449,6 +457,7 @@ "INVALID_DOC_FILE_EXTENSION_ALERT": "Invalid document type selected. Accepted file types are .dxf, .pdf, .idf, and .osm", "INVALID_EXTENSION_ALERT": "Sorry!<\/strong> SEED doesn't currently support that file format. Only .csv<\/strong>, .xls<\/strong>, .xlsx<\/strong>, and .xml<\/strong> files are supported.", "INVALID_GEOJSON_EXTENSION_ALERT": "Sorry!<\/strong> SEED doesn't currently support that file format. Only .geojson<\/strong> and .json<\/strong> files are supported.", + "INVALID_XLSX_EXTENSION_ALERT": "Sorry!<\/strong> SEED doesn't currently support that file format. Only .xlsx<\/strong> files are supported.", "INVALID_XML_EXTENSION_ALERT": "Sorry!<\/strong> SEED doesn't currently support that file format. Only .xml<\/strong> files are supported.", "INVALID_XML_ZIP_EXTENSION_ALERT": "Sorry!<\/strong> SEED doesn't currently support that file format. Only .xml<\/strong> and .zip<\/strong> files are supported.", "IRREVERSIBLE_OPERATION_WARNING": "This operation is irreversible.", @@ -458,7 +467,9 @@ "Ignored property duplicates within the import file": "Ignored property duplicates within the import file", "Ignored tax lot duplicates within the import file": "Ignored tax lot duplicates within the import file", "Import Access Level Instances": "Import Access Level Instances", + "Import Audit Template Buildings": "Import Audit Template Buildings", "Import Portfolio Manager Data": "Import Portfolio Manager Data", + "Import directly from ESPM": "Import directly from ESPM", "Import from Audit Template": "Import from Audit Template", "In addition, you need to specify where the field should be associated with Tax Lot data or Property data. This will affect how the data is matched and merged, as well as how it is displayed in the Inventory view.": "In addition, you need to specify where the field should be associated with Tax Lot data or Property data. This will affect how the data is matched and merged, as well as how it is displayed in the Inventory view.", "Inactive": "Inactive", @@ -894,6 +905,7 @@ "Select All": "Select All", "Select All Columns": "Select All Columns", "Select None": "Select None", + "Select Properties to Update": "Select Properties to Update", "Select a Custom Report to get started!": "Select a Custom Report to get started!", "Select a Program to get started!": "Select a Program to get started!", "Select a column to show data on this axis. Only columns with one of these data types will be listed:": "Select a column to show data on this axis. Only columns with one of these data types will be listed:", @@ -1000,6 +1012,7 @@ "There is also a link to the SEED-Platform Users forum, where you can connect with other users.": "There is also a link to the SEED-Platform Users forum, where you can connect with other users.", "There was an error loading the page": "There was an error loading the page", "This action replaces any of your current columns with the comma-delmited columns you provided. Would you like to continue?": "This action replaces any of your current columns with the comma-delmited columns you provided. Would you like to continue?", + "This action updates properties within the selected cycle with data from the Audit Template account associated with this organization. Only Properties with Audit Template Building IDs corresponding to those saved in Audit Template will be updated.": "This action updates properties within the selected cycle with data from the Audit Template account associated with this organization. Only Properties with Audit Template Building IDs corresponding to those saved in Audit Template will be updated.", "This cycle name is already taken.": "This cycle name is already taken.", "This email link is invalid.": "This email link is invalid.", "This label name is already taken.": "This label name is already taken.", @@ -1056,13 +1069,17 @@ "Update Filters": "Update Filters", "Update Salesforce": "Update Salesforce", "Update UBID": "Update UBID", + "Update with Audit Template": "Update with Audit Template", "Update with BuildingSync": "Update with BuildingSync", + "Update with ESPM": "Update with ESPM", "Updated": "Updated", + "Updating selected properties...": "Updating selected properties...", "Upload Access Level Instances": "Upload Access Level Instances", "Upload Audit Template XML": "Upload Audit Template XML", "Upload BuildingSync Data": "Upload BuildingSync Data", "Upload Green Button Data": "Upload Green Button Data", "Upload Portfolio Manager Data": "Upload Portfolio Manager Data", + "Upload ESPM Property": "Upload ESPM Property", "Upload a Spreadsheet": "Upload a Spreadsheet", "Upload another energy data file": "Upload another energy data file", "Upload your buildings list": "Upload your buildings list", @@ -1235,4 +1252,4 @@ "users": "users", "white": "white", "your data set name": "your data set name" -} \ No newline at end of file +} diff --git a/seed/static/seed/locales/fr_CA.json b/seed/static/seed/locales/fr_CA.json index 3a35776c69..3b0613a3e9 100644 --- a/seed/static/seed/locales/fr_CA.json +++ b/seed/static/seed/locales/fr_CA.json @@ -88,6 +88,7 @@ "Audit Template Email": "Audit Template Email", "Audit Template Organization Token": "Audit Template jeton d'organisation", "Audit Template Password": "Audit Template le mot de passe", + "Audit Template Upload Results": "Résultats du téléchargement du modèle d'audit", "Auditing": "Audit", "Auto Matching": "Correspondance automatique", "BEFORE_GEOCODING": "Avant le géocodage", @@ -180,6 +181,7 @@ "Conditioned Floor Area": "Surface climatisé", "Configuration": "Configuration", "Confirm": "Confirmer", + "Confirm Audit Template Building Import?": "Confirmer l'importation de la création du modèle d'audit ?", "Confirm Save Mappings?": "Confirmer enregistrer les mappages?", "Confirm delete": "Confirmer la supprimation", "Confirm new password": "Confirmer le nouveau mot de passe", @@ -319,6 +321,11 @@ "ENERGY STAR Score": "Compte ENERGY STAR", "ENERGY_TYPE_DISPLAY_CHOICE_PLACEHOLDER": "---- Choisissez le type d'énergie ----", "ENERGY_UNIT_DISPLAY_CHOICE_PLACEHOLDER": "---- Changer l'unité d'affichage ----", + "ESPM_FILE_UPLOAD": "Téléchargement de la feuille de calcul ESPM (.xlsx)", + "ESPM_IMPORT_TEXT": "Choisissez une méthode d'importation de données EnergyStar Portfolio Manager (ESPM) ci-dessous: vous pouvez soit télécharger une feuille de calcul de propriétés précédemment téléchargée depuis ESPM, soit vous connecter directement à ESPM pour accéder aux données.", + "ESPM Password": "Mot de passe ESPM", + "ESPM Property ID": "ID de propriété ESPM", + "ESPM Username": "Nom d'utilisateur ESPM", "EUI": "IUE", "EXCLUDE": "EXCLURE", "EXTRA_DATA_COL_TYPE_CHANGE": "Pour les champs «données supplémentaires», cela permet à l'utilisateur de définir le type, tel que Texte, Numéro, Date, etc.", @@ -369,6 +376,7 @@ "FIELD_NAMES_FOR_MATCHING": "Noms de zone pour l'appariement", "FILE_TYPES_SUPPORTED": "Types de fichiers pris en charge: .csv<\/strong>, .xls<\/strong>, .xlsx<\/strong>, .xml<\/strong>, .zip<\/strong>, .geojson<\/strong>, et .json<\/strong>.", "Failed to delete inventory": "Échec de la suppression de l'inventaire", + "Fetching your buildings from Audit Template...": "Récupération de vos bâtiments à partir du modèle d'audit...", "Field": "Champ", "Field Name": "Nom de champ", "Fields that have a \"Must Contain\" or \"Must Not Contain\" Condition Check rule cannot have a \"Range\" Condition Check rule.": "Les champs qui ont une règle de vérification de condition «Doit contenir» ou «Ne doit pas contenir» ne peuvent pas avoir de règle de vérification de condition «Plage».", @@ -447,10 +455,11 @@ "INTERNAL": "INTERNE", "INVALID_CSV_EXTENSION_ALERT": "Désolé!<\/strong> SEED ne prend actuellement pas en charge ce format de fichier. Seuls les fichiers .csv<\/strong> sont pris en charge.", "INVALID_DOC_FILE_EXTENSION_ALERT": "Type de document sélectionné non valide. Les types de fichiers acceptés sont .dxf, .pdf, .idf et .osm", - "INVALID_EXTENSION_ALERT": "Désolée!<\/strong> SEED ne supporte actuellement pas ce format de fichier. Seuls les fichiers .csv<\/strong>, .xls<\/strong>, .xlsx<\/strong>, et .xml<\/strong> sont pris en charge.", - "INVALID_GEOJSON_EXTENSION_ALERT": "Désolée!<\/strong> SEED ne supporte actuellement pas ce format de fichier. Seuls les fichiers .geojson<\/strong>, et .json<\/strong> sont pris en charge.", - "INVALID_XML_EXTENSION_ALERT": "Désolée!<\/strong> SEED ne supporte actuellement pas ce format de fichier. Seuls les fichiers .xml<\/strong> sont pris en charge.", - "INVALID_XML_ZIP_EXTENSION_ALERT": "Désolée!<\/strong> SEED ne supporte actuellement pas ce format de fichier. Seuls les fichiers .xml<\/strong> et .zip<\/strong> sont pris en charge.", + "INVALID_EXTENSION_ALERT": "Désolé!<\/strong> SEED ne supporte actuellement pas ce format de fichier. Seuls les fichiers .csv<\/strong>, .xls<\/strong>, .xlsx<\/strong>, et .xml<\/strong> sont pris en charge.", + "INVALID_GEOJSON_EXTENSION_ALERT": "Désolé!<\/strong> SEED ne supporte actuellement pas ce format de fichier. Seuls les fichiers .geojson<\/strong>, et .json<\/strong> sont pris en charge.", + "INVALID_XLSX_EXTENSION_ALERT": "Désolé!< \/strong > SEED ne prend actuellement pas en charge ce format de fichier. Seuls les fichiers .xlsx < \/strong > sont pris en charge.", + "INVALID_XML_EXTENSION_ALERT": "Désolé!<\/strong> SEED ne supporte actuellement pas ce format de fichier. Seuls les fichiers .xml<\/strong> sont pris en charge.", + "INVALID_XML_ZIP_EXTENSION_ALERT": "Désolé!<\/strong> SEED ne supporte actuellement pas ce format de fichier. Seuls les fichiers .xml<\/strong> et .zip<\/strong> sont pris en charge.", "IRREVERSIBLE_OPERATION_WARNING": "Cette opération est irréversible.", "ITEMS_WILL_NOT_CHANGE": "ceux-ci ne changeront pas", "Ignored duplicates of existing properties": "Copies ignorées des propriétés existantes", @@ -458,7 +467,9 @@ "Ignored property duplicates within the import file": "Copies de propriété ignorées dans le fichier d'importation", "Ignored tax lot duplicates within the import file": "Copies des lots d'impôt ignorées dans the fichier d'importation", "Import Access Level Instances": "Importer des instances de niveau d'accès", + "Import Audit Template Buildings": "Importer des bâtiments de modèle d'audit", "Import Portfolio Manager Data": "Importer les données de Portfolio Manager", + "Import directly from ESPM": "Importer directement depuis ESPM", "Import from Audit Template": "Importer à partir du Audit Template", "In addition, you need to specify where the field should be associated with Tax Lot data or Property data. This will affect how the data is matched and merged, as well as how it is displayed in the Inventory view.": "En outre, vous devez spécifier où le champ doit être associé aux données du lot d'impôt ou aux données de la propriété. Cela affectera la manière dont les données sont mises en correspondance et fusionnées, ainsi que la manière dont elles sont affichées dans la vue Inventaire.", "Inactive": "Inactif", @@ -894,6 +905,7 @@ "Select All": "Tout sélectionner", "Select All Columns": "Sélectionnez toutes les colonnes", "Select None": "Sélectionnez Aucun", + "Select Properties to Update": "Sélectionnez les propriétés à mettre à jour", "Select a Custom Report to get started!": "Sélectionnez un rapport personnalisé pour commencer !", "Select a Program to get started!": "Sélectionnez un programme pour commencer !", "Select a column to show data on this axis. Only columns with one of these data types will be listed:": "Sélectionnez une colonne pour afficher les données sur cet axe. Seules les colonnes contenant l'un de ces types de données seront répertoriées :", @@ -1000,6 +1012,7 @@ "There is also a link to the SEED-Platform Users forum, where you can connect with other users.": "Il y a aussi un lien vers le forum SEED-Platform Users, où vous pouvez vous connecter avec d'autres utilisateurs.", "There was an error loading the page": "Une erreur s'est produite lors du chargement de la page", "This action replaces any of your current columns with the comma-delmited columns you provided. Would you like to continue?": "Cette action remplace n'importe laquelle de vos colonnes actuelles par les colonnes délimitées par des virgules que vous avez fournies. Voulez-vous continuer?", + "This action updates properties within the selected cycle with data from the Audit Template account associated with this organization. Only Properties with Audit Template Building IDs corresponding to those saved in Audit Template will be updated.": "Cette action met à jour les propriétés du cycle sélectionné avec les données du compte de modèle d'audit associé à cette organisation. Seules les propriétés avec des ID de bâtiment de modèle d'audit correspondant à ceux enregistrés dans le modèle d'audit seront mises à jour.", "This cycle name is already taken.": "Ce nom de cycle est déjà pris.", "This email link is invalid.": "Cette opération est irréversible.", "This label name is already taken.": "Ce nom d'étiquette est déjà pris.", @@ -1056,13 +1069,17 @@ "Update Filters": "Mise à jour les filtres", "Update Salesforce": "Mettre à jour Salesforce", "Update UBID": "Mettre à jour UBID", + "Update with Audit Template": "Mise à jour avec Audit Template", "Update with BuildingSync": "Mettre à jour avec BuildingSync", + "Update with ESPM": "Mise à jour avec ESPM", "Updated": "Mise à jour", + "Updating selected properties...": "Mise à jour des propriétés sélectionnées...", "Upload Access Level Instances": "Importer des instances de niveau d'accès", "Upload Audit Template XML": "Téléchargement Audit Template XML", "Upload BuildingSync Data": "Télécharger les données du BuildingSync", "Upload Green Button Data": "Télécharger les données du Green Button", "Upload Portfolio Manager Data": "Télécharger les données du Portfolio Manager", + "Upload Single ESPM Property": "Télécharger une seule propriété ESPM", "Upload a Spreadsheet": "Télécharger une feuille de calcul", "Upload another energy data file": "Télécharger un autre fichier de données d'énergie", "Upload your buildings list": "Télécharger votre liste de bâtiments", @@ -1235,4 +1252,4 @@ "users": "utilisateurs", "white": "blanc", "your data set name": "votre nom du jeu de données" -} \ No newline at end of file +} diff --git a/seed/static/seed/partials/data_upload_espm_modal.html b/seed/static/seed/partials/data_upload_espm_modal.html new file mode 100644 index 0000000000..4d59d18d7e --- /dev/null +++ b/seed/static/seed/partials/data_upload_espm_modal.html @@ -0,0 +1,80 @@ + + +
+ + +
diff --git a/seed/static/seed/partials/inventory_detail.html b/seed/static/seed/partials/inventory_detail.html index 5ea688def3..03acbfa694 100644 --- a/seed/static/seed/partials/inventory_detail.html +++ b/seed/static/seed/partials/inventory_detail.html @@ -64,6 +64,10 @@

{$:: 'Update with Audit Template' | translate $} + +
  • {$:: 'Export BuildingSync' | translate $}
  • @@ -104,6 +108,7 @@

    +
    diff --git a/seed/static/seed/scss/style.scss b/seed/static/seed/scss/style.scss index 86c63ba834..51724da3f7 100755 --- a/seed/static/seed/scss/style.scss +++ b/seed/static/seed/scss/style.scss @@ -4872,3 +4872,16 @@ ul.r-list { border-color: #dd2c00; } } + +.or-text { + font-size: 1.3em; + text-align: center; +} + +.modal-content-section { + padding: 1em; + + &:not(:last-child) { + border-bottom: 1px solid #ddd; + } +} diff --git a/seed/templates/seed/_scripts.html b/seed/templates/seed/_scripts.html index ccd52971d7..4a7b5edd3c 100644 --- a/seed/templates/seed/_scripts.html +++ b/seed/templates/seed/_scripts.html @@ -51,6 +51,7 @@ + @@ -141,6 +142,7 @@ + diff --git a/seed/tests/data/mappings/espm-single-mapping.csv b/seed/tests/data/mappings/espm-single-mapping.csv new file mode 100644 index 0000000000..101b3df8bb --- /dev/null +++ b/seed/tests/data/mappings/espm-single-mapping.csv @@ -0,0 +1,25 @@ +Raw Columns,units,SEED Table,SEED Columns +How Many Buildings?,,PropertyState,building_count +City/Municipality,,PropertyState,city +Construction Status,,PropertyState,Construction Status +Country,,PropertyState,Country +Federal Agency/Department,,PropertyState,Federal Agency/Department +GFA Units,,PropertyState,GFA Units +Gross Floor Area,ft**2,PropertyState,gross_floor_area +Irrigated Area,,PropertyState,Irrigated Area +Irrigated Area Units,,PropertyState,Irrigated Area Units +Is this an Institutional Property? (Applicable only for Canadian properties),,PropertyState,Is this an Institutional Property? (Applicable only for Canadian properties) +Is this Property Owned or Operated by the US or Canadian Federal Government?,,PropertyState,Is this Property Owned or Operated by the US or Canadian Federal Government? +Number of Buildings,,PropertyState,Number of Buildings +Occupancy (%),,PropertyState,Occupancy +Other State/Province,,PropertyState,Other State +Parent Property ID,,PropertyState,Parent Property ID +Parent Property Name (if Applicable),,PropertyState,Parent Property Name (if Applicable) +Portfolio Manager ID,,PropertyState,pm_property_id +Postal Code,,PropertyState,postal_code +Property Name,,PropertyState,property_name +Property Type - Self-Selected,,PropertyState,property_type +State/Province,,PropertyState,state +Street Address,,PropertyState,address_line_1 +Street Address 2,,PropertyState,address_line_2 +Year Built,,PropertyState,year_built diff --git a/seed/tests/data/portfolio-manager-single-22482007.xlsx b/seed/tests/data/portfolio-manager-single-22482007.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0317d2c7f3b294c93fd8e617cfa7933c832fe321 GIT binary patch literal 29921 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 a_1-XB@k>l-^=S9e?h61o str: + """helper method to assemble the download url for a single property report. + + Args: + pm_property_id (int): PM Property ID to download + + Returns: + str: URL + """ + url = f"{self.DOWNLOAD_SINGLE_PROPERTY_REPORT_URL}/{pm_property_id}/download/{pm_property_id}.xlsx" + _log.debug(f"ESPM single property download URL is {url}") + return url diff --git a/seed/views/v3/properties.py b/seed/views/v3/properties.py index 3bf53d1437..34d56cfe04 100644 --- a/seed/views/v3/properties.py +++ b/seed/views/v3/properties.py @@ -6,6 +6,8 @@ import os from collections import namedtuple +from django.conf import settings +from django.core.files.storage import FileSystemStorage from django.db.models import Q, Subquery from django.http import HttpResponse, JsonResponse from django_filters import CharFilter, DateFilter @@ -17,16 +19,26 @@ from rest_framework.renderers import JSONRenderer from seed.building_sync.building_sync import BuildingSync +from seed.data_importer import tasks +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 from seed.data_importer.utils import kbtu_thermal_conversion_factors from seed.decorators import ajax_request_class from seed.hpxml.hpxml import HPXML +from seed.lib.progress_data.progress_data import ProgressData from seed.lib.superperms.orgs.decorators import has_perm_class from seed.models import ( + AUDIT_USER_CREATE, AUDIT_USER_EDIT, + DATA_STATE_MAPPING, DATA_STATE_MATCHING, MERGE_STATE_DELETE, MERGE_STATE_MERGED, MERGE_STATE_NEW, + PORTFOLIO_RAW, + SEED_DATA_SOURCES, Analysis, BuildingFile, Column, @@ -36,6 +48,7 @@ InventoryDocument, Meter, Note, + Organization, Property, PropertyAuditLog, PropertyMeasure, @@ -50,6 +63,7 @@ from seed.serializers.pint import PintJSONEncoder from seed.serializers.properties import ( PropertySerializer, + PropertyStatePromoteWritableSerializer, PropertyStateSerializer, PropertyViewAsStateSerializer, PropertyViewSerializer, @@ -1012,6 +1026,139 @@ def retrieve(self, request, pk=None): 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( + { + 'cycle_id': 'integer', + 'state': 'object', + }, + required=['cycle_id', 'state'] + ), + ) + @api_endpoint_class + @ajax_request_class + @has_perm_class('can_modify_data') + def create(self, request): + """ + Create a propertyState and propertyView via promote for given cycle + """ + org_id = self.get_organization(self.request) + data = request.data + # get state data + property_state_data = data.get('state', None) + cycle_pk = data.get('cycle_id', None) + + if cycle_pk is None: + return JsonResponse({ + 'status': 'error', + 'message': 'Missing required parameter cycle_id', + }, status=status.HTTP_400_BAD_REQUEST) + + if property_state_data is None: + return JsonResponse({ + 'status': 'error', + 'message': 'Missing required parameter state', + }, status=status.HTTP_400_BAD_REQUEST) + + # ensure that state organization_id is set to org in the request + state_org_id = property_state_data.get('organization_id', org_id) + if state_org_id != org_id: + return JsonResponse({ + 'status': 'error', + 'message': 'State organization_id does not match request organization_id', + }, status=status.HTTP_400_BAD_REQUEST) + property_state_data['organization_id'] = state_org_id + + # get cycle + try: + cycle = Cycle.objects.get(pk=cycle_pk, organization_id=org_id) + except Cycle.DoesNotExist: + return JsonResponse({ + 'status': 'error', + 'message': 'Invalid cycle_id', + }, status=status.HTTP_400_BAD_REQUEST) + + # set empty strings to None + try: + for key, val in property_state_data.items(): + if val == '': + property_state_data[key] = None + except AttributeError: + return JsonResponse({ + 'status': 'error', + 'message': 'Invalid state', + }, status=status.HTTP_400_BAD_REQUEST) + + # extra data fields that do not match existing columns will not be imported + extra_data_columns = list(Column.objects.filter( + organization_id=org_id, + table_name='PropertyState', + is_extra_data=True, + derived_column_id=None + ).values_list('column_name', flat=True)) + + extra_data = property_state_data.get('extra_data', {}) + new_data = {} + + for k, v in extra_data.items(): + # keep only those that match a column + if k in extra_data_columns: + new_data[k] = v + + property_state_data['extra_data'] = new_data + + # this serializer is meant to be used by a `create` action + property_state_serializer = PropertyStatePromoteWritableSerializer( + data=property_state_data + ) + + try: + valid = property_state_serializer.is_valid() + except ValueError as e: + return JsonResponse({ + 'status': 'error', + 'message': 'Invalid state: {}'.format(str(e)) + }, status=status.HTTP_400_BAD_REQUEST) + + if valid: + # create the new property state, and perform an initial save + new_state = property_state_serializer.save() + # set `merge_state` to new, rather than unknown + new_state.merge_state = MERGE_STATE_NEW + + # Log this appropriately - "Import Creation" ? + PropertyAuditLog.objects.create(organization_id=org_id, + parent1=None, + parent2=None, + parent_state1=None, + parent_state2=None, + state=new_state, + name='Import Creation', + description='Creation from API', + import_filename=None, + record_type=AUDIT_USER_CREATE) + + # promote to view + view = new_state.promote(cycle) + + return JsonResponse({ + 'status': 'success', + 'property_view_id': view.id, + 'property_state_id': new_state.id, + 'property_id': view.property.id, + 'view': PropertyViewSerializer(view).data + }, encoder=PintJSONEncoder, status=status.HTTP_201_CREATED) + + else: + # invalid request + return JsonResponse({ + 'status': 'error', + 'message': 'Invalid state: {}'.format(property_state_serializer.errors) + }, status=status.HTTP_400_BAD_REQUEST) + @swagger_auto_schema_org_query_param @api_endpoint_class @ajax_request_class @@ -1412,6 +1559,200 @@ def update_with_building_sync(self, request, pk): 'message': "Could not process building file with messages {}".format(messages) }, status=status.HTTP_400_BAD_REQUEST) + @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.query_integer_field( + 'mapping_profile_id', + required=True, + description='ID of the column mapping profile to use' + ), + AutoSchemaHelper.upload_file_field( + 'file', + required=True, + description='ESPM property report to use (in XLSX format)', + ), + ], + request_body=no_body, + ) + @action(detail=True, methods=['PUT'], parser_classes=(MultiPartParser,)) + @has_perm_class('can_modify_data') + def update_with_espm(self, request, pk): + """Update an existing PropertyView with an exported singular ESPM file. + """ + if len(request.FILES) == 0: + return JsonResponse({ + 'success': False, + 'message': 'Must pass file in as a multipart/form-data request' + }, status=status.HTTP_400_BAD_REQUEST) + + the_file = request.data['file'] + cycle_pk = request.query_params.get('cycle_id', None) + org_id = self.get_organization(self.request) + org_inst = Organization.objects.get(pk=org_id) + + # get mapping profile (ensure it is part of the org) + mapping_profile_id = request.query_params.get('mapping_profile_id', None) + if not mapping_profile_id: + return JsonResponse({ + 'success': False, + 'message': 'Must provide a column mapping profile' + }, status=status.HTTP_400_BAD_REQUEST) + + column_mapping_profile = org_inst.columnmappingprofile_set.filter( + pk=mapping_profile_id + ) + if len(column_mapping_profile) == 0: + return JsonResponse({ + 'success': False, + 'message': 'Could not find ESPM column mapping profile' + }, status=status.HTTP_400_BAD_REQUEST) + elif len(column_mapping_profile) > 1: + return JsonResponse({ + 'success': False, + 'message': f"Found multiple ESPM column mapping profiles, found {len(column_mapping_profile)}" + }, status=status.HTTP_400_BAD_REQUEST) + column_mapping_profile = column_mapping_profile[0] + + try: + 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) + + # create a new "datafile" object to store the file + import_record, _ = ImportRecord.objects.get_or_create( + name='Manual ESPM Records', + owner=request.user, + last_modified_by=request.user, + super_organization_id=org_id + ) + + filename = the_file.name + path = os.path.join(settings.MEDIA_ROOT, "uploads", filename) + + # Get a unique filename using the get_available_name method in FileSystemStorage + s = FileSystemStorage() + path = s.get_available_name(path) + + # verify the directory exists + if not os.path.exists(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + + # save the file + with open(path, 'wb+') as temp_file: + for chunk in the_file.chunks(): + temp_file.write(chunk) + + import_file = ImportFile.objects.create( + cycle_id=cycle_pk, + import_record=import_record, + uploaded_filename=filename, + file=path, + source_type=SEED_DATA_SOURCES[PORTFOLIO_RAW][1], + source_program='PortfolioManager', + source_program_version='1.0', + ) + + # save the raw data, but do it synchronously in the foreground + tasks.save_raw_espm_data_synchronous(import_file.pk) + + # verify that there is only one property in the file + import_file.refresh_from_db() + if import_file.num_rows != 1: + return JsonResponse({ + 'success': False, + 'message': f"File must contain exactly one property, found {import_file.num_rows or 0} properties" + }, status=status.HTTP_400_BAD_REQUEST) + + # create the column mappings + Column.retrieve_mapping_columns(import_file.pk) + + # assign the mappings to the import file id + Column.create_mappings(column_mapping_profile.mappings, org_inst, request.user, import_file.pk) + + # call the mapping process - but do this in the foreground, not asynchronously. + tasks.map_data_synchronous(import_file.pk) + + # The data should now be mapped, but since we called the task, we have the IDs of the + # mapped files, so query for the files. + new_property_state = PropertyState.objects.filter( + organization_id=org_id, + import_file_id=import_file.pk, + data_state=DATA_STATE_MAPPING, + ) + if len(new_property_state) == 0: + return JsonResponse({ + 'success': False, + 'message': "Could not find newly mapped property state" + }, status=status.HTTP_400_BAD_REQUEST) + elif len(new_property_state) > 1: + return JsonResponse({ + 'success': False, + 'message': f"Found multiple newly mapped property states, found {len(new_property_state)}" + }, status=status.HTTP_400_BAD_REQUEST) + new_property_state = new_property_state[0] + + # retrieve the column merge priorities and then save the update new property state. + # This is called merge protection on the front end. + priorities = Column.retrieve_priorities(org_id) + merged_state = save_state_match(property_view.state, new_property_state, priorities) + + # save the merged state to the latest property view + property_view.state = merged_state + property_view.save() + + # now save the meters, need a progress_data object to pass to the tasks, although + # not used. + progress_data = ProgressData(func_name='meter_import', unique_id=import_file.pk) + # -- Start -- + # For now, we are duplicating the methods that are called in the tasks in order + # to circumvent the celery background task management (i.e., run in foreground) + meters_parser = MetersParser.factory(import_file.local_file, org_id) + meters_and_readings = meters_parser.meter_and_reading_objs + for meter_readings in meters_and_readings: + _save_pm_meter_usage_data_task(meter_readings, import_file.id, progress_data.key) + # -- End -- of duplicate (and simplified) meter import methods + progress_data.delete() + + if merged_state: + return JsonResponse({ + 'success': True, + 'status': 'success', + 'message': 'successfully updated property with ESPM file', + 'data': { + 'status': 'success', + 'property_view': PropertyViewAsStateSerializer(property_view).data, + }, + }, status=status.HTTP_200_OK) + else: + return JsonResponse({ + 'status': 'error', + 'message': "Could not process ESPM file" + }, status=status.HTTP_400_BAD_REQUEST) + @action(detail=True, methods=['PUT'], parser_classes=(MultiPartParser,)) @has_perm_class('can_modify_data') def upload_inventory_document(self, request, pk):