diff --git a/CHANGELOG.md b/CHANGELOG.md index 0117a7093b..6f95b97440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +# SEED Version 3.2.1 + + + +## What's Changed +### New Features 🎉 +* Updates to portfolio summary by @perryr16 in https://github.com/SEED-platform/seed/pull/4862 +* Add export charts to default reports by @haneslinger in https://github.com/SEED-platform/seed/pull/4869 +* Add progress bar to derived data update by @perryr16 in https://github.com/SEED-platform/seed/pull/4825 +### Improvements 📈 +* Add filter groups to default reports page by @crutan in https://github.com/SEED-platform/seed/pull/4812 +* Speed up ESPM meters preview by @haneslinger in https://github.com/SEED-platform/seed/pull/4867 +* Add derived columns to default reports by @haneslinger in https://github.com/SEED-platform/seed/pull/4864 +* Create a mechanism to save report configurations to a named slots by @crutan in https://github.com/SEED-platform/seed/pull/4871 +* Update building upgrade recommendation modal by @kflemin in https://github.com/SEED-platform/seed/pull/4873 +* Add zoom/pan capability to report charting, add reset zoom buttons by @crutan in https://github.com/SEED-platform/seed/pull/4872 +* Move `Only Show Populated` functionality to backend by @haneslinger in https://github.com/SEED-platform/seed/pull/4866 +* Update aggregation table with pivot functionality by @kflemin in https://github.com/SEED-platform/seed/pull/4878 +### Bug Fixes 🐛 +* Fix Element Statistics KeyError by @haneslinger in https://github.com/SEED-platform/seed/pull/4824 +* Fix update display name by @haneslinger in https://github.com/SEED-platform/seed/pull/4827 +* Fix scatter plot for non-numeric x-axis by @crutan in https://github.com/SEED-platform/seed/pull/4828 +* Fix CTS export by @haneslinger in https://github.com/SEED-platform/seed/pull/4829 +* Fix reports axis display names by @haneslinger in https://github.com/SEED-platform/seed/pull/4832 +* Fix rollup table by @crutan in https://github.com/SEED-platform/seed/pull/4835 +* More CTS fixes by @haneslinger in https://github.com/SEED-platform/seed/pull/4830 +* Lock sass dependency and enforce Node v20 by @crutan in https://github.com/SEED-platform/seed/pull/4855 +* Recheck for only numeric data in the rollup table by @crutan in https://github.com/SEED-platform/seed/pull/4845 +* Include organization `access_level_names` in filter construction by @crutan in https://github.com/SEED-platform/seed/pull/4839 +* Ensure `year_built` labels do not have commas, adjust width of scatterchart by @crutan in https://github.com/SEED-platform/seed/pull/4859 +* Use old cycles during unmerge by @haneslinger in https://github.com/SEED-platform/seed/pull/4452 +* Fix stats table for various ali layouts by @kflemin in https://github.com/SEED-platform/seed/pull/4880 + + +**Full Changelog**: https://github.com/SEED-platform/seed/compare/v3.2.0...v3.2.1 + # SEED Version 3.2.0 diff --git a/docs/source/migrations.rst b/docs/source/migrations.rst index af857674e9..ef06f55a4b 100644 --- a/docs/source/migrations.rst +++ b/docs/source/migrations.rst @@ -38,6 +38,10 @@ local_untracked.py file ), ) +Version 3.2.1 +------------- +- There are no special migrations needed for this version. Simply run ``./manage.py migrate``. + Version 3.2.0 ------------- - There are no special migrations needed for this version. Simply run ``./manage.py migrate``. diff --git a/locale/en_US/LC_MESSAGES/django.mo b/locale/en_US/LC_MESSAGES/django.mo index abf4514071..88b5107dbe 100644 Binary files a/locale/en_US/LC_MESSAGES/django.mo and b/locale/en_US/LC_MESSAGES/django.mo differ diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index 67e39cce4e..4b7e4da6ec 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -120,7 +120,7 @@ msgid "ANALYSIS_DESCRIPTION_BSyncr" msgstr "The BSyncr analysis leverages the Normalized Metered Energy Consumption (NMEC) analysis to calculate a change point model. The data are passed to the analysis using BuildingSync. The result of the analysis are the coefficients of the change point model." msgid "ANALYSIS_DESCRIPTION_BuildingUpgradeRecommendation" -msgstr "The Building Upgrade Recommendation analysis implements a workflow to identify buildings that may need a deep energy retrofit, equipment replaced or re-tuning based on building attributes such as energy use, year built, and square footage." +msgstr "The Building Upgrade Recommendation analysis implements a workflow to identify buildings that may need a deep energy retrofit, equipment replaced or re-tuning based on building attributes such as energy use, year built, and square footage. If your organization contains elements, the Element Statistics Analysis should be run prior to running this analysis." msgid "ANALYSIS_DESCRIPTION_CO2" msgstr "This analysis calculates the average annual CO2 emissions for the property's meter data. The analysis requires an eGRID Subregion to be defined in order to accurately determine the emission rates." @@ -132,7 +132,7 @@ msgid "ANALYSIS_DESCRIPTION_EUI" msgstr "The EUI analysis will sum the property's meter readings for the last twelve months to calculate the energy use per square footage per year. If there are missing meter readings, then the analysis will return a less that 100% coverage to alert the user that there is a missing meter reading." msgid "ANALYSIS_DESCRIPTION_ElementStatistics" -msgstr "The Element Statistics analysis looks through a property's element data (if any) to count the number of elements of type 'D.D.C. Control Panel' and saves that quantity to the property" +msgstr "The Element Statistics analysis looks through a property's element data (if any) to count the number of elements of type 'D.D.C. Control Panel'. It also generates the aggregated (average) condition index values for scope 1 emission elements and saves those quantities to the property." msgid "AND" msgstr "AND" @@ -390,6 +390,9 @@ msgstr "Are you sure you want to unmerge these properties and then merge with th msgid "Are you sure you want to unmerge these tax lots and then merge with the selected tax lots?" msgstr "Are you sure you want to unmerge these tax lots and then merge with the selected tax lots?" +msgid "Are you sure you want to unmerge these tax lots?" +msgstr "Are you sure you want to unmerge these tax lots?" + msgid "Area" msgstr "Area" @@ -514,6 +517,9 @@ msgstr "Building Performance Standard (BPS) Compliance Pathway Support" msgid "Building Square Footage Threshold" msgstr "Building Square Footage Threshold" +msgid "Building has BAS field" +msgstr "Building has BAS field" + msgid "BuildingSync Recommended Measures" msgstr "BuildingSync Recommended Measures" @@ -1811,6 +1817,9 @@ msgstr "Goal Setup" msgid "Gross Floor Area" msgstr "Gross Floor Area" +msgid "HAS_BAS_HELP" +msgstr "Select the field that indicates whether or not the building has a Building Automation System (BAS). This analysis expects a boolean field." + msgid "HOW THE SYSTEM AUTO-MATCHES YOUR PROPERTIES AND TAX LOTS:" msgstr "HOW THE SYSTEM AUTO-MATCHES YOUR PROPERTIES AND TAX LOTS:" @@ -2962,6 +2971,9 @@ msgstr "Properties updated:" msgid "Properties with Data" msgstr "Properties with Data" +msgid "Properties with elements cannot be unmerged." +msgstr "Properties with elements cannot be unmerged." + msgid "Property" msgstr "Property" @@ -3976,7 +3988,7 @@ msgid "This password must contain at least %(quantity)d %(type)s characters." msgstr "This password must contain at least %(quantity)d %(type)s characters." msgid "This will reset your visible columns and column order to only columns that contain data. Are you sure you want to continue?" -msgstr "This will reset your visible columns and column order to only non-derived columns that contain data. Are you sure you want to continue?" +msgstr "This will reset your visible columns and column order to show only columns that contain data. Are you sure you want to continue?" msgid "Time Period" msgstr "Time Period" @@ -4095,6 +4107,9 @@ msgstr "UBID comparison result:" msgid "UBID thresholds are between 0.0 and 1.0" msgstr "UBID thresholds are between 0.0 and 1.0" +msgid "UNMERGE_PROPERTIES_MODAL" +msgstr "Are you sure you want to unmerge these properties? The unmerge action will keep meter data on both properties. Review meter data on both properties once they are unmerged." + msgid "UPDATE_DERIVED_DATA_TEXT_1" msgstr "Press the 'Begin Update' button below to begin the process of recalculating the data stored in derived columns for the selected properties and tax lots. The derived columns will appear yellow to indicate that the update is in progress." diff --git a/locale/es/LC_MESSAGES/django.mo b/locale/es/LC_MESSAGES/django.mo index 6e31274f76..9a0b9a397d 100644 Binary files a/locale/es/LC_MESSAGES/django.mo and b/locale/es/LC_MESSAGES/django.mo differ diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index b103cff7b1..ab831e0347 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: lokalise.com\n" "Project-Id-Version: SEED Platform\n" -"PO-Revision-Date: 2024-11-01 21:34\n" +"PO-Revision-Date: 2024-11-25 06:35\n" "Last-Translator: lokalise.com\n" "Language-Team: lokalise.com\n\n" "Language: es\n" @@ -153,9 +153,8 @@ msgstr "El análisis BETTER aprovecha better.lbl.gov para calcular el ahorro de msgid "ANALYSIS_DESCRIPTION_BSyncr" msgstr "El análisis BSyncr aprovecha el análisis de consumo energético medido normalizado (NMEC) para calcular un modelo de punto de cambio. Los datos se transmiten al análisis mediante BuildingSync. El resultado del análisis son los coeficientes del modelo de punto de cambio." -#, fuzzy msgid "ANALYSIS_DESCRIPTION_BuildingUpgradeRecommendation" -msgstr "El análisis de recomendaciones de mejora de edificios implementa un flujo de trabajo para identificar los edificios que pueden necesitar una profunda modernización energética, la sustitución de equipos o una nueva puesta a punto basada en atributos del edificio como el uso de la energía, el año de construcción y los metros cuadrados." +msgstr "El análisis de recomendación de mejora de edificios implementa un flujo de trabajo para identificar los edificios que pueden necesitar una renovación energética profunda, el reemplazo o la puesta a punto de equipos en función de los atributos del edificio, como el uso de energía, el año de construcción y la superficie en metros cuadrados. Si su organización contiene elementos, se debe ejecutar el análisis de estadísticas de elementos antes de ejecutar este análisis." #, fuzzy msgid "ANALYSIS_DESCRIPTION_CO2" @@ -169,9 +168,8 @@ msgstr "El análisis EEEJ utiliza la dirección de cada propiedad para identific msgid "ANALYSIS_DESCRIPTION_EUI" msgstr "El análisis EUI sumará las lecturas de los contadores de la propiedad de los últimos doce meses para calcular el consumo de energía por metro cuadrado al año. Si faltan lecturas de contadores, el análisis devolverá una cobertura inferior al 100% para alertar al usuario de que falta una lectura de contador." -#, fuzzy msgid "ANALYSIS_DESCRIPTION_ElementStatistics" -msgstr "El análisis de estadísticas de elementos examina los datos de los elementos de una propiedad (si los hay) para contar el número de elementos de tipo \"Panel de control D.D.C.\" y guarda esa cantidad en la propiedad" +msgstr "El análisis de estadísticas de elementos analiza los datos de elementos de una propiedad (si los hay) para contar la cantidad de elementos del tipo \"Panel de control DDC\". También genera los valores de índice de condición agregados (promedio) para los elementos de emisión de alcance 1 y guarda esas cantidades en la propiedad." #, fuzzy msgid "AND" @@ -509,6 +507,9 @@ msgstr "¿Está seguro de que desea deshacer la fusión de estas propiedades y, msgid "Are you sure you want to unmerge these tax lots and then merge with the selected tax lots?" msgstr "¿Está seguro de que desea deshacer la fusión de estos lotes fiscales y, a continuación, fusionarlos con los lotes fiscales seleccionados?" +msgid "Are you sure you want to unmerge these tax lots?" +msgstr "¿Está seguro de que desea separar estos lotes de impuestos?" + #, fuzzy msgid "Area" msgstr "Zona" @@ -669,6 +670,9 @@ msgstr "Apoyo para el cumplimiento de las normas de rendimiento de edificios (BP msgid "Building Square Footage Threshold" msgstr "Umbral de metros cuadrados del edificio" +msgid "Building has BAS field" +msgstr "El edificio tiene campo BAS" + #, fuzzy msgid "BuildingSync Recommended Measures" msgstr "BuildingSync Medidas recomendadas" @@ -2367,6 +2371,9 @@ msgstr "Configuración de la meta" msgid "Gross Floor Area" msgstr "Superficie bruta" +msgid "HAS_BAS_HELP" +msgstr "Seleccione el campo que indica si el edificio tiene o no un sistema de automatización de edificios (BAS). Este análisis espera un campo booleano." + #, fuzzy msgid "HOW THE SYSTEM AUTO-MATCHES YOUR PROPERTIES AND TAX LOTS:" msgstr "CÓMO EL SISTEMA EMPAREJA AUTOMÁTICAMENTE SUS PROPIEDADES Y LOTES FISCALES:" @@ -3879,6 +3886,9 @@ msgstr "Propiedades actualizadas:" msgid "Properties with Data" msgstr "Propiedades con datos" +msgid "Properties with elements cannot be unmerged." +msgstr "Las propiedades con elementos no se pueden separar." + #, fuzzy msgid "Property" msgstr "Propiedad" @@ -5208,7 +5218,7 @@ msgstr "Esta contraseña debe contener al menos %(quantity)d %(type)s caracteres #, fuzzy msgid "This will reset your visible columns and column order to only columns that contain data. Are you sure you want to continue?" -msgstr "Esto restablecerá las columnas visibles y el orden de las columnas a sólo las columnas no derivadas que contengan datos. ¿Está seguro de que desea continuar?" +msgstr "Esto restablecerá las columnas visibles y el orden de las columnas a sólo las columnas que contengan datos. ¿Está seguro de que desea continuar?" #, fuzzy msgid "Time Period" @@ -5362,6 +5372,9 @@ msgstr "Resultado de la comparación de UBID:" msgid "UBID thresholds are between 0.0 and 1.0" msgstr "Los umbrales UBID se sitúan entre 0,0 y 1,0" +msgid "UNMERGE_PROPERTIES_MODAL" +msgstr "¿Está seguro de que desea desvincular estas propiedades? La acción de desvincular conservará los datos del medidor en ambas propiedades. Revise los datos del medidor en ambas propiedades una vez que se hayan desvinculado." + #, fuzzy msgid "UPDATE_DERIVED_DATA_TEXT_1" msgstr "Pulse el botón \"Iniciar actualización\" para iniciar el proceso de recálculo de los datos almacenados en las columnas derivadas para las propiedades y lotes fiscales seleccionados. Las columnas derivadas aparecerán en amarillo para indicar que la actualización está en curso." diff --git a/locale/fr_CA/LC_MESSAGES/django.mo b/locale/fr_CA/LC_MESSAGES/django.mo index ded4216672..851ffe6ab6 100644 Binary files a/locale/fr_CA/LC_MESSAGES/django.mo and b/locale/fr_CA/LC_MESSAGES/django.mo differ diff --git a/locale/fr_CA/LC_MESSAGES/django.po b/locale/fr_CA/LC_MESSAGES/django.po index 4455aa086c..7275707e86 100644 --- a/locale/fr_CA/LC_MESSAGES/django.po +++ b/locale/fr_CA/LC_MESSAGES/django.po @@ -120,9 +120,8 @@ msgstr "L'analyse BETTER s'appuie sur better.lbl.gov pour calculer les économie msgid "ANALYSIS_DESCRIPTION_BSyncr" msgstr "L'analyse BSyncr exploite l'analyse de consommation d'énergie normalisée mesurée (NMEC) pour calculer un modèle de point de changement. Les données sont transmises à l'analyse à l'aide de BuildingSync. Le résultat de l'analyse sont les coefficients du modèle de point de changement." -#, fuzzy msgid "ANALYSIS_DESCRIPTION_BuildingUpgradeRecommendation" -msgstr "Cette analyse met en œuvre un flux de travail pour identifier les bâtiments qui peuvent nécessiter une rénovation énergétique en profondeur, un remplacement ou un réajustement de l'équipement en fonction des attributs du bâtiment tels que la consommation d'énergie, l'année de construction et la superficie en pieds carrés." +msgstr "L'analyse des recommandations de mise à niveau des bâtiments met en œuvre un flux de travail pour identifier les bâtiments qui peuvent nécessiter une rénovation énergétique approfondie, un remplacement de l'équipement ou un réajustement en fonction des attributs du bâtiment tels que la consommation d'énergie, l'année de construction et la superficie en pieds carrés. Si votre organisation contient des éléments, l'analyse des statistiques des éléments doit être exécutée avant d'exécuter cette analyse." msgid "ANALYSIS_DESCRIPTION_CO2" msgstr "Cette analyse calcule les émissions annuelles moyennes de CO2 pour les données des compteurs de la propriété. L’analyse nécessite la définition d’une sous-région eGRID afin de déterminer avec précision les taux d’émission." @@ -135,7 +134,7 @@ msgid "ANALYSIS_DESCRIPTION_EUI" msgstr "L'analyse EUI additionnera les relevés de compteurs de la propriété au cours des douze derniers mois pour calculer la consommation d'énergie par pied carré et par an. S'il manque des relevés de compteur, l'analyse renvoie une couverture inférieure à 100 % pour alerter l'utilisateur qu'il manque un relevé de compteur." msgid "ANALYSIS_DESCRIPTION_ElementStatistics" -msgstr "Cette analyse examine les données des éléments d'une propriété (le cas échéant) pour compter le nombre d'éléments de type « Panneau de configuration DDC » et enregistre cette quantité dans la propriété" +msgstr "L'analyse des statistiques des éléments examine les données des éléments d'une propriété (le cas échéant) pour compter le nombre d'éléments de type « Panneau de contrôle DDC ». Elle génère également les valeurs d'indice de condition agrégées (moyennes) pour les éléments d'émission de portée 1 et enregistre ces quantités dans la propriété." msgid "AND" msgstr "ET" @@ -395,6 +394,9 @@ msgstr "Êtes-vous sûr de vouloir annuler la fusion de ces propriétés et fusi msgid "Are you sure you want to unmerge these tax lots and then merge with the selected tax lots?" msgstr "Êtes-vous sûr de vouloir annuler la fusion de ces lots fiscaux pour ensuite fusionner avec les lots d'impôt sélectionnés?" +msgid "Are you sure you want to unmerge these tax lots?" +msgstr "Êtes-vous sûr de vouloir dissocier ces lots fiscaux ?" + msgid "Area" msgstr "Superficie" @@ -520,6 +522,9 @@ msgstr "Assistance au parcours de conformité aux normes de performance des bât msgid "Building Square Footage Threshold" msgstr "Seuil de superficie en pieds carrés du bâtiment" +msgid "Building has BAS field" +msgstr "Le bâtiment dispose d'un champ SAB" + msgid "BuildingSync Recommended Measures" msgstr "Mesures recommandées par BuildingSync" @@ -1827,6 +1832,9 @@ msgstr "Configuration des objectifs" msgid "Gross Floor Area" msgstr "Surface brute" +msgid "HAS_BAS_HELP" +msgstr "Sélectionnez le champ qui indique si le bâtiment est équipé ou non d'un système d'automatisation de bâtiment (SAB). Cette analyse attend un champ booléen." + msgid "HOW THE SYSTEM AUTO-MATCHES YOUR PROPERTIES AND TAX LOTS:" msgstr "COMMENT LE SYSTÈME ADAPTE AUTOMATIQUEMENT VOS PROPRIETES ET LOTS D'IMPÔT:" @@ -2982,6 +2990,9 @@ msgstr "Propriétés mises à jour :" msgid "Properties with Data" msgstr "Propriétés avec données" +msgid "Properties with elements cannot be unmerged." +msgstr "Les propriétés avec des éléments ne peuvent pas être dissociées." + msgid "Property" msgstr "Propriété" @@ -4007,8 +4018,9 @@ msgstr "Ce nom d'étiquette est déjà pris." msgid "This password must contain at least %(quantity)d %(type)s characters." msgstr "Ce mot de passe doit contenir au moins% (quantité) d% (type) s caractères." +#, fuzzy msgid "This will reset your visible columns and column order to only columns that contain data. Are you sure you want to continue?" -msgstr "Cela réinitialisera vos colonnes visibles et l'ordre des colonnes aux seules colonnes non dérivées contenant des données. Es-tu sur de vouloir continuer?" +msgstr "Cela réinitialisera vos colonnes visibles et l'ordre des colonnes à seulement celles contenant des données. Êtes-vous sur de vouloir continuer?" msgid "Time Period" msgstr "Période de temps" @@ -4127,6 +4139,9 @@ msgstr "Résultat de la comparaison UBID :" msgid "UBID thresholds are between 0.0 and 1.0" msgstr "Les seuils UBID sont compris entre 0,0 et 1,0" +msgid "UNMERGE_PROPERTIES_MODAL" +msgstr "Êtes-vous sûr de vouloir dissocier ces propriétés? L'action de dissociation conservera les données de compteur sur les deux propriétés. Vérifiez les données de compteur sur les deux propriétés une fois qu'elles sont dissociées." + msgid "UPDATE_DERIVED_DATA_TEXT_1" msgstr "Appuyez sur le bouton « Démarrer la mise à jour » ci-dessous pour commencer le processus de recalcul des données stockées dans les colonnes dérivées pour les propriétés et les lots fiscaux sélectionnés. Les colonnes dérivées apparaîtront en jaune pour indiquer que la mise à jour est en cours." diff --git a/package-lock.json b/package-lock.json index 565c96c6cb..10bc12cbf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,27 @@ { "name": "seed", - "version": "3.2.0", + "version": "3.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "seed", - "version": "3.2.0", + "version": "3.2.1", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.md", "devDependencies": { - "eslint": "^8.57.0", + "eslint": "^8.57.1", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-angular": "^4.1.0", - "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-prefer-arrow": "^1.2.3", "lodash": "^4.17.21", "npm-run-all": "^4.1.5", "prettier": "^3.3.3", - "puppeteer": "^22.13.0", - "sass": "^1.79.4", - "stylelint": "^16.7.0", + "puppeteer": "^22.15.0", + "sass": "1.79.4", + "stylelint": "^16.10.0", "stylelint-config-standard-scss": "^13.1.0" }, "engines": { @@ -137,9 +137,9 @@ } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", - "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", "dev": true, "funding": [ { @@ -153,16 +153,16 @@ ], "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.4.1" + "@csstools/css-tokenizer": "^3.0.3" } }, "node_modules/@csstools/css-tokenizer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", - "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", "dev": true, "funding": [ { @@ -176,13 +176,13 @@ ], "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", - "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz", + "integrity": "sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw==", "dev": true, "funding": [ { @@ -196,17 +196,17 @@ ], "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1" } }, "node_modules/@csstools/selector-specificity": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz", - "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-4.0.0.tgz", + "integrity": "sha512-189nelqtPd8++phaHNwYovKZI0FOzH1vQEE3QhHHkNIGrg5fSs9CbYP3RvfEH5geztnIA9Jwq91wyOIwAW5JIQ==", "dev": true, "funding": [ { @@ -218,11 +218,12 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" }, "peerDependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss-selector-parser": "^6.1.0" } }, "node_modules/@dual-bundle/import-meta-resolve": { @@ -283,22 +284,24 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -324,7 +327,8 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -362,20 +366,20 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.3.tgz", - "integrity": "sha512-bJ0UBsk0ESOs6RFcLXOt99a3yTDcOKlzfjad+rhFwdaG1Lu/Wzq58GHYCDTlZ9z6mldf4g+NTb+TXEfe0PpnsQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "progress": "2.0.3", - "proxy-agent": "6.4.0", - "semver": "7.6.0", - "tar-fs": "3.0.5", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.2" + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" @@ -384,46 +388,12 @@ "node": ">=18" } }, - "node_modules/@puppeteer/browsers/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@puppeteer/browsers/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -431,6 +401,13 @@ "node": ">=10" } }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -445,14 +422,14 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.8" } }, "node_modules/@types/yauzl": { @@ -713,9 +690,9 @@ } }, "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", "dev": true, "license": "Apache-2.0" }, @@ -726,17 +703,17 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", - "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", "dev": true, "license": "Apache-2.0", "optional": true }, "node_modules/bare-fs": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", - "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz", + "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -747,9 +724,9 @@ } }, "node_modules/bare-os": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", - "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz", + "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==", "dev": true, "license": "Apache-2.0", "optional": true @@ -766,14 +743,14 @@ } }, "node_modules/bare-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", - "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.2.tgz", + "integrity": "sha512-EFZHSIBkDgSHIwj2l2QZfP4U5OcD4xFAOwhSb/vlr9PIqyGJGvB/nfClJbcnh3EY4jtPE4zsb5ztae96bVF79A==", "dev": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "streamx": "^2.18.0" + "streamx": "^2.20.0" } }, "node_modules/base64-js": { @@ -924,9 +901,9 @@ } }, "node_modules/chromium-bidi": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.0.tgz", - "integrity": "sha512-VnxVrpGojAjkiGFN2I+KtsDILFAjiGWVEDizOEnKzEDkT93eQT1cqTfUkqmOyLq33i1q4a1KDYbH+52CUe4Ufw==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1030,21 +1007,23 @@ } }, "node_modules/css-functions-list": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.2.tgz", - "integrity": "sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", + "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12 || >=16" } }, "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.0.1.tgz", + "integrity": "sha512-8Fxxv+tGhORlshCdCwnNJytvlvq46sOLSYEx2ZIGurahWvMucSRnyjPA3AmrMq4VPRYbHVpWj5VkiVasrM2H4Q==", "dev": true, + "license": "MIT", "dependencies": { - "mdn-data": "2.0.30", + "mdn-data": "2.12.1", "source-map-js": "^1.0.1" }, "engines": { @@ -1125,12 +1104,13 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1197,9 +1177,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1299070", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1299070.tgz", - "integrity": "sha512-+qtL3eX50qsJ7c+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7//Xe8yhMmQ+elj3uAqSg==", + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", "dev": true, "license": "BSD-3-Clause" }, @@ -1404,9 +1384,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -1448,16 +1428,18 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -1542,10 +1524,11 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -1563,6 +1546,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -1574,34 +1558,37 @@ "dev": true }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -2510,12 +2497,16 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2940,10 +2931,11 @@ } }, "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.1.tgz", + "integrity": "sha512-rsfnCbOHjqrhWxwt5/wtSLzpoKTzW7OXdT5lLOIH1OTYhWu9rRJveGq0sKvDZODABH7RX+uoR+DYcpFnq4Tf6Q==", + "dev": true, + "license": "CC0-1.0" }, "node_modules/memorystream": { "version": "0.3.1", @@ -3018,10 +3010,11 @@ "license": "MIT" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.7", @@ -3528,10 +3521,11 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3576,9 +3570,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -3597,8 +3591,8 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -3611,15 +3605,16 @@ "dev": true }, "node_modules/postcss-resolve-nested-selector": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", - "integrity": "sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==", - "dev": true + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true, + "license": "MIT" }, "node_modules/postcss-safe-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.0.tgz", - "integrity": "sha512-ovehqRNVCpuFzbXoTb4qLtyzK3xn3t/CUBxOs8LsnQjQrShaB4lKiHoVqY8ANaC0hBMHq5QVWk77rwGklFUDrg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", "dev": true, "funding": [ { @@ -3635,6 +3630,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "engines": { "node": ">=18.0" }, @@ -3669,10 +3665,11 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3750,9 +3747,9 @@ "license": "MIT" }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, "license": "MIT", "dependencies": { @@ -3770,17 +3767,17 @@ } }, "node_modules/puppeteer": { - "version": "22.13.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.0.tgz", - "integrity": "sha512-nmICzeHTBtZiu+y4vs0fboe/NKIFwH5W8RZuxmEVAKNfBQg/8u5FEQAvPlWmyVpJoAVM5kXD5PEl3GlK3F9pPA==", + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.15.0.tgz", + "integrity": "sha512-XjCY1SiSEi1T7iSYuxS82ft85kwDJUS7wj1Z0eGVXKdtr5g4xnVcbjwxhq5xBnpK/E7x1VZZoJDxpjAOasHT4Q==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.2.3", + "@puppeteer/browsers": "2.3.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1299070", - "puppeteer-core": "22.13.0" + "devtools-protocol": "0.0.1312386", + "puppeteer-core": "22.15.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" @@ -3790,16 +3787,16 @@ } }, "node_modules/puppeteer-core": { - "version": "22.13.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.0.tgz", - "integrity": "sha512-ZkpRX8nm/S39BnpcCverMzIc6oGWBPOUeOeaWRLKHqiKVCZ1l28HxPTYLitJlDiB16xZATSKpjul+sl+ZEm0HQ==", + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.2.3", - "chromium-bidi": "0.6.0", - "debug": "^4.3.5", - "devtools-protocol": "0.0.1299070", + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", "ws": "^8.18.0" }, "engines": { @@ -4012,6 +4009,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.4.tgz", "integrity": "sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==", "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^4.0.0", "immutable": "^4.0.0", @@ -4204,10 +4202,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -4252,9 +4251,9 @@ "license": "BSD-3-Clause" }, "node_modules/streamx": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", - "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "version": "2.20.2", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.2.tgz", + "integrity": "sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA==", "dev": true, "license": "MIT", "dependencies": { @@ -4381,9 +4380,9 @@ } }, "node_modules/stylelint": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.7.0.tgz", - "integrity": "sha512-Q1ATiXlz+wYr37a7TGsfvqYn2nSR3T/isw3IWlZQzFzCNoACHuGBb6xBplZXz56/uDRJHIygxjh7jbV/8isewA==", + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.10.0.tgz", + "integrity": "sha512-z/8X2rZ52dt2c0stVwI9QL2AFJhLhbPkyfpDFcizs200V/g7v+UYY6SNcB9hKOLcDDX/yGLDsY/pX08sLkz9xQ==", "dev": true, "funding": [ { @@ -4397,42 +4396,41 @@ ], "license": "MIT", "dependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/media-query-list-parser": "^2.1.13", - "@csstools/selector-specificity": "^3.1.1", + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1", + "@csstools/media-query-list-parser": "^3.0.1", + "@csstools/selector-specificity": "^4.0.0", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", - "css-functions-list": "^3.2.2", - "css-tree": "^2.3.1", - "debug": "^4.3.5", + "css-functions-list": "^3.2.3", + "css-tree": "^3.0.0", + "debug": "^4.3.7", "fast-glob": "^3.3.2", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^9.0.0", + "file-entry-cache": "^9.1.0", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", - "ignore": "^5.3.1", + "ignore": "^6.0.2", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.34.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", - "micromatch": "^4.0.7", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.0.1", - "postcss": "^8.4.39", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-safe-parser": "^7.0.0", - "postcss-selector-parser": "^6.1.0", + "postcss": "^8.4.47", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^6.1.2", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", - "strip-ansi": "^7.1.0", - "supports-hyperlinks": "^3.0.0", + "supports-hyperlinks": "^3.1.0", "svg-tags": "^1.0.0", "table": "^6.8.2", "write-file-atomic": "^5.0.1" @@ -4535,18 +4533,6 @@ "stylelint": "^16.0.2" } }, - "node_modules/stylelint/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/stylelint/node_modules/balanced-match": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", @@ -4554,10 +4540,11 @@ "dev": true }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.0.0.tgz", - "integrity": "sha512-6MgEugi8p2tiUhqO7GnPsmbCCzj0YRCwwaTbpGRyKZesjRSzkqkAE9fPp7V2yMs5hwfgbQLgdvSSkGNg1s5Uvw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", + "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^5.0.0" }, @@ -4570,6 +4557,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz", "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.3.1", "keyv": "^4.5.4" @@ -4578,6 +4566,16 @@ "node": ">=18" } }, + "node_modules/stylelint/node_modules/ignore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/stylelint/node_modules/known-css-properties": { "version": "0.34.0", "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz", @@ -4594,21 +4592,6 @@ "node": ">=8" } }, - "node_modules/stylelint/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4622,16 +4605,20 @@ } }, "node_modules/supports-hyperlinks": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", - "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz", + "integrity": "sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" }, "engines": { "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -4691,9 +4678,9 @@ "dev": true }, "node_modules/tar-fs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", - "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "dev": true, "license": "MIT", "dependencies": { @@ -4718,14 +4705,11 @@ } }, "node_modules/text-decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", - "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } + "license": "Apache-2.0" }, "node_modules/text-table": { "version": "0.2.0", @@ -4765,9 +4749,9 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD" }, @@ -4895,9 +4879,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT", "optional": true @@ -5072,13 +5056,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index b848e6e742..fb151613cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "seed", - "version": "3.2.0", + "version": "3.2.1", "description": "Standard Energy Efficiency Data (SEED) Platform™", "license": "SEE LICENSE IN LICENSE.md", "directories": { @@ -17,18 +17,18 @@ "node": ">=20" }, "devDependencies": { - "eslint": "^8.57.0", + "eslint": "^8.57.1", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-angular": "^4.1.0", - "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-prefer-arrow": "^1.2.3", "lodash": "^4.17.21", "npm-run-all": "^4.1.5", "prettier": "^3.3.3", - "puppeteer": "^22.13.0", + "puppeteer": "^22.15.0", "sass": "1.79.4", - "stylelint": "^16.7.0", + "stylelint": "^16.10.0", "stylelint-config-standard-scss": "^13.1.0" }, "scripts": { diff --git a/seed/analysis_pipelines/element_statistics.py b/seed/analysis_pipelines/element_statistics.py index 8981e6e887..73a01ff31a 100644 --- a/seed/analysis_pipelines/element_statistics.py +++ b/seed/analysis_pipelines/element_statistics.py @@ -8,7 +8,7 @@ from celery import chain, shared_task from django.db import connection -from django.db.models import Count, Q +from django.db.models import BooleanField, Count, Q from seed.analysis_pipelines.pipeline import ( AnalysisPipeline, @@ -115,9 +115,10 @@ def _run_analysis(self, analysis_property_view_ids, analysis_id): # add ddc count by property id if ddc_count_column: ddc_count = ddc_count_by_property_id.get(property_view.property_id, 0) - property_view.state.extra_data[ddc_count_column.column_name] = ddc_count - if ddc_count: - analysis_property_view.parsed_results[ddc_count_column.column_name] = ddc_count + # convert to true/false + has_ddc = bool(ddc_count > 0) + property_view.state.extra_data[ddc_count_column.column_name] = has_ddc + analysis_property_view.parsed_results[ddc_count_column.column_name] = has_ddc # add mean condition index by code for code, col in existing_columns_names_by_code.items(): @@ -172,7 +173,7 @@ def _create_element_columns(analysis): def _create_ddc_count_column(analysis): try: return Column.objects.get( - column_name="Number of D.D.C Control Panels", + column_name="Has D.D.C Control Panels", organization=analysis.organization, table_name="PropertyState", ) @@ -181,12 +182,13 @@ def _create_ddc_count_column(analysis): if analysis.can_create(): column = Column.objects.create( is_extra_data=True, - column_name="Number of D.D.C Control Panels", + column_name="Has D.D.C Control Panels", organization=analysis.organization, table_name="PropertyState", + data_type=BooleanField, ) - column.display_name = "Number of D.D.C Control Panels" - column.column_description = "Number of D.D.C Control Panels" + column.display_name = "Has D.D.C Control Panels" + column.column_description = "Has D.D.C Control Panels" column.save() return column diff --git a/seed/analysis_pipelines/upgrade_recommendation.py b/seed/analysis_pipelines/upgrade_recommendation.py index 1c4418703c..019b84b65e 100644 --- a/seed/analysis_pipelines/upgrade_recommendation.py +++ b/seed/analysis_pipelines/upgrade_recommendation.py @@ -7,7 +7,6 @@ import logging from celery import chain, shared_task -from django.db.models import Count, Q from pint import Quantity from seed.analysis_pipelines.pipeline import ( @@ -91,6 +90,7 @@ def get_value(name): target_gas_eui = get_value("target_gas_eui") target_electric_eui = get_value("target_electric_eui") condition_index = get_value("condition_index") + has_bas = get_value("has_bas") year_built = property_view.state.year_built gross_floor_area = property_view.state.gross_floor_area elements = Element.objects.filter(property=property_view.property_id) @@ -124,16 +124,17 @@ def get_value(name): # if young building: retrofit_threshold_year = config.get("year_built_threshold") if year_built > retrofit_threshold_year: + # comment this out: switched to a boolean field instead of a count # if has BAS and actual to benchmark eui ratio is "fair" - ddc_control_panel_count = elements.annotate( - ddc_control_panel_count=Count("id", filter=Q(extra_data__Component_SubType="D.D.C. Control Panel")) - ) - has_bas = False - if ddc_control_panel_count: - has_bas = ddc_control_panel_count.order_by("ddc_control_panel_count").first().ddc_control_panel_count > 0 + # ddc_control_panel_count = elements.annotate( + # ddc_control_panel_count=Count("id", filter=Q(extra_data__Component_SubType="D.D.C. Control Panel")) + # ) + # has_bas = False + # if ddc_control_panel_count: + # has_bas = ddc_control_panel_count.order_by("ddc_control_panel_count").first().ddc_control_panel_count > 0 fair_actual_to_benchmark_eui_ratio = config.get("fair_actual_to_benchmark_eui_ratio") - if ((eui / benchmark) > fair_actual_to_benchmark_eui_ratio) and has_bas: + if ((eui / benchmark) > fair_actual_to_benchmark_eui_ratio) and has_bas is True: return "Re-tuning" else: return "NO DER project recommended" diff --git a/seed/migrations/0233_alter_goal_options.py b/seed/migrations/0233_alter_goal_options.py new file mode 100644 index 0000000000..e28621fc4c --- /dev/null +++ b/seed/migrations/0233_alter_goal_options.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.25 on 2024-11-01 21:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("seed", "0232_reportconfiguration"), + ] + + operations = [ + migrations.AlterModelOptions( + name="goal", + options={"ordering": ["name"]}, + ), + ] diff --git a/seed/models/analyses.py b/seed/models/analyses.py index efc2addd25..8ed29f1665 100644 --- a/seed/models/analyses.py +++ b/seed/models/analyses.py @@ -183,7 +183,13 @@ def get_highlights(self, property_id=None): # Element Statistics elif self.service == self.ELEMENTSTATISTICS: - return [{"name": k, "value": round(v, 2)} for k, v in results.items()] + res = [] + for k, v in results.items(): + if isinstance(v, str): + res.append({"name": k, "value": v}) + else: + res.append({"name": k, "value": round(v, 2)}) + return res # Building Upgrade Recommendation elif self.service == self.UPGRADERECOMMENDATION: recommendation = results.get("Building Upgrade Recommendation") diff --git a/seed/models/columns.py b/seed/models/columns.py index e2b10d043b..c701d10205 100644 --- a/seed/models/columns.py +++ b/seed/models/columns.py @@ -14,8 +14,8 @@ from django.apps import apps from django.core.exceptions import ValidationError -from django.db import IntegrityError, models, transaction -from django.db.models import Q +from django.db import IntegrityError, connection, models, transaction +from django.db.models import Count, Q from django.db.models.signals import pre_save from django.utils.translation import gettext_lazy as _ @@ -1665,6 +1665,48 @@ def retrieve_all_by_tuple(org_id): return result + @staticmethod + def get_num_of_nonnulls_by_column_name(state_ids, inventory_class, columns): + states = inventory_class.objects.filter(id__in=state_ids) + + # init dicts + num_of_nonnulls_by_column_name = {c.column_name: 0 for c in columns} + canonical_columns = [c.column_name for c in columns if not c.is_extra_data] + + # add non-null counts for extra_data columns + with connection.cursor() as cursor: + table_name = "seed_propertystate" if inventory_class.__name__ == "PropertyState" else "seed_taxlotstate" + non_null_extra_data_counts_query = ( + f'SELECT key, COUNT(*)\n' + f'FROM {table_name}, LATERAL JSONB_EACH_TEXT(extra_data) AS each_entry(key, value)\n' + f'WHERE id IN ({", ".join(map(str, state_ids))})\n' + f' AND value IS NOT NULL\n' + f'GROUP BY key;' + ) + cursor.execute(non_null_extra_data_counts_query) + extra_data_counts = dict(cursor.fetchall()) + num_of_nonnulls_by_column_name.update(extra_data_counts) + + # add non-null counts for derived_data columns + with connection.cursor() as cursor: + table_name = "seed_propertystate" if inventory_class.__name__ == "PropertyState" else "seed_taxlotstate" + non_null_derived_data_counts_query = ( + f'SELECT key, COUNT(*)\n' + f'FROM {table_name}, LATERAL JSONB_EACH_TEXT(derived_data) AS each_entry(key, value)\n' + f'WHERE id IN ({", ".join(map(str, state_ids))})\n' + f' AND value IS NOT NULL\n' + f'GROUP BY key;' + ) + cursor.execute(non_null_derived_data_counts_query) + derived_data_counts = dict(cursor.fetchall()) + num_of_nonnulls_by_column_name.update(derived_data_counts) + + # add non-null counts for canonical columns + canonical_counts = states.aggregate(**{col: Count(col) for col in canonical_columns}) + num_of_nonnulls_by_column_name.update(canonical_counts) + + return num_of_nonnulls_by_column_name + def validate_model(sender, **kwargs): instance = kwargs["instance"] diff --git a/seed/models/compliance_metrics.py b/seed/models/compliance_metrics.py index 27e06dd01b..98016a697c 100644 --- a/seed/models/compliance_metrics.py +++ b/seed/models/compliance_metrics.py @@ -195,6 +195,11 @@ def _calculate_compliance(self, the_property, bool_metric, metric_type): actual_col = self.actual_energy_column if metric_type == "energy" else self.actual_emission_column target_col = self.target_energy_column if metric_type == "energy" else self.target_emission_column actual_val = self._get_column_data(the_property, actual_col) + try: + actual_val = float(actual_val) + except Exception: + return "u" + if not isinstance(actual_val, numbers.Number): return "u" @@ -202,6 +207,11 @@ def _calculate_compliance(self, the_property, bool_metric, metric_type): return "y" if bool(actual_val) else "n" target_val = self._get_column_data(the_property, target_col) + try: + target_val = float(target_val) + except Exception: + return "u" + if not isinstance(target_val, numbers.Number): return "u" diff --git a/seed/models/data_quality.py b/seed/models/data_quality.py index 09c32e392e..7e45d5f4fe 100644 --- a/seed/models/data_quality.py +++ b/seed/models/data_quality.py @@ -977,10 +977,6 @@ def append_to_apply_labels(): self.add_result_range_error(row["current"].id, rule, data_type, value) self.update_status_label(PropertyViewLabel, rule, current_view.id, row["current"].id) - # other rule condition types - else: - logging.error(">>> OTHER") - else: # Within Cycle for cycle_key in ["baseline", "current"]: state = row["baseline"] if cycle_key == "baseline" else row["current"] @@ -1005,10 +1001,6 @@ def append_to_apply_labels(): self.add_result_is_null(state.id, rule, data_type, value) self.update_status_label(PropertyViewLabel, rule, view.id, state.id) - # other rule condition types. - else: - logging.error(">>> OTHER") - goal_note.passed_checks = all(results) # if there are multiple rules with the same label, determine if they are all passing to add or remove the label diff --git a/seed/models/filter_group.py b/seed/models/filter_group.py index e50faca992..c776f0bf25 100644 --- a/seed/models/filter_group.py +++ b/seed/models/filter_group.py @@ -38,15 +38,13 @@ def views(self, views, columns=[]): ) if not columns: columns = Column.retrieve_all(org_id=self.organization_id, inventory_type=related_model, only_used=False, include_related=False) - if self.query_dict: qd = QueryDict(mutable=True) qd.update(self.query_dict) - filters, _annotations, _order_by = build_view_filters_and_sorts( - qd, columns, related_model, self.organization.access_level_names - ) - filtered_views = views.filter(filters) + filters, annotations, _order_by = build_view_filters_and_sorts(qd, columns, related_model, self.organization.access_level_names) + + filtered_views = views.annotate(**annotations).filter(filters) else: filtered_views = views diff --git a/seed/models/goals.py b/seed/models/goals.py index dc2ce0f50c..f803fcd546 100644 --- a/seed/models/goals.py +++ b/seed/models/goals.py @@ -26,6 +26,9 @@ class Goal(models.Model): commitment_sqft = models.IntegerField(blank=True, null=True, validators=[MinValueValidator(0)]) name = models.CharField(max_length=255, unique=True) + class Meta: + ordering = ["name"] + def __str__(self): return f"Goal - {self.name}" diff --git a/seed/serializers/goals.py b/seed/serializers/goals.py index 95afcde5ec..177ff647b1 100644 --- a/seed/serializers/goals.py +++ b/seed/serializers/goals.py @@ -16,7 +16,20 @@ class Meta: def to_representation(self, obj): result = super().to_representation(obj) - result["level_name_index"] = obj.access_level_instance.depth - 1 + level_index = obj.access_level_instance.depth - 1 + + details = { + "level_name_index": level_index, + "level_name": obj.organization.access_level_names[level_index], + "baseline_cycle_name": obj.baseline_cycle.name, + "current_cycle_name": obj.current_cycle.name, + "eui_column1_name": obj.eui_column1.display_name, + "eui_column2_name": obj.eui_column2.display_name if obj.eui_column2 else None, + "eui_column3_name": obj.eui_column3.display_name if obj.eui_column3 else None, + "area_column_name": obj.area_column.display_name, + } + result.update(details) + return result def validate(self, data): diff --git a/seed/static/seed/js/controllers/inventory_detail_analyses_controller.js b/seed/static/seed/js/controllers/inventory_detail_analyses_controller.js index 6482ddbcc9..504365c46b 100644 --- a/seed/static/seed/js/controllers/inventory_detail_analyses_controller.js +++ b/seed/static/seed/js/controllers/inventory_detail_analyses_controller.js @@ -18,6 +18,7 @@ angular.module('SEED.controller.inventory_detail_analyses', []).controller('inve 'Notification', 'uploader_service', 'cycle_service', + 'columns', // eslint-disable-next-line func-names function ( $state, @@ -34,7 +35,8 @@ angular.module('SEED.controller.inventory_detail_analyses', []).controller('inve analyses_service, Notification, uploader_service, - cycle_service + cycle_service, + columns ) { $scope.item_state = inventory_payload.state; $scope.inventory_type = $stateParams.inventory_type; @@ -139,8 +141,9 @@ angular.module('SEED.controller.inventory_detail_analyses', []).controller('inve controller: 'inventory_detail_analyses_modal_controller', resolve: { inventory_ids: () => [$scope.inventory.view_id], - cycles: () => cycle_service.get_cycles().then((result) => result.cycles), + property_columns: () => columns.filter((x) => x.table_name === 'PropertyState'), current_cycle: () => $scope.cycle, + cycles: () => cycle_service.get_cycles().then((result) => result.cycles), user: () => $scope.menu.user } }) diff --git a/seed/static/seed/js/controllers/inventory_detail_analyses_modal_controller.js b/seed/static/seed/js/controllers/inventory_detail_analyses_modal_controller.js index 8d79a41220..e1112933ba 100644 --- a/seed/static/seed/js/controllers/inventory_detail_analyses_modal_controller.js +++ b/seed/static/seed/js/controllers/inventory_detail_analyses_modal_controller.js @@ -14,19 +14,19 @@ angular.module('SEED.controller.inventory_detail_analyses_modal', []).controller 'Notification', 'analyses_service', 'inventory_ids', - 'all_columns', + 'property_columns', 'current_cycle', 'cycles', 'user', // eslint-disable-next-line func-names - function ($scope, $sce, $log, $uibModalInstance, Notification, analyses_service, inventory_ids, all_columns, current_cycle, cycles, user) { + function ($scope, $sce, $log, $uibModalInstance, Notification, analyses_service, inventory_ids, property_columns, current_cycle, cycles, user) { $scope.inventory_count = inventory_ids.length; // used to disable buttons on submit $scope.waiting_for_server = false; $scope.cycles = cycles; $scope.user = user; - $scope.all_columns = all_columns; - $scope.eui_columns = $scope.all_columns.filter((o) => o.data_type === 'eui'); + $scope.property_columns = property_columns; + $scope.eui_columns = $scope.property_columns.filter((o) => o.data_type === 'eui'); $scope.new_analysis = { name: null, @@ -125,7 +125,8 @@ angular.module('SEED.controller.inventory_detail_analyses_modal', []).controller electric_eui: null, target_gas_eui: null, target_electric_eui: null, - condition_index: null + condition_index: null, + has_bas: null }, total_eui_goal: null, ff_eui_goal: null, diff --git a/seed/static/seed/js/controllers/inventory_detail_controller.js b/seed/static/seed/js/controllers/inventory_detail_controller.js index 3ca350b575..ea2769859a 100644 --- a/seed/static/seed/js/controllers/inventory_detail_controller.js +++ b/seed/static/seed/js/controllers/inventory_detail_controller.js @@ -259,7 +259,7 @@ angular.module('SEED.controller.inventory_detail', []).controller('inventory_det resolve: { columns: () => columns, currentProfile: () => $scope.currentProfile, - cycle: () => null, + cycle: () => $scope.cycle, inventory_type: () => $stateParams.inventory_type, provided_inventory() { const provided_inventory = []; @@ -568,6 +568,7 @@ angular.module('SEED.controller.inventory_detail', []).controller('inventory_det controller: 'inventory_detail_analyses_modal_controller', resolve: { inventory_ids: () => [$scope.inventory.view_id], + property_columns: () => columns.filter((x) => x.table_name === 'PropertyState'), current_cycle: () => $scope.cycle, cycles: () => cycle_service.get_cycles().then((result) => result.cycles), user: () => $scope.menu.user diff --git a/seed/static/seed/js/controllers/inventory_list_controller.js b/seed/static/seed/js/controllers/inventory_list_controller.js index 6aa92d1c71..a9c64d44c6 100644 --- a/seed/static/seed/js/controllers/inventory_list_controller.js +++ b/seed/static/seed/js/controllers/inventory_list_controller.js @@ -1327,15 +1327,18 @@ angular.module('SEED.controller.inventory_list', []).controller('inventory_list_ taxlot_view_ids: () => ($scope.inventory_type === 'taxlots' ? selectedViewIds : []) } }); - modalInstance.result.then(() => { - $scope.gridOptions.columnDefs.forEach((col) => { - if (col.derived_column) { - col.is_updating = true; - col.headerCellClass = 'updating-derived-column-display-name'; - } - }); - $scope.gridApi.core.notifyDataChange(uiGridConstants.dataChange.COLUMN); - }); + modalInstance.result.then( + () => {}, // on close + () => { // on dismiss + $scope.gridOptions.columnDefs.forEach((col) => { + if (col.derived_column) { + col.is_updating = true; + col.headerCellClass = 'updating-derived-column-display-name'; + } + }); + $scope.gridApi.core.notifyDataChange(uiGridConstants.dataChange.COLUMN); + } + ); }; $scope.open_delete_modal = (selectedViewIds) => { @@ -1640,9 +1643,9 @@ angular.module('SEED.controller.inventory_list', []).controller('inventory_list_ controller: 'inventory_detail_analyses_modal_controller', resolve: { inventory_ids: () => ($scope.inventory_type === 'properties' ? selectedViewIds : []), - all_columns: () => all_columns.filter((x) => x.table_name === 'PropertyState'), - cycles: () => cycles.cycles, + property_columns: () => all_columns.filter((x) => x.table_name === 'PropertyState'), current_cycle: () => $scope.cycle.selected_cycle, + cycles: () => cycles.cycles, user: () => $scope.menu.user } }); diff --git a/seed/static/seed/js/controllers/inventory_reports_controller.js b/seed/static/seed/js/controllers/inventory_reports_controller.js index e6bfd4657e..7cd7619855 100644 --- a/seed/static/seed/js/controllers/inventory_reports_controller.js +++ b/seed/static/seed/js/controllers/inventory_reports_controller.js @@ -65,15 +65,25 @@ angular.module('SEED.controller.inventory_reports', []).controller('inventory_re $scope.filter_groups = filter_groups; $scope.report_configurations = report_configurations; $scope.filter_group_id = null; - function path_to_string(path) { - const orderedPath = []; - for (const i in $scope.level_names) { - if (Object.prototype.hasOwnProperty.call(path, $scope.level_names[i])) { - orderedPath.push(path[$scope.level_names[i]]); - } + + $scope.has_children = (obj) => { + // check if the access level selected has children levels for stats table + let children = false; + if ('children' in obj && Object.keys(obj.children).length > 0) { + children = true; } - return orderedPath.join(' : '); - } + return children; + }; + + // function path_to_string(path) { + // const orderedPath = []; + // for (const i in $scope.level_names) { + // if (Object.prototype.hasOwnProperty.call(path, $scope.level_names[i])) { + // orderedPath.push(path[$scope.level_names[i]]); + // } + // } + // return orderedPath.join(' : '); + // } const access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth($scope.access_level_tree); // cannot select parents alis const [users_depth] = Object.entries(access_level_instances_by_depth).find(([, x]) => x.length === 1 && x[0].id === parseInt($scope.users_access_level_instance_id, 10)); @@ -81,9 +91,9 @@ angular.module('SEED.controller.inventory_reports', []).controller('inventory_re $scope.change_selected_level_index = () => { const new_level_instance_depth = parseInt($scope.level_name_index, 10) + parseInt(users_depth, 10); $scope.potential_level_instances = access_level_instances_by_depth[new_level_instance_depth]; - for (const key in $scope.potential_level_instances) { - $scope.potential_level_instances[key].name = path_to_string($scope.potential_level_instances[key].path); - } + // for (const key in $scope.potential_level_instances) { + // $scope.potential_level_instances[key].name = path_to_string($scope.potential_level_instances[key].path); + // } $scope.access_level_instance_id = null; $scope.setModified(); }; @@ -380,7 +390,7 @@ angular.module('SEED.controller.inventory_reports', []).controller('inventory_re return ctx[0]?.raw.display_name; }, label: (ctx) => [ - `${$scope.xAxisSelectedItem.label}: ${type === 'bar' ? ctx.label : ctx.parsed.x}`, + `${$scope.xAxisSelectedItem.label}: ${type === 'bar' ? ctx.label : ctx.raw.x}`, `${$scope.yAxisSelectedItem.label}: ${type === 'bar' ? ctx.raw : ctx.parsed.y}` ] } @@ -621,9 +631,14 @@ angular.module('SEED.controller.inventory_reports', []).controller('inventory_re $scope.scatterChart.options.scales.x.ticks.callback = (value) => String(value); } } else { + // restore title text as this syntax overwrites it $scope.scatterChart.options.scales.x = { type: 'category', - labels: Array.from([...new Set($scope.chartData.chartData.map((d) => d.x))]).sort() + labels: Array.from([...new Set($scope.chartData.chartData.map((d) => d.x))]).sort(), + title: { + display: true, + text: $scope.xAxisSelectedItem.label + } }; } if ($scope.yAxisSelectedItem.varName === 'year_built') { @@ -718,6 +733,18 @@ angular.module('SEED.controller.inventory_reports', []).controller('inventory_re }); } + $scope.downloadChart = () => { + const a = document.createElement('a'); + a.href = $scope.barChart.toBase64Image(); + a.download = 'default_report_bar.png'; + a.click(); + + const b = document.createElement('a'); + b.href = $scope.scatterChart.toBase64Image(); + b.download = 'default_report_scatter.png'; + b.click(); + }; + function updateStorage() { // Save axis and cycle selections localStorage.setItem(localStorageXAxisKey, JSON.stringify($scope.xAxisSelectedItem ?? '')); diff --git a/seed/static/seed/js/controllers/organization_settings_controller.js b/seed/static/seed/js/controllers/organization_settings_controller.js index f19c8051ab..8082942327 100644 --- a/seed/static/seed/js/controllers/organization_settings_controller.js +++ b/seed/static/seed/js/controllers/organization_settings_controller.js @@ -312,11 +312,11 @@ angular.module('SEED.controller.organization_settings', []).controller('organiza }; const acceptable_column_types = ['area', 'eui', 'float', 'integer', 'number']; - const filtered_columns = _.filter($scope.columns, (column) => _.includes(acceptable_column_types, column.data_type)); + // filtered columns should include derived columns + const filtered_columns = _.filter($scope.columns, (column) => column.derived_column || _.includes(acceptable_column_types, column.data_type)); $scope.selected_x_columns = $scope.org.default_reports_x_axis_options.map((c) => c.id); $scope.available_x_columns = () => $scope.columns.filter(({ id }) => !$scope.selected_x_columns.includes(id)); - $scope.add_x_column = (x_column_id) => { $scope.selected_x_columns.push(x_column_id); }; diff --git a/seed/static/seed/js/controllers/portfolio_summary_controller.js b/seed/static/seed/js/controllers/portfolio_summary_controller.js index 20ecc0c8a1..d8291ab781 100644 --- a/seed/static/seed/js/controllers/portfolio_summary_controller.js +++ b/seed/static/seed/js/controllers/portfolio_summary_controller.js @@ -85,14 +85,41 @@ angular.module('SEED.controller.portfolio_summary', []) // Can only sort based on baseline or current, not both. In the event of a conflict, use the more recent. let baseline_first = false; - const sort_goals = (goals) => goals.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)); + const load_data = (page) => { + $scope.data_loading = true; + const per_page = 50; + const data = { + goal_id: $scope.goal.id, + page, + per_page, + baseline_first, + access_level_instance_id: $scope.goal.access_level_instance, + related_model_sort: $scope.related_model_sort + }; + const column_filters = $scope.column_filters; + const order_by = $scope.column_sorts; + goal_service.load_data(data, column_filters, order_by).then((response) => { + const data = response.data; + $scope.inventory_pagination = data.pagination; + $scope.property_lookup = data.property_lookup; + $scope.data = data.properties; + get_all_labels(); + set_grid_options(); + $scope.data_valid = Boolean(data.properties); + $scope.data_loading = false; + }); + }; + // optionally pass a goal name to be set as $scope.goal - used on modal close const get_goals = (goal_name = false) => { goal_service.get_goals().then((result) => { - $scope.goals = _.isEmpty(result.goals) ? [] : sort_goals(result.goals); + $scope.goals = result.goals; $scope.goal = goal_name ? $scope.goals.find((goal) => goal.name === goal_name) : $scope.goals[0]; + format_goal_details(); + load_summary(); + load_data(1); }); }; get_goals(); @@ -104,13 +131,13 @@ angular.module('SEED.controller.portfolio_summary', []) }; // If goal changes, reset grid filters and repopulate ui-grids - $scope.$watch('goal', () => { + $scope.$watch('goal', (cur, old) => { if ($scope.gridApi) $scope.reset_sorts_filters(); $scope.data_valid = false; if (_.isEmpty($scope.goal)) { $scope.valid = false; $scope.summary_valid = false; - } else { + } else if (old.id) { // prevent duplicate request on page load reset_data(); } }); @@ -118,31 +145,28 @@ angular.module('SEED.controller.portfolio_summary', []) // selected goal details const format_goal_details = () => { $scope.change_selected_level_index(); - const get_column_name = (column_id) => $scope.columns.find((col) => col.id === column_id).displayName; - const get_cycle_name = (cycle_id) => $scope.cycles.find((col) => col.id === cycle_id).name; - const level_name = $scope.level_names[$scope.goal.level_name_index]; const access_level_instance = $scope.potential_level_instances.find((level) => level.id === $scope.goal.access_level_instance).name; - const commitment_sqft = $scope.goal.commitment_sqft ? $scope.goal.commitment_sqft.toLocaleString() : 'n/a'; + const commitment_sqft = $scope.goal.commitment_sqft?.toLocaleString() || 'n/a'; $scope.goal_details = [ { // column 1 - 'Baseline Cycle': get_cycle_name($scope.goal.baseline_cycle), - 'Current Cycle': get_cycle_name($scope.goal.current_cycle), - [level_name]: access_level_instance, + 'Baseline Cycle': $scope.goal.baseline_cycle_name, + 'Current Cycle': $scope.goal.current_cycle_name, + [$scope.goal.level_name]: access_level_instance, 'Total Properties': null, 'Commitment Sq. Ft': commitment_sqft }, { // column 2 'Portfolio Target': `${$scope.goal.target_percentage} %`, - 'Area Column': get_column_name($scope.goal.area_column), - 'Primary EUI': get_column_name($scope.goal.eui_column1) + 'Area Column': $scope.goal.area_column_name, + 'Primary EUI': $scope.goal.eui_column1_name } ]; if ($scope.goal.eui_column2) { - $scope.goal_details[1]['Secondary EUI'] = get_column_name($scope.goal.eui_column2); + $scope.goal_details[1]['Secondary EUI'] = $scope.goal.eui_column2_name; } if ($scope.goal.eui_column3) { - $scope.goal_details[1]['Tertiary EUI'] = get_column_name($scope.goal.eui_column3); + $scope.goal_details[1]['Tertiary EUI'] = $scope.goal.eui_column3_name; } }; @@ -206,7 +230,7 @@ angular.module('SEED.controller.portfolio_summary', []) const refresh_data = () => { load_summary(); - load_inventory(1); + load_data(1); }; const load_summary = () => { @@ -225,71 +249,7 @@ angular.module('SEED.controller.portfolio_summary', []) $scope.page_change = (page) => { spinner_utility.show(); - load_inventory(page); - }; - const load_inventory = (page) => { - $scope.data_loading = true; - - const access_level_instance_id = $scope.goal.access_level_instance; - const combined_result = {}; - const per_page = 50; - const current_cycle = { id: $scope.goal.current_cycle }; - const baseline_cycle = { id: $scope.goal.baseline_cycle }; - // order of cycle property filter is dynamic based on column_sorts - const cycle_priority = baseline_first ? [baseline_cycle, current_cycle] : [current_cycle, baseline_cycle]; - - get_paginated_properties(page, per_page, cycle_priority[0], access_level_instance_id, true, null).then((result0) => { - $scope.inventory_pagination = result0.pagination; - let properties = result0.results; - combined_result[cycle_priority[0].id] = properties; - const property_ids = properties.map((p) => p.id); - - get_paginated_properties(page, per_page, cycle_priority[1], access_level_instance_id, false, property_ids).then((result1) => { - properties = result1.results; - // if result0 returns fewer properties than result1, use result1 for ui-grid config - if (result1.pagination.num_pages > $scope.inventory_pagination.num_pages) { - baseline_first = !baseline_first; - $scope.inventory_pagination = result1.pagination; - } - combined_result[cycle_priority[1].id] = properties; - get_all_labels(); - set_grid_options(combined_result); - }).then(() => { - $scope.data_loading = false; - $scope.data_valid = true; - }); - }); - }; - - const get_paginated_properties = (page, chunk, cycle, access_level_instance_id, include_filters_sorts, include_property_ids = null) => { - const fn = inventory_service.get_properties; - const [filters, sorts] = include_filters_sorts ? [$scope.column_filters, $scope.column_sorts] : [[], []]; - - return fn( - page, - chunk, - cycle, - undefined, // profile_id - undefined, // include_view_ids - undefined, // exclude_view_ids - true, // save_last_cycle - $scope.organization.id, - true, // include_related - filters, - sorts, - false, // ids_only - table_column_ids.join(), - access_level_instance_id, - include_property_ids, - $scope.goal.id, // optional param to retrieve goal note details - $scope.related_model_sort // optional param to sort on related models - ); - }; - - const percentage = (a, b) => { - if (!a || b == null) return null; - const value = Math.round(((a - b) / a) * 100); - return Number.isNaN(value) ? null : value; + load_data(page); }; // -------------- LABEL LOGIC ------------- @@ -404,83 +364,6 @@ angular.module('SEED.controller.portfolio_summary', []) // ------------ DATA TABLE LOGIC --------- - const set_eui_goal = (baseline, current, property, preferred_columns) => { - // only check defined columns - for (const col of preferred_columns.filter((c) => c)) { - if (baseline && _.isNil(property.baseline_eui)) { - property.baseline_eui = baseline[col.name]; - } - if (current && _.isNil(property.current_eui)) { - property.current_eui = current[col.name]; - } - } - - property.baseline_kbtu = Math.round(property.baseline_sqft * property.baseline_eui) || undefined; - property.current_kbtu = Math.round(property.current_sqft * property.current_eui) || undefined; - property.eui_change = percentage(property.baseline_eui, property.current_eui); - }; - - const format_properties = (properties) => { - const area = $scope.columns.find((c) => c.id === $scope.goal.area_column); - const preferred_columns = [$scope.columns.find((c) => c.id === $scope.goal.eui_column1)]; - if ($scope.goal.eui_column2) preferred_columns.push($scope.columns.find((c) => c.id === $scope.goal.eui_column2)); - if ($scope.goal.eui_column3) preferred_columns.push($scope.columns.find((c) => c.id === $scope.goal.eui_column3)); - - const baseline_cycle_name = $scope.cycles.find((c) => c.id === $scope.goal.baseline_cycle).name; - const current_cycle_name = $scope.cycles.find((c) => c.id === $scope.goal.current_cycle).name; - // some fields span cycles (id, name, type) - // and others are cycle specific (source EUI, sqft) - const current_properties = properties[$scope.goal.current_cycle]; - const baseline_properties = properties[$scope.goal.baseline_cycle]; - const flat_properties = baseline_first ? - [baseline_properties, current_properties].flat() : - [current_properties, baseline_properties].flat(); - - // labels are related to property views, but cross cycles displays based on property - // create a lookup between property_view.id to property.id - $scope.property_lookup = {}; - for (const p of flat_properties) { - $scope.property_lookup[p.property_view_id] = p.id; - } - const unique_ids = [...new Set(flat_properties.map((property) => property.id))]; - const combined_properties = []; - for (const id of unique_ids) { - // find matching properties - const baseline = baseline_properties.find((p) => p.id === id) || {}; - const current = current_properties.find((p) => p.id === id) || {}; - // set accumulator - const property = combine_properties(current, baseline); - // add baseline stats - if (baseline) { - property.baseline_cycle = baseline_cycle_name; - property.baseline_sqft = baseline[area.name]; - } - // add current stats - if (current) { - property.current_cycle = current_cycle_name; - property.current_sqft = current[area.name]; - property.current_view_id = current.property_view_id; - } - // comparison stats - property.sqft_change = percentage(property.current_sqft, property.baseline_sqft); - set_eui_goal(baseline, current, property, preferred_columns); - combined_properties.push(property); - } - return combined_properties; - }; - - const combine_properties = (current, baseline) => { - // Given 2 properties, find non-null values and combine into a single property (favoring baseline if baseline_first) - const [a, b] = baseline_first ? [baseline, current] : [current, baseline]; - const c = { ...b }; - Object.keys(a).forEach((key) => { - if (a[key] !== null && a[key] !== undefined) { - c[key] = a[key]; - } - }); - return c; - }; - const apply_defaults = (cols, ...defaults) => { _.map(cols, (col) => _.defaults(col, ...defaults)); }; @@ -905,10 +788,9 @@ angular.module('SEED.controller.portfolio_summary', []) } }; - const set_grid_options = (result) => { + const set_grid_options = () => { $scope.show_full_labels = { baseline: false, current: false }; $scope.selected_ids = []; - $scope.data = format_properties(result); spinner_utility.hide(); $scope.gridOptions = { data: 'data', @@ -940,14 +822,14 @@ angular.module('SEED.controller.portfolio_summary', []) spinner_utility.show(); _.debounce(() => { updateColumnFilterSort(); - load_inventory(1); + load_data(1); }, 500)(); }); gridApi.core.on.filterChanged($scope, _.debounce(() => { spinner_utility.show(); updateColumnFilterSort(); - load_inventory(1); + load_data(1); }, 2000)); const selectionChanged = () => { @@ -1025,7 +907,7 @@ angular.module('SEED.controller.portfolio_summary', []) $scope.selected_option = 'none'; $scope.selected_count = 0; $scope.gridApi.selection.clearSelectedRows(); - load_inventory(); + load_data(); }); }; @@ -1049,7 +931,7 @@ angular.module('SEED.controller.portfolio_summary', []) $scope.selected_count = 0; $scope.gridApi.selection.clearSelectedRows(); load_summary(); - load_inventory(); + load_data(); }); }; @@ -1124,6 +1006,7 @@ angular.module('SEED.controller.portfolio_summary', []) }; }; + // --- DATA QUALITY --- $scope.run_data_quality_check = () => { spinner_utility.show(); data_quality_service.start_data_quality_checks([], [], $scope.goal.id) @@ -1146,7 +1029,7 @@ angular.module('SEED.controller.portfolio_summary', []) }); spinner_utility.hide(); load_summary(); - load_inventory(); + load_data(); }); }); }) diff --git a/seed/static/seed/js/controllers/program_setup_controller.js b/seed/static/seed/js/controllers/program_setup_controller.js index edd64869a7..c7c85ca6a0 100644 --- a/seed/static/seed/js/controllers/program_setup_controller.js +++ b/seed/static/seed/js/controllers/program_setup_controller.js @@ -44,8 +44,8 @@ angular.module('SEED.controller.program_setup', []).controller('program_setup_co $scope.valid_column_data_types = ['number', 'float', 'integer', 'ghg', 'ghg_intensity', 'area', 'eui', 'boolean']; $scope.valid_x_axis_data_types = ['number', 'string', 'float', 'integer', 'ghg', 'ghg_intensity', 'area', 'eui', 'boolean']; - $scope.property_columns = _.reject(property_columns, (item) => item.related || !$scope.valid_column_data_types.includes(item.data_type)).sort((a, b) => naturalSort(a.displayName, b.displayName)); - $scope.x_axis_columns = _.reject(property_columns, (item) => item.related || !$scope.valid_x_axis_data_types.includes(item.data_type)).sort((a, b) => naturalSort(a.displayName, b.displayName)); + $scope.property_columns = _.reject(property_columns, (item) => (item.related || !$scope.valid_column_data_types.includes(item.data_type)) && item.derived_column == null).sort((a, b) => naturalSort(a.displayName, b.displayName)); + $scope.x_axis_columns = _.reject(property_columns, (item) => (item.related || !$scope.valid_x_axis_data_types.includes(item.data_type)) && item.derived_column == null).sort((a, b) => naturalSort(a.displayName, b.displayName)); $scope.x_axis_selection = ''; $scope.cycle_selection = ''; $scope.compliance_metrics_error = []; diff --git a/seed/static/seed/js/controllers/show_populated_columns_modal_controller.js b/seed/static/seed/js/controllers/show_populated_columns_modal_controller.js index aa5bd47e86..54e980a262 100644 --- a/seed/static/seed/js/controllers/show_populated_columns_modal_controller.js +++ b/seed/static/seed/js/controllers/show_populated_columns_modal_controller.js @@ -17,118 +17,22 @@ angular.module('SEED.controller.show_populated_columns_modal', []).controller('s 'inventory_type', // eslint-disable-next-line func-names function ($scope, $window, $uibModalInstance, Notification, inventory_service, modified_service, spinner_utility, columns, currentProfile, cycle, provided_inventory, inventory_type) { - $scope.columns = columns; - $scope.currentProfile = currentProfile; - $scope.cycle = cycle; - $scope.inventory_type = inventory_type; - - _.forEach($scope.columns, (col) => { - col.pinnedLeft = false; - col.visible = true; - }); - - const notEmpty = (value) => !_.isNil(value) && value !== ''; - - const fetch = (page, chunk) => { - let fn; - if ($scope.inventory_type === 'properties') { - fn = inventory_service.get_properties; - } else if ($scope.inventory_type === 'taxlots') { - fn = inventory_service.get_taxlots; - } - return fn(page, chunk, $scope.cycle, -1).then((data) => { - $scope.progress = Math.round((data.pagination.end / data.pagination.total) * 100); - if (data.pagination.has_next) { - return fetch(page + 1, chunk).then((data2) => data.results.concat(data2)); - } - return data.results; - }); - }; - - const update_profile_with_populated_columns = (inventory) => { - $scope.status = `Processing ${$scope.columns.length} columns in ${inventory.length} records`; - - const cols = _.reject($scope.columns, 'related'); - // console.log('cols', cols); - - const relatedCols = _.filter($scope.columns, 'related'); - // console.log('relatedCols', relatedCols); - - const col_key = provided_inventory ? 'column_name' : 'name'; - - _.forEach(inventory, (record, index) => { - // console.log(cols.length + ' remaining cols to check'); - _.forEachRight(cols, (col, colIndex) => { - if (notEmpty(record[col[col_key]])) { - // console.log('Removing ' + col[col_key] + ' from cols'); - cols.splice(colIndex, 1); - } - }); - - _.forEach(record.related, (relatedRecord) => { - // console.log(relatedCols.length + ' remaining related cols to check'); - _.forEachRight(relatedCols, (col, colIndex) => { - if (notEmpty(relatedRecord[col[col_key]])) { - // console.log('Removing ' + col[col_key] + ' from relatedCols'); - relatedCols.splice(colIndex, 1); - } - }); - }); - - $scope.progress = (index / inventory.length) * 50 + 50; - }); - - // determine hidden columns - const visible = _.reject($scope.columns, (col) => { - if (!col.related) { - return _.find(cols, { id: col.id }); - } - return _.find(relatedCols, { id: col.id }); - }); - - const hidden = _.reject($scope.columns, (col) => _.find(visible, { id: col.id })); - - _.forEach(hidden, (col) => { - col.visible = false; - }); - - const columns = []; - _.forEach(visible, (col) => { - columns.push({ - column_name: col.column_name, - id: col.id, - order: columns.length + 1, - pinned: col.pinnedLeft, - table_name: col.table_name - }); - }); + $scope.start = () => { + $scope.state = 'running'; + $scope.status = 'Processing...'; + $scope.inventory_type = inventory_type === 'properties' ? 'Property' : 'Tax lot'; - const { id } = $scope.currentProfile; - const profile = _.omit($scope.currentProfile, 'id'); - profile.columns = columns; - inventory_service.update_column_list_profile(id, profile).then((/* updatedProfile */) => { + inventory_service.update_column_list_profile_to_show_populated(currentProfile.id, cycle.id, $scope.inventory_type).then((/* updatedProfile */) => { modified_service.resetModified(); $scope.progress = 100; $scope.state = 'done'; - $scope.status = `Found ${visible.length} populated columns`; + $scope.refresh(); }); }; - $scope.start = () => { - $scope.state = 'running'; - $scope.status = 'Fetching Inventory'; - - if (provided_inventory) { - update_profile_with_populated_columns(provided_inventory); - } else { - const page = 1; - const chunk = 5000; - fetch(page, chunk).then(update_profile_with_populated_columns); - } - }; - $scope.refresh = () => { spinner_utility.show(); + $uibModalInstance.close(); $window.location.reload(); }; diff --git a/seed/static/seed/js/controllers/update_derived_data_modal_controller.js b/seed/static/seed/js/controllers/update_derived_data_modal_controller.js index b5ab463f73..6294808cd2 100644 --- a/seed/static/seed/js/controllers/update_derived_data_modal_controller.js +++ b/seed/static/seed/js/controllers/update_derived_data_modal_controller.js @@ -4,18 +4,52 @@ */ angular.module('SEED.controller.update_derived_data_modal', []).controller('update_derived_data_modal_controller', [ '$scope', + '$state', '$q', '$uibModalInstance', 'inventory_service', + 'Notification', + 'uploader_service', 'property_view_ids', 'taxlot_view_ids', // eslint-disable-next-line func-names - function ($scope, $q, $uibModalInstance, inventory_service, property_view_ids, taxlot_view_ids) { + function ( + $scope, + $state, + $q, + $uibModalInstance, + inventory_service, + Notification, + uploader_service, + property_view_ids, + taxlot_view_ids + ) { $scope.property_view_ids = _.uniq(property_view_ids); $scope.taxlot_view_ids = _.uniq(taxlot_view_ids); + $scope.status = 'ready'; + $scope.uploader = { progress: 0 }; + $scope.begin_update = () => { - inventory_service.update_derived_data($scope.property_view_ids, $scope.taxlot_view_ids).then($uibModalInstance.close); + inventory_service.update_derived_data($scope.property_view_ids, $scope.taxlot_view_ids).then((data) => { + Notification.primary('Updating derived columns. This may take a few minutes...'); + $scope.status = 'in progress'; + + const resultHandler = (notification_type, message) => { + $uibModalInstance.close(); + Notification[notification_type](message); + $state.reload(); + }; + + uploader_service.check_progress_loop( + data.progress_key, + 0, + 1, + () => resultHandler('success', 'Derived columns updated'), + () => resultHandler('error', 'Unexpected Error'), + $scope.uploader + ); + }); }; /** diff --git a/seed/static/seed/js/seed.js b/seed/static/seed/js/seed.js index 3a714f53ae..b169b7e022 100644 --- a/seed/static/seed/js/seed.js +++ b/seed/static/seed/js/seed.js @@ -2357,6 +2357,23 @@ return promise; } + ], + columns: [ + '$stateParams', + 'inventory_service', + ($stateParams, inventory_service) => { + if ($stateParams.inventory_type === 'properties') { + return inventory_service.get_property_columns().then((columns) => { + _.remove(columns, 'related'); + _.remove(columns, { column_name: 'lot_number', table_name: 'PropertyState' }); + return _.map(columns, (col) => _.omit(col, ['pinnedLeft', 'related'])); + }); + } + return inventory_service.get_taxlot_columns().then((columns) => { + _.remove(columns, 'related'); + return _.map(columns, (col) => _.omit(col, ['pinnedLeft', 'related'])); + }); + } ] } }) diff --git a/seed/static/seed/js/services/goal_service.js b/seed/static/seed/js/services/goal_service.js index 819b98cddf..802811cea0 100644 --- a/seed/static/seed/js/services/goal_service.js +++ b/seed/static/seed/js/services/goal_service.js @@ -78,6 +78,46 @@ angular.module('SEED.service.goal', []).factory('goal_service', [ .then((response) => response) .catch((response) => response); + const format_column_filters = (column_filters) => { + if (!column_filters) { + return {}; + } + const filters = {}; + for (const { name, operator, value } of column_filters) { + filters[`${name}__${operator}`] = value; + } + return filters; + }; + + const format_column_sorts = (column_sorts) => { + if (!column_sorts) { + return []; + } + + const result = []; + for (const { name, direction } of column_sorts) { + const direction_operator = direction === 'desc' ? '-' : ''; + result.push(`${direction_operator}${name}`); + } + + return { order_by: result }; + }; + + goal_service.load_data = (data, filters, sorts) => { + const params = { + organization_id: user_service.get_organization().id, + ...format_column_filters(filters), + ...format_column_sorts(sorts) + }; + return $http.put( + `/api/v3/goals/${data.goal_id}/data/`, + data, + { params } + ) + .then((response) => response) + .catch((response) => response); + }; + return goal_service; } ]); diff --git a/seed/static/seed/js/services/inventory_service.js b/seed/static/seed/js/services/inventory_service.js index a7d4905091..d916bcb5a3 100644 --- a/seed/static/seed/js/services/inventory_service.js +++ b/seed/static/seed/js/services/inventory_service.js @@ -64,9 +64,7 @@ angular.module('SEED.service.inventory', []).factory('inventory_service', [ ids_only = null, shown_column_ids = null, access_level_instance_id = null, - include_property_ids = null, - goal_id = null, - related_model_sort = null + include_property_ids = null ) => { organization_id = organization_id ?? user_service.get_organization().id; @@ -103,9 +101,7 @@ angular.module('SEED.service.inventory', []).factory('inventory_service', [ // Pass the current profile (if one exists) to limit the column data that is returned profile_id, // conditionally add optional params - ...(access_level_instance_id && { access_level_instance_id }), - ...(goal_id && { goal_id }), - ...(related_model_sort && { related_model_sort }) + ...(access_level_instance_id && { access_level_instance_id }) }; // add access_level_instance if it exists @@ -1200,6 +1196,26 @@ angular.module('SEED.service.inventory', []).factory('inventory_service', [ .then((response) => response.data.data); }; + inventory_service.update_column_list_profile_to_show_populated = (id, cycle_id, inventory_type) => { + if (id === null) { + Notification.error('This settings profile is protected from modifications'); + return $q.reject(); + } + return $http + .put( + `/api/v3/column_list_profiles/${id}/show_populated/`, + { + cycle_id, + inventory_type + }, + { + params: { + organization_id: user_service.get_organization().id + } + } + ).then((response) => response.data.data); + }; + inventory_service.remove_column_list_profile = (id) => { if (id === null) { Notification.error('This settings profile is protected from modifications'); diff --git a/seed/static/seed/locales/en_US.json b/seed/static/seed/locales/en_US.json index 49690c1181..626162c3b7 100644 --- a/seed/static/seed/locales/en_US.json +++ b/seed/static/seed/locales/en_US.json @@ -35,11 +35,11 @@ "ALSO_DELETE_BATCH_ANALYSES": "Also delete {num,plural, one{1 associated batch analysis} other{# associated batch analyses}}.", "ANALYSIS_DESCRIPTION_BETTER": "The BETTER analysis leverages better.lbl.gov to calculate energy, cost, and GHG emission savings by comparing the property's change point model with a benchmarked model. The results include saving potential and a list of recommended high-level energy conservation measures.", "ANALYSIS_DESCRIPTION_BSyncr": "The BSyncr analysis leverages the Normalized Metered Energy Consumption (NMEC) analysis to calculate a change point model. The data are passed to the analysis using BuildingSync. The result of the analysis are the coefficients of the change point model.", - "ANALYSIS_DESCRIPTION_BuildingUpgradeRecommendation": "The Building Upgrade Recommendation analysis implements a workflow to identify buildings that may need a deep energy retrofit, equipment replaced or re-tuning based on building attributes such as energy use, year built, and square footage.", + "ANALYSIS_DESCRIPTION_BuildingUpgradeRecommendation": "The Building Upgrade Recommendation analysis implements a workflow to identify buildings that may need a deep energy retrofit, equipment replaced or re-tuning based on building attributes such as energy use, year built, and square footage. If your organization contains elements, the Element Statistics Analysis should be run prior to running this analysis.", "ANALYSIS_DESCRIPTION_CO2": "This analysis calculates the average annual CO2 emissions for the property's meter data. The analysis requires an eGRID Subregion to be defined in order to accurately determine the emission rates.", "ANALYSIS_DESCRIPTION_EEEJ": "The EEEJ Analysis uses each property's address to identify the 2010 census tract. Based on census tract, disadvantaged community classification and energy burden information can be retrieved from the CEJST dataset. The number of affordable housing locations is retrieved from HUD datasets. Location is used to generate a link to view an EJScreen Report providing more demographic indicators.", "ANALYSIS_DESCRIPTION_EUI": "The EUI analysis will sum the property's meter readings for the last twelve months to calculate the energy use per square footage per year. If there are missing meter readings, then the analysis will return a less that 100% coverage to alert the user that there is a missing meter reading.", - "ANALYSIS_DESCRIPTION_ElementStatistics": "The Element Statistics analysis looks through a property's element data (if any) to count the number of elements of type 'D.D.C. Control Panel' and saves that quantity to the property", + "ANALYSIS_DESCRIPTION_ElementStatistics": "The Element Statistics analysis looks through a property's element data (if any) to count the number of elements of type 'D.D.C. Control Panel'. It also generates the aggregated (average) condition index values for scope 1 emission elements and saves those quantities to the property.", "AND": "AND", "API Documentation": "API Documentation", "API Key": "API Key", @@ -124,6 +124,7 @@ "Are you sure you want to delete the filter group": "Are you sure you want to delete the filter group", "Are you sure you want to unmerge these properties and then merge with the selected properties?": "Are you sure you want to unmerge these properties and then merge with the selected properties?", "Are you sure you want to unmerge these tax lots and then merge with the selected tax lots?": "Are you sure you want to unmerge these tax lots and then merge with the selected tax lots?", + "Are you sure you want to unmerge these tax lots?": "Are you sure you want to unmerge these tax lots?", "Area": "Area", "Area Column": "Area Column", "Area Target Column": "Area Target Column", @@ -164,6 +165,7 @@ "Building Filters": "Building Filters", "Building Performance Standard (BPS) Compliance Pathway Support": "Building Performance Standard (BPS) Compliance Pathway Support", "Building Square Footage Threshold": "Building Square Footage Threshold", + "Building has BAS field": "Building has BAS field", "BuildingSync Recommended Measures": "BuildingSync Recommended Measures", "Buildings": "Buildings", "By clicking the Log In button you accept the NREL Data Terms.": "By clicking the Log In button you accept the NREL Data Terms.", @@ -590,6 +592,7 @@ "Goal": "Goal", "Goal Setup": "Goal Setup", "Gross Floor Area": "Gross Floor Area", + "HAS_BAS_HELP": "Select the field that indicates whether or not the building has a Building Automation System (BAS). This analysis expects a boolean field.", "HOW THE SYSTEM AUTO-MATCHES YOUR PROPERTIES AND TAX LOTS:": "HOW THE SYSTEM AUTO-MATCHES YOUR PROPERTIES AND TAX LOTS:", "HOW TO MANUALLY MATCH YOUR PROPERTIES AND TAX LOTS:": "HOW TO MANUALLY MATCH YOUR PROPERTIES AND TAX LOTS:", "HOW_SYSTEM_AUTO_MATCHES_EXPLANATION": "Your source data file(s) are presented in the table on the left. All properties\/tax lots where a possible data match exists are presented in a table on the right. The system attempts to auto-match records using shared unique IDs like: PM Property ID, Jurisdiction Tax Lot ID, and Custom IDs as well as Address information. Where the system believes a match exists between a record in your source file and an existing record it will auto-check the 'match' <\/i> checkbox — effectively making a match between these records.", @@ -966,6 +969,7 @@ "Properties failed to update:": "Properties failed to update:", "Properties updated:": "Properties updated:", "Properties with Data": "Properties with Data", + "Properties with elements cannot be unmerged.": "Properties with elements cannot be unmerged.", "Property": "Property", "Property Classification": "Property Classification", "Property Column List Profiles": "Property Column List Profiles", @@ -1296,7 +1300,7 @@ "This email link is invalid.": "This email link is invalid.", "This label name is already taken.": "This label name is already taken.", "This password must contain at least %(quantity)d %(type)s characters.": "This password must contain at least %(quantity)d %(type)s characters.", - "This will reset your visible columns and column order to only columns that contain data. Are you sure you want to continue?": "This will reset your visible columns and column order to only non-derived columns that contain data. Are you sure you want to continue?", + "This will reset your visible columns and column order to only columns that contain data. Are you sure you want to continue?": "This will reset your visible columns and column order to show only columns that contain data. Are you sure you want to continue?", "Time Period": "Time Period", "Timeline": "Timeline", "To Date": "To Date", @@ -1335,6 +1339,7 @@ "UBID already exists": "UBID already exists", "UBID comparison result:": "UBID comparison result:", "UBID thresholds are between 0.0 and 1.0": "UBID thresholds are between 0.0 and 1.0", + "UNMERGE_PROPERTIES_MODAL": "Are you sure you want to unmerge these properties? The unmerge action will keep meter data on both properties. Review meter data on both properties once they are unmerged.", "UPDATE_DERIVED_DATA_TEXT_1": "Press the 'Begin Update' button below to begin the process of recalculating the data stored in derived columns for the selected properties and tax lots. The derived columns will appear yellow to indicate that the update is in progress.", "UPDATE_DERIVED_DATA_TEXT_2": "Note that while the columns are yellow, the data may not be up to date. Refresh the page to refresh the columns status.", "UPDATE_MANUALLY_GEOCODED_INSTRUCTIONS": "If the properties have been manually geocoded, then they will not be updated. To update geocoding, remove the lat\/long.", diff --git a/seed/static/seed/locales/es.json b/seed/static/seed/locales/es.json index 27f4e615f6..4c6ac34b63 100644 --- a/seed/static/seed/locales/es.json +++ b/seed/static/seed/locales/es.json @@ -35,11 +35,11 @@ "ALSO_DELETE_BATCH_ANALYSES": "Borre también {num,plural, one{1 análisis de lotes asociados} other{# análisis de lotes asociados}}.", "ANALYSIS_DESCRIPTION_BETTER": "El análisis BETTER aprovecha better.lbl.gov para calcular el ahorro de energía, costes y emisiones de GEI comparando el modelo de puntos de cambio del inmueble con un modelo de referencia. Los resultados incluyen el potencial de ahorro y una lista de medidas de conservación de energía de alto nivel recomendadas.", "ANALYSIS_DESCRIPTION_BSyncr": "El análisis BSyncr aprovecha el análisis de consumo energético medido normalizado (NMEC) para calcular un modelo de punto de cambio. Los datos se transmiten al análisis mediante BuildingSync. El resultado del análisis son los coeficientes del modelo de punto de cambio.", - "ANALYSIS_DESCRIPTION_BuildingUpgradeRecommendation": "El análisis de recomendaciones de mejora de edificios implementa un flujo de trabajo para identificar los edificios que pueden necesitar una profunda modernización energética, la sustitución de equipos o una nueva puesta a punto basada en atributos del edificio como el uso de la energía, el año de construcción y los metros cuadrados.", + "ANALYSIS_DESCRIPTION_BuildingUpgradeRecommendation": "El análisis de recomendación de mejora de edificios implementa un flujo de trabajo para identificar los edificios que pueden necesitar una renovación energética profunda, el reemplazo o la puesta a punto de equipos en función de los atributos del edificio, como el uso de energía, el año de construcción y la superficie en metros cuadrados. Si su organización contiene elementos, se debe ejecutar el análisis de estadísticas de elementos antes de ejecutar este análisis.", "ANALYSIS_DESCRIPTION_CO2": "Este análisis calcula las emisiones medias anuales de CO2 para los datos de los contadores de la propiedad. El análisis requiere que se defina una subregión eGRID para determinar con precisión los índices de emisión.", "ANALYSIS_DESCRIPTION_EEEJ": "El análisis EEEJ utiliza la dirección de cada propiedad para identificar la sección censal de 2010. Basándose en la zona censal, la clasificación de comunidad desfavorecida y la información sobre la carga energética pueden recuperarse del conjunto de datos CEJST. El número de ubicaciones de viviendas asequibles se recupera de los conjuntos de datos del HUD. La ubicación se utiliza para generar un enlace para ver un informe EJScreen que proporciona más indicadores demográficos.", "ANALYSIS_DESCRIPTION_EUI": "El análisis EUI sumará las lecturas de los contadores de la propiedad de los últimos doce meses para calcular el consumo de energía por metro cuadrado al año. Si faltan lecturas de contadores, el análisis devolverá una cobertura inferior al 100% para alertar al usuario de que falta una lectura de contador.", - "ANALYSIS_DESCRIPTION_ElementStatistics": "El análisis de estadísticas de elementos examina los datos de los elementos de una propiedad (si los hay) para contar el número de elementos de tipo \"Panel de control D.D.C.\" y guarda esa cantidad en la propiedad", + "ANALYSIS_DESCRIPTION_ElementStatistics": "El análisis de estadísticas de elementos analiza los datos de elementos de una propiedad (si los hay) para contar la cantidad de elementos del tipo \"Panel de control DDC\". También genera los valores de índice de condición agregados (promedio) para los elementos de emisión de alcance 1 y guarda esas cantidades en la propiedad.", "AND": "Y", "API Documentation": "Documentación API", "API Key": "Clave API", @@ -124,6 +124,7 @@ "Are you sure you want to delete the filter group": "¿Está seguro de que desea eliminar el grupo de filtros?", "Are you sure you want to unmerge these properties and then merge with the selected properties?": "¿Está seguro de que desea deshacer la fusión de estas propiedades y, a continuación, fusionarlas con las propiedades seleccionadas?", "Are you sure you want to unmerge these tax lots and then merge with the selected tax lots?": "¿Está seguro de que desea deshacer la fusión de estos lotes fiscales y, a continuación, fusionarlos con los lotes fiscales seleccionados?", + "Are you sure you want to unmerge these tax lots?": "¿Está seguro de que desea separar estos lotes de impuestos?", "Area": "Zona", "Area Column": "Columna de área", "Area Target Column": "Área Columna objetivo", @@ -164,6 +165,7 @@ "Building Filters": "Filtros para edificios", "Building Performance Standard (BPS) Compliance Pathway Support": "Apoyo para el cumplimiento de las normas de rendimiento de edificios (BPS)", "Building Square Footage Threshold": "Umbral de metros cuadrados del edificio", + "Building has BAS field": "El edificio tiene campo BAS", "BuildingSync Recommended Measures": "BuildingSync Medidas recomendadas", "Buildings": "Edificios", "By clicking the Log In button you accept the NREL Data Terms.": "Al hacer clic en el botón Iniciar sesión, acepta las Condiciones de datos del NREL.", @@ -590,6 +592,7 @@ "Goal": "Objetivo", "Goal Setup": "Configuración de la meta", "Gross Floor Area": "Superficie bruta", + "HAS_BAS_HELP": "Seleccione el campo que indica si el edificio tiene o no un sistema de automatización de edificios (BAS). Este análisis espera un campo booleano.", "HOW THE SYSTEM AUTO-MATCHES YOUR PROPERTIES AND TAX LOTS:": "CÓMO EL SISTEMA EMPAREJA AUTOMÁTICAMENTE SUS PROPIEDADES Y LOTES FISCALES:", "HOW TO MANUALLY MATCH YOUR PROPERTIES AND TAX LOTS:": "CÓMO EMPAREJAR MANUALMENTE SUS PROPIEDADES Y LOTES FISCALES:", "HOW_SYSTEM_AUTO_MATCHES_EXPLANATION": "Su(s) archivo(s) de datos de origen se presentan en la tabla de la izquierda. Todas las propiedades\/lotes fiscales en los que existe una posible coincidencia de datos se presentan en una tabla a la derecha. El sistema intenta hacer coincidir automáticamente los registros utilizando identificadores únicos compartidos como: ID de propiedad PM, ID de lote fiscal de jurisdicción e ID personalizados, así como información de dirección. Cuando el sistema cree que existe una coincidencia entre un registro de su archivo de origen y un registro existente, marcará automáticamente la casilla \"coincidencia\" <\/i>— haciendo coincidir estos registros.", @@ -966,6 +969,7 @@ "Properties failed to update:": "No se han podido actualizar las propiedades:", "Properties updated:": "Propiedades actualizadas:", "Properties with Data": "Propiedades con datos", + "Properties with elements cannot be unmerged.": "Las propiedades con elementos no se pueden separar.", "Property": "Propiedad", "Property Classification": "Clasificación de la propiedad", "Property Column List Profiles": "Perfiles de la lista de columnas de propiedades", @@ -1296,7 +1300,7 @@ "This email link is invalid.": "Este enlace de correo electrónico no es válido.", "This label name is already taken.": "Este nombre de etiqueta ya está ocupado.", "This password must contain at least %(quantity)d %(type)s characters.": "Esta contraseña debe contener al menos %(quantity)d %(type)s caracteres.", - "This will reset your visible columns and column order to only columns that contain data. Are you sure you want to continue?": "Esto restablecerá las columnas visibles y el orden de las columnas a sólo las columnas no derivadas que contengan datos. ¿Está seguro de que desea continuar?", + "This will reset your visible columns and column order to only columns that contain data. Are you sure you want to continue?": "Esto restablecerá las columnas visibles y el orden de las columnas a sólo las columnas que contengan datos. ¿Está seguro de que desea continuar?", "Time Period": "Periodo de tiempo", "Timeline": "Cronología", "To Date": "Hasta la fecha", @@ -1335,6 +1339,7 @@ "UBID already exists": "El UBID ya existe", "UBID comparison result:": "Resultado de la comparación de UBID:", "UBID thresholds are between 0.0 and 1.0": "Los umbrales UBID se sitúan entre 0,0 y 1,0", + "UNMERGE_PROPERTIES_MODAL": "¿Está seguro de que desea desvincular estas propiedades? La acción de desvincular conservará los datos del medidor en ambas propiedades. Revise los datos del medidor en ambas propiedades una vez que se hayan desvinculado.", "UPDATE_DERIVED_DATA_TEXT_1": "Pulse el botón \"Iniciar actualización\" para iniciar el proceso de recálculo de los datos almacenados en las columnas derivadas para las propiedades y lotes fiscales seleccionados. Las columnas derivadas aparecerán en amarillo para indicar que la actualización está en curso.", "UPDATE_DERIVED_DATA_TEXT_2": "Tenga en cuenta que mientras las columnas estén en amarillo, los datos pueden no estar actualizados. Actualice la página para actualizar el estado de las columnas.", "UPDATE_MANUALLY_GEOCODED_INSTRUCTIONS": "Si las propiedades se han geocodificado manualmente, no se actualizarán. Para actualizar la geocodificación, elimine la lat\/long.", diff --git a/seed/static/seed/locales/fr_CA.json b/seed/static/seed/locales/fr_CA.json index 456a654a73..76cbede631 100644 --- a/seed/static/seed/locales/fr_CA.json +++ b/seed/static/seed/locales/fr_CA.json @@ -35,11 +35,11 @@ "ALSO_DELETE_BATCH_ANALYSES": "Supprimez également {num,plural, one{ 1 analyse de lot associée } other{# analyses de lot associées }} .", "ANALYSIS_DESCRIPTION_BETTER": "L'analyse BETTER s'appuie sur better.lbl.gov pour calculer les économies d'énergie, de coûts et d'émissions de GES en comparant le modèle de point de changement de la propriété avec un modèle de référence. Les résultats comprennent un potentiel d'économie et une liste de mesures d'économie d'énergie de haut niveau recommandées.", "ANALYSIS_DESCRIPTION_BSyncr": "L'analyse BSyncr exploite l'analyse de consommation d'énergie normalisée mesurée (NMEC) pour calculer un modèle de point de changement. Les données sont transmises à l'analyse à l'aide de BuildingSync. Le résultat de l'analyse sont les coefficients du modèle de point de changement.", - "ANALYSIS_DESCRIPTION_BuildingUpgradeRecommendation": "Cette analyse met en œuvre un flux de travail pour identifier les bâtiments qui peuvent nécessiter une rénovation énergétique en profondeur, un remplacement ou un réajustement de l'équipement en fonction des attributs du bâtiment tels que la consommation d'énergie, l'année de construction et la superficie en pieds carrés.", + "ANALYSIS_DESCRIPTION_BuildingUpgradeRecommendation": "L'analyse des recommandations de mise à niveau des bâtiments met en œuvre un flux de travail pour identifier les bâtiments qui peuvent nécessiter une rénovation énergétique approfondie, un remplacement de l'équipement ou un réajustement en fonction des attributs du bâtiment tels que la consommation d'énergie, l'année de construction et la superficie en pieds carrés. Si votre organisation contient des éléments, l'analyse des statistiques des éléments doit être exécutée avant d'exécuter cette analyse.", "ANALYSIS_DESCRIPTION_CO2": "Cette analyse calcule les émissions annuelles moyennes de CO2 pour les données des compteurs de la propriété. L’analyse nécessite la définition d’une sous-région eGRID afin de déterminer avec précision les taux d’émission.", "ANALYSIS_DESCRIPTION_EEEJ": "L'analyse EEEJ utilise l'adresse de chaque propriété pour identifier le secteur de recensement de 2010. Sur la base des secteurs de recensement, la classification des communautés défavorisées et les informations sur la charge énergétique peuvent être extraites de l'ensemble de données CEJST. Le nombre de logements abordables est extrait des ensembles de données HUD. L'emplacement est utilisé pour générer un lien permettant d'afficher un rapport EJScreen fournissant davantage d'indicateurs démographiques.", "ANALYSIS_DESCRIPTION_EUI": "L'analyse EUI additionnera les relevés de compteurs de la propriété au cours des douze derniers mois pour calculer la consommation d'énergie par pied carré et par an. S'il manque des relevés de compteur, l'analyse renvoie une couverture inférieure à 100 % pour alerter l'utilisateur qu'il manque un relevé de compteur.", - "ANALYSIS_DESCRIPTION_ElementStatistics": "Cette analyse examine les données des éléments d'une propriété (le cas échéant) pour compter le nombre d'éléments de type « Panneau de configuration DDC » et enregistre cette quantité dans la propriété", + "ANALYSIS_DESCRIPTION_ElementStatistics": "L'analyse des statistiques des éléments examine les données des éléments d'une propriété (le cas échéant) pour compter le nombre d'éléments de type « Panneau de contrôle DDC ». Elle génère également les valeurs d'indice de condition agrégées (moyennes) pour les éléments d'émission de portée 1 et enregistre ces quantités dans la propriété.", "AND": "ET", "API Documentation": "Documentation de l'API", "API Key": "Clé API", @@ -124,6 +124,7 @@ "Are you sure you want to delete the filter group": "Êtes-vous sûr de vouloir supprimer le groupe de filtres", "Are you sure you want to unmerge these properties and then merge with the selected properties?": "Êtes-vous sûr de vouloir annuler la fusion de ces propriétés et fusionner avec les propriétés sélectionnées?", "Are you sure you want to unmerge these tax lots and then merge with the selected tax lots?": "Êtes-vous sûr de vouloir annuler la fusion de ces lots fiscaux pour ensuite fusionner avec les lots d'impôt sélectionnés?", + "Are you sure you want to unmerge these tax lots?": "Êtes-vous sûr de vouloir dissocier ces lots fiscaux ?", "Area": "Superficie", "Area Column": "Colonne de Superficie", "Area Target Column": "Colonne Cible de Superficie", @@ -164,6 +165,7 @@ "Building Filters": "Filtres de bâtiments", "Building Performance Standard (BPS) Compliance Pathway Support": "Assistance au parcours de conformité aux normes de performance des bâtiments (BPS)", "Building Square Footage Threshold": "Seuil de superficie en pieds carrés du bâtiment", + "Building has BAS field": "Le bâtiment dispose d'un champ SAB", "BuildingSync Recommended Measures": "Mesures recommandées par BuildingSync", "Buildings": "Bâtiments", "By clicking the Log In button you accept the NREL Data Terms.": "En cliquant sur le bouton Connexion, vous acceptez les conditions d'utilisation des données NREL.", @@ -590,6 +592,7 @@ "Goal": "Objectif", "Goal Setup": "Configuration des objectifs", "Gross Floor Area": "Surface brute", + "HAS_BAS_HELP": "Sélectionnez le champ qui indique si le bâtiment est équipé ou non d'un système d'automatisation de bâtiment (SAB). Cette analyse attend un champ booléen.", "HOW THE SYSTEM AUTO-MATCHES YOUR PROPERTIES AND TAX LOTS:": "COMMENT LE SYSTÈME ADAPTE AUTOMATIQUEMENT VOS PROPRIETES ET LOTS D'IMPÔT:", "HOW TO MANUALLY MATCH YOUR PROPERTIES AND TAX LOTS:": "COMMENT APPORTER MANUELLEMENT VOS PROPRIETES ET LOTS D'IMPÔT:", "HOW_SYSTEM_AUTO_MATCHES_EXPLANATION": "Votre fichier de données source est présenté dans le tableau de gauche. Toutes les propriétés\/lots d'impôt où une correspondance de données possible existe sont présentées dans un tableau sur la droite. Le système tente de faire correspondre automatiquement les enregistrements à l'aide d'ID uniques partagés tels que: ID de Propriété PM, ID de Lot d'Impôt de Juridiction et les ID personnalisés, ainsi que des informations d'adresse. Lorsque le système croit qu'une correspondance existe entre un enregistrement de votre fichier source et un enregistrement existant, il vérifie automatiquement la 'correspondance' <\/i> case à cocher — faire efficacement une correspondance entre ces enregistrements.", @@ -966,6 +969,7 @@ "Properties failed to update:": "Les propriétés n'ont pas pu être mises à jour :", "Properties updated:": "Propriétés mises à jour :", "Properties with Data": "Propriétés avec données", + "Properties with elements cannot be unmerged.": "Les propriétés avec des éléments ne peuvent pas être dissociées.", "Property": "Propriété", "Property Classification": "Classification de la propriété", "Property Column List Profiles": "Paramètres de la liste de propriétés", @@ -1296,7 +1300,7 @@ "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.", "This password must contain at least %(quantity)d %(type)s characters.": "Ce mot de passe doit contenir au moins% (quantité) d% (type) s caractères.", - "This will reset your visible columns and column order to only columns that contain data. Are you sure you want to continue?": "Cela réinitialisera vos colonnes visibles et l'ordre des colonnes aux seules colonnes non dérivées contenant des données. Es-tu sur de vouloir continuer?", + "This will reset your visible columns and column order to only columns that contain data. Are you sure you want to continue?": "Cela réinitialisera vos colonnes visibles et l'ordre des colonnes à seulement celles contenant des données. Êtes-vous sur de vouloir continuer?", "Time Period": "Période de temps", "Timeline": "Chronologie", "To Date": "À ce jour", @@ -1335,6 +1339,7 @@ "UBID already exists": "UBID existe déjà", "UBID comparison result:": "Résultat de la comparaison UBID :", "UBID thresholds are between 0.0 and 1.0": "Les seuils UBID sont compris entre 0,0 et 1,0", + "UNMERGE_PROPERTIES_MODAL": "Êtes-vous sûr de vouloir dissocier ces propriétés? L'action de dissociation conservera les données de compteur sur les deux propriétés. Vérifiez les données de compteur sur les deux propriétés une fois qu'elles sont dissociées.", "UPDATE_DERIVED_DATA_TEXT_1": "Appuyez sur le bouton « Démarrer la mise à jour » ci-dessous pour commencer le processus de recalcul des données stockées dans les colonnes dérivées pour les propriétés et les lots fiscaux sélectionnés. Les colonnes dérivées apparaîtront en jaune pour indiquer que la mise à jour est en cours.", "UPDATE_DERIVED_DATA_TEXT_2": "Notez que pendant que les colonnes sont jaunes, les données peuvent ne pas être à jour. Actualisez la page pour actualiser l'état des colonnes.", "UPDATE_MANUALLY_GEOCODED_INSTRUCTIONS": "Si les propriétés ont été géocodées manuellement, elles ne seront pas mises à jour. Pour mettre à jour le géocodage, supprimez le lat \/ long.", diff --git a/seed/static/seed/partials/inventory_detail.html b/seed/static/seed/partials/inventory_detail.html index 5e5767b817..502dfce68b 100644 --- a/seed/static/seed/partials/inventory_detail.html +++ b/seed/static/seed/partials/inventory_detail.html @@ -355,9 +355,9 @@

{$:: 'Property Elements' | translate $}
-
- - +
+
+ @@ -396,8 +396,8 @@

Building Performance Standar
-
-

Uniformat Category Description
+
+
diff --git a/seed/static/seed/partials/inventory_detail_analyses_modal.html b/seed/static/seed/partials/inventory_detail_analyses_modal.html index 281e954b43..a9e1e997c1 100644 --- a/seed/static/seed/partials/inventory_detail_analyses_modal.html +++ b/seed/static/seed/partials/inventory_detail_analyses_modal.html @@ -280,52 +280,52 @@ - - -
- TOTAL_EUI_HELP -
- - -
FOSSIL_FUEL_EUI_HELP
+ + +
HAS_BAS_HELP
+ + + +
+ TOTAL_EUI_HELP +
+ + +
FOSSIL_FUEL_EUI_HELP
- - -
YEAR_BUILT_THRESHOLD_HELP
+ + +
YEAR_BUILT_THRESHOLD_HELP
- - -
FAIR_BENCHMARK_RATIO_HELP
- - -
POOR_BENCHMARK_RATIO_HELP
- - -
SQUARE_FOOTAGE_THRESHOLD_HELP
- - -
CONDITION_INDEX_THRESHOLD_HELP
- - -
FF_RSL_THRESHOLD_HELP
- + + +
FAIR_BENCHMARK_RATIO_HELP
+ + +
POOR_BENCHMARK_RATIO_HELP
+ + +
SQUARE_FOOTAGE_THRESHOLD_HELP
+ + +
CONDITION_INDEX_THRESHOLD_HELP
+ + +
FF_RSL_THRESHOLD_HELP
diff --git a/seed/static/seed/partials/inventory_reports.html b/seed/static/seed/partials/inventory_reports.html index f012df3334..2e8b4936ce 100644 --- a/seed/static/seed/partials/inventory_reports.html +++ b/seed/static/seed/partials/inventory_reports.html @@ -122,7 +122,6 @@

{$:: 'X Axis' | translate $}: -
@@ -134,13 +133,10 @@

CONFIGURE_XY_AXES

- -
-
- -
- +
+ +
@@ -153,6 +149,14 @@

+
+
+ + +
+
@@ -232,28 +236,77 @@

{$ chart2Title | translate $} 

-

Uniformat Category
- - - - - - - - - - - - - - - - - - - - -
AxisAccess Level InstanceSumMin5th Percentile25th PercentileMeanMedian75 Percentile95th PercentileMax
{$ item $}
+

Statistics

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
AxisAccess Level InstanceSumMin5th Percentile25th PercentileMeanMedian75 Percentile95th PercentileMax
{$ key $} + {$ item $} + {$ item | tolerantNumber:2 $} + + + +
+ + + + + + +
 {$ key2 $} + {$ item $} + {$ item | tolerantNumber:2 $} +
+
+
+
diff --git a/seed/static/seed/partials/notes.html b/seed/static/seed/partials/notes.html index f8f6286adb..f8d85022e6 100644 --- a/seed/static/seed/partials/notes.html +++ b/seed/static/seed/partials/notes.html @@ -26,12 +26,13 @@ {$:: note.name $} {$ note.text $} - +
Edit(s):
  • {$:: log.field $} updated from "{$:: log.previous_value $}" to "{$:: log.new_value $}"
+ {$ note.text $} - + + diff --git a/seed/static/seed/scss/style.scss b/seed/static/seed/scss/style.scss index a091629aaf..9136fb2000 100755 --- a/seed/static/seed/scss/style.scss +++ b/seed/static/seed/scss/style.scss @@ -5674,7 +5674,7 @@ tags-input .tags .tag-item { } .bold { - font-size: bold; + font-weight: bold; } } @@ -5683,3 +5683,9 @@ tags-input .tags .tag-item { white-space: normal; min-width: 350px; } + +.no-lr-pad { + padding-left: 0 !important; + padding-right: 0 !important; + padding-top: 0 !important; +} diff --git a/seed/tasks.py b/seed/tasks.py index d8793ceb9c..9e61a209ed 100644 --- a/seed/tasks.py +++ b/seed/tasks.py @@ -7,6 +7,7 @@ import math import sys from datetime import datetime +from random import randint import pytz from celery import chain, chord, shared_task @@ -535,6 +536,11 @@ def set_update_to_now(property_view_ids, taxlot_view_ids, progress_key): @shared_task def update_state_derived_data(property_state_ids=[], taxlot_state_ids=[], derived_column_ids=[]): + progress_data = ProgressData(func_name="update_derived_data", unique_id=randint(10000, 99999)) + progress_data.total = len(property_state_ids) + len(taxlot_state_ids) + progress_data.save() + progress_key = progress_data.key + chunk_size = 100 derived_columns = DerivedColumn.objects.filter(id__in=derived_column_ids) @@ -543,15 +549,19 @@ def update_state_derived_data(property_state_ids=[], taxlot_state_ids=[], derive tasks = [] for chunk_ids in batch(property_state_ids, chunk_size): - tasks.append(_update_property_state_derived_data_chunk.si(chunk_ids, property_derived_column_ids)) + tasks.append(_update_property_state_derived_data_chunk.si(progress_key, chunk_ids, property_derived_column_ids)) for chunk_ids in batch(taxlot_state_ids, chunk_size): - tasks.append(_update_taxlot_state_derived_data_chunk.si(chunk_ids, taxlot_derived_column_ids)) + tasks.append(_update_taxlot_state_derived_data_chunk.si(progress_key, chunk_ids, taxlot_derived_column_ids)) - chord(tasks, interval=15)(_finish_update_state_derived_data.si(property_derived_column_ids + taxlot_derived_column_ids)) + chord(tasks, interval=15)(_finish_update_state_derived_data.si(progress_key, property_derived_column_ids + taxlot_derived_column_ids)) + + return progress_data.result() @shared_task -def _update_property_state_derived_data_chunk(property_state_ids=[], derived_column_ids=[]): +def _update_property_state_derived_data_chunk(progress_key, property_state_ids=[], derived_column_ids=[]): + progress_data = ProgressData.from_key(progress_key) + states = PropertyState.objects.filter(id__in=property_state_ids) derived_columns = DerivedColumn.objects.filter(id__in=derived_column_ids) @@ -559,10 +569,13 @@ def _update_property_state_derived_data_chunk(property_state_ids=[], derived_col for derived_column in derived_columns: state.derived_data[derived_column.name] = derived_column.evaluate(state) state.save() + progress_data.step() @shared_task -def _update_taxlot_state_derived_data_chunk(taxlot_state_ids=[], derived_column_ids=[]): +def _update_taxlot_state_derived_data_chunk(progress_key, taxlot_state_ids=[], derived_column_ids=[]): + progress_data = ProgressData.from_key(progress_key) + states = TaxLotState.objects.filter(id__in=taxlot_state_ids) derived_columns = DerivedColumn.objects.filter(id__in=derived_column_ids) @@ -570,9 +583,13 @@ def _update_taxlot_state_derived_data_chunk(taxlot_state_ids=[], derived_column_ for derived_column in derived_columns: state.derived_data[derived_column.name] = derived_column.evaluate(state) state.save() + progress_data.step() @shared_task -def _finish_update_state_derived_data(derived_column_ids): +def _finish_update_state_derived_data(progress_key, derived_column_ids): derived_columns = DerivedColumn.objects.filter(id__in=derived_column_ids) Column.objects.filter(derived_column__in=derived_columns).update(is_updating=False) + + progress_data = ProgressData.from_key(progress_key) + progress_data.finish_with_success("Updated Derived Data") diff --git a/seed/tests/test_column_list_profiles_views.py b/seed/tests/test_column_list_profiles_views.py index e4843f948d..5939ec29e3 100644 --- a/seed/tests/test_column_list_profiles_views.py +++ b/seed/tests/test_column_list_profiles_views.py @@ -9,9 +9,16 @@ from rest_framework import status from seed.landing.models import SEEDUser as User -from seed.models import Column +from seed.models import VIEW_LIST_TAXLOT, Column from seed.models.derived_columns import DerivedColumn -from seed.test_helpers.fake import FakeColumnListProfileFactory +from seed.test_helpers.fake import ( + FakeColumnListProfileFactory, + FakeCycleFactory, + FakePropertyStateFactory, + FakePropertyViewFactory, + FakeTaxLotStateFactory, + FakeTaxLotViewFactory, +) from seed.tests.util import AccessLevelBaseTestCase, DeleteModelsTestCase from seed.utils.organizations import create_organization @@ -26,6 +33,15 @@ def setUp(self): self.user = User.objects.create_superuser(**user_details) self.org, _, _ = create_organization(self.user, "test-organization-a") + self.cycle_factory = FakeCycleFactory(organization=self.org, user=self.user) + self.cycle = self.cycle_factory.get_cycle() + + self.column_list_factory = FakeColumnListProfileFactory(organization=self.org) + self.property_view_factory = FakePropertyViewFactory(organization=self.org, cycle=self.cycle) + self.property_state_factory = FakePropertyStateFactory(organization=self.org) + self.taxlot_view_factory = FakeTaxLotViewFactory(organization=self.org, cycle=self.cycle) + self.taxlot_state_factory = FakeTaxLotStateFactory(organization=self.org) + self.column_1 = Column.objects.get(organization=self.org, table_name="PropertyState", column_name="address_line_1") self.column_2 = Column.objects.get(organization=self.org, table_name="PropertyState", column_name="city") self.column_3 = Column.objects.create( @@ -193,6 +209,84 @@ def test_update_column_profile(self): self.assertEqual(result["data"]["columns"][0]["order"], 999) self.assertEqual(result["data"]["columns"][0]["pinned"], True) + def test_column_profile_show_populated(self): + # Set Up + columnlistprofile = self.column_list_factory.get_columnlistprofile(columns=["address_line_1", "city"]) + state = self.property_state_factory.get_property_state(no_default_data=True, city="Denver") + self.property_view_factory.get_property_view(state=state) + + # Action + response = self.client.put( + reverse("api:v3:column_list_profiles-show-populated", args=[columnlistprofile.id]) + f"?organization_id={self.org.id}", + data=json.dumps({"cycle_id": self.cycle.id, "inventory_type": "Property"}), + content_type="application/json", + ) + result = json.loads(response.content) + + # Assertion + self.assertEqual(response.status_code, status.HTTP_200_OK) + columns = {c["column_name"] for c in result["data"]["columns"]} + self.assertSetEqual(columns, {"city", "updated", "created"}) + + def test_column_profile_show_populated_taxlots(self): + columnlistprofile = self.column_list_factory.get_columnlistprofile( + columns=["address_line_1", "city"], inventory_type=VIEW_LIST_TAXLOT + ) + state = self.taxlot_state_factory.get_taxlot_state(no_default_data=True, longitude=12345) + self.taxlot_view_factory.get_taxlot_view(state=state) + + response = self.client.put( + reverse("api:v3:column_list_profiles-show-populated", args=[columnlistprofile.id]) + f"?organization_id={self.org.id}", + data=json.dumps({"cycle_id": self.cycle.id, "inventory_type": "Tax Lot"}), + content_type="application/json", + ) + result = json.loads(response.content) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + columns = {c["column_name"] for c in result["data"]["columns"]} + self.assertSetEqual(columns, {"updated", "longitude", "created"}) + + def test_column_profile_show_populated_extra_data(self): + # Set Up + columnlistprofile = self.column_list_factory.get_columnlistprofile(columns=["address_line_1", "city"]) + state = self.property_state_factory.get_property_state(no_default_data=True, extra_data={self.column_3.column_name: "Denver"}) + self.property_view_factory.get_property_view(state=state) + + # Action + response = self.client.put( + reverse("api:v3:column_list_profiles-show-populated", args=[columnlistprofile.id]) + f"?organization_id={self.org.id}", + data=json.dumps({"cycle_id": self.cycle.id, "inventory_type": "Property"}), + content_type="application/json", + ) + result = json.loads(response.content) + + # Assertion + self.assertEqual(response.status_code, status.HTTP_200_OK) + columns = {c["column_name"] for c in result["data"]["columns"]} + self.assertSetEqual(columns, {self.column_3.column_name, "updated", "created"}) + + def test_column_profile_show_populated_derived_data(self): + # Set Up + self.derived_column = DerivedColumn.objects.create(name="dc", expression="$a + 10", organization=self.org, inventory_type=0) + columnlistprofile = self.column_list_factory.get_columnlistprofile(columns=["address_line_1", "city"]) + state = self.property_state_factory.get_property_state( + no_default_data=True, derived_data={self.derived_column.column.column_name: "20"} + ) + self.property_view_factory.get_property_view(state=state) + + # Action + response = self.client.put( + reverse("api:v3:column_list_profiles-show-populated", args=[columnlistprofile.id]) + f"?organization_id={self.org.id}", + data=json.dumps({"cycle_id": self.cycle.id, "inventory_type": "Property"}), + content_type="application/json", + ) + result = json.loads(response.content) + + # Assertion + self.assertEqual(response.status_code, status.HTTP_200_OK) + columns = {c["column_name"] for c in result["data"]["columns"]} + self.assertSetEqual(columns, {self.derived_column.column.column_name, "updated", "created"}) + class ColumnsListProfileViewPermissionsTests(AccessLevelBaseTestCase, DeleteModelsTestCase): def setUp(self): diff --git a/seed/tests/test_goals.py b/seed/tests/test_goals.py index 49f8bcfb14..1c73c52a94 100644 --- a/seed/tests/test_goals.py +++ b/seed/tests/test_goals.py @@ -91,7 +91,7 @@ def setUp(self): self.view11 = self.property_view_factory.get_property_view(prprty=self.property1, state=self.state_11, cycle=self.cycle1) self.view13 = self.property_view_factory.get_property_view(prprty=self.property1, state=self.state_13, cycle=self.cycle3) self.view2 = self.property_view_factory.get_property_view(prprty=self.property2, state=self.state_2, cycle=self.cycle2) - self.view21 = self.property_view_factory.get_property_view(prprty=self.property3, state=self.state_31, cycle=self.cycle1) + self.view31 = self.property_view_factory.get_property_view(prprty=self.property3, state=self.state_31, cycle=self.cycle1) self.view33 = self.property_view_factory.get_property_view(prprty=self.property3, state=self.state_33, cycle=self.cycle3) self.view41 = self.property_view_factory.get_property_view(prprty=self.property4, state=self.state_41, cycle=self.cycle1) @@ -486,3 +486,114 @@ def test_portfolio_summary(self): } assert summary == exp_summary + + def test_goal_data(self): + self.login_as_root_member() + url = reverse_lazy("api:v3:goals-data", args=[self.root_goal.id]) + "?organization_id=" + str(self.org.id) + data = { + "goal_id": self.root_goal.id, + "page": 1, + "per_page": 50, + "baseline_first": True, + "access_level_instance_id": self.org.root.id, + "related_model_sort": False, + } + response = self.client.put(url, data=json.dumps(data), content_type="application/json") + assert response.status_code == 200 + data = response.json() + assert list(data.keys()) == ["pagination", "properties", "property_lookup"] + + data = { + "goal_id": self.root_goal.id, + "page": 2, + "per_page": 1, + "baseline_first": True, + "access_level_instance_id": self.org.root.id, + "related_model_sort": False, + } + response = self.client.put(url, data=json.dumps(data), content_type="application/json") + data = response.json() + assert len(data["properties"]) == 1 + assert data["property_lookup"] == {str(self.view31.id): self.property3.id, str(self.view33.id): self.property3.id} + + def test_related_filter(self): + alphabet = ["a", "c", "b"] + questions = ["Is this value correct?", "Are these values correct?", "Other or multiple flags; explain in Additional Notes field"] + booleans = [True, False, True] + for idx, goal_note in enumerate(self.root_goal.goalnote_set.all()): + goal_note.resolution = alphabet[idx] + goal_note.question = questions[idx] + goal_note.passed_checks = booleans[idx] + goal_note.new_or_acquired = booleans[idx] + goal_note.save() + + for idx, historical_note in enumerate(HistoricalNote.objects.filter(property__in=self.root_goal.properties())): + historical_note.text = alphabet[idx] + historical_note.save() + + goal_note = self.root_goal.goalnote_set.first() + goal_note.new_or_acquired = True + goal_note.passed_checks = True + goal_note.save() + + # sort resolution ascending + params = f"?organization_id={self.org.id}&order_by=property__goal_note__resolution" + path = reverse_lazy("api:v3:goals-data", args=[self.root_goal.id]) + url = path + params + data = { + "goal_id": self.root_goal.id, + "page": 1, + "per_page": 50, + "baseline_first": True, + "access_level_instance_id": self.org.root.id, + "related_model_sort": True, + } + response = self.client.put(url, data=json.dumps(data), content_type="application/json") + assert response.status_code == 200 + response = response.json() + resolutions = [p["goal_note"]["resolution"] for p in response["properties"]] + assert resolutions == ["a", "b", "c"] + + # sort resolution descending + params = f"?organization_id={self.org.id}&order_by=-property__goal_note__resolution" + url = path + params + response = self.client.put(url, data=json.dumps(data), content_type="application/json") + response = response.json() + resolutions = [p["goal_note"]["resolution"] for p in response["properties"]] + assert resolutions == ["c", "b", "a"] + + # sort historical note text + params = f"?organization_id={self.org.id}&order_by=property__historical_note__text" + url = path + params + response = self.client.put(url, data=json.dumps(data), content_type="application/json") + response = response.json() + historical_notes = [p["historical_note"]["text"] for p in response["properties"]] + assert historical_notes == ["a", "b", "c"] + + # sort question + params = f"?organization_id={self.org.id}&order_by=property__goal_note__question" + url = path + params + response = self.client.put(url, data=json.dumps(data), content_type="application/json") + response = response.json() + questions = [p["goal_note"]["question"] for p in response["properties"]] + assert questions == [ + "Are these values correct?", + "Is this value correct?", + "Other or multiple flags; explain in Additional Notes field", + ] + + # sort passsed checks + params = f"?organization_id={self.org.id}&order_by=property__goal_note__passed_checks" + url = path + params + response = self.client.put(url, data=json.dumps(data), content_type="application/json") + response = response.json() + passed_checks = [p["goal_note"]["passed_checks"] for p in response["properties"]] + assert passed_checks == [True, True, False] + + # sort new or acquired desc + params = f"?organization_id={self.org.id}&order_by=-property__goal_note__new_or_acquired" + url = path + params + response = self.client.put(url, data=json.dumps(data), content_type="application/json") + response = response.json() + passed_checks = [p["goal_note"]["passed_checks"] for p in response["properties"]] + assert passed_checks == [False, True, True] diff --git a/seed/tests/test_property_views.py b/seed/tests/test_property_views.py index 970c414262..a00f7068b4 100644 --- a/seed/tests/test_property_views.py +++ b/seed/tests/test_property_views.py @@ -1738,21 +1738,6 @@ def test_properties_unmerge_without_losing_labels(self): label_names = list(new_view.labels.values_list("name", flat=True)) self.assertCountEqual(label_names, [label_1.name, label_2.name]) - def test_unmerging_assigns_new_canonical_records_to_each_resulting_records(self): - # Capture old property_ids - view = PropertyView.objects.first() # There's only one PropertyView - existing_property_ids = [ - view.property_id, - self.property_1.id, - self.property_2.id, - ] - - # Unmerge the properties - url = reverse("api:v3:properties-unmerge", args=[view.id]) + f"?organization_id={self.org.pk}" - self.client.put(url, content_type="application/json") - - self.assertFalse(PropertyView.objects.filter(property_id__in=existing_property_ids).exists()) - def test_unmerging_two_properties_with_meters_gives_meters_to_both_of_the_resulting_records(self): # Unmerge the properties view_id = PropertyView.objects.first().id # There's only one PropertyView @@ -1782,6 +1767,23 @@ def test_unmerging_two_properties_with_meters_gives_meters_to_both_of_the_result self.assertEqual(reading_sets[0], reading_sets[1]) + def test_unmerging_two_properties_with_notes(self): + view = PropertyView.objects.first() # There's only one PropertyView + + # add note + note_factory = FakeNoteFactory(organization=self.org, user=self.user) + note1 = note_factory.get_note(name="non_default_name_1") + view.notes.add(note1) + + # Unmerge the properties + url = reverse("api:v3:properties-unmerge", args=[view.id]) + f"?organization_id={self.org.pk}" + self.client.put(url, content_type="application/json") + view_1, view_2 = PropertyView.objects.all() + + assert view_1.notes.count() == 1 + assert view_2.notes.count() == 2 + assert view_2.notes.first().text == f"This PropertyView was unmerged from PropertyView id {view_1.id}" + def test_unmerge_results_in_the_use_of_new_canonical_records_and_deletion_of_old_canonical_state_if_unrelated_to_any_views(self): # Capture "old" property_id - there's only one PropertyView view = PropertyView.objects.first() @@ -1791,7 +1793,7 @@ def test_unmerge_results_in_the_use_of_new_canonical_records_and_deletion_of_old url = reverse("api:v3:properties-unmerge", args=[view.id]) + f"?organization_id={self.org.pk}" self.client.put(url, content_type="application/json") - self.assertFalse(Property.objects.filter(pk=property_id).exists()) + self.assertTrue(Property.objects.filter(pk=property_id).exists()) self.assertEqual(Property.objects.count(), 2) def test_unmerge_results_in_the_persistence_of_old_canonical_state_if_related_to_any_views(self): @@ -1808,7 +1810,7 @@ def test_unmerge_results_in_the_persistence_of_old_canonical_state_if_related_to self.client.put(url, content_type="application/json") self.assertTrue(Property.objects.filter(pk=view.property_id).exists()) - self.assertEqual(Property.objects.count(), 3) + self.assertEqual(Property.objects.count(), 2) class PropertyViewExportTests(DataMappingBaseTestCase): diff --git a/seed/tests/test_taxlot_views.py b/seed/tests/test_taxlot_views.py index ad970f038b..cbf6a07856 100644 --- a/seed/tests/test_taxlot_views.py +++ b/seed/tests/test_taxlot_views.py @@ -788,9 +788,34 @@ def test_unmerge_results_in_the_use_of_new_canonical_taxlots_and_deletion_of_old url = reverse("api:v3:taxlots-unmerge", args=[view.id]) + f"?organization_id={self.org.pk}" self.client.post(url, content_type="application/json") - self.assertFalse(TaxLot.objects.filter(pk=taxlot_id).exists()) + self.assertTrue(TaxLot.objects.filter(pk=taxlot_id).exists()) self.assertEqual(TaxLot.objects.count(), 2) + def test_unmerging_two_taxlots_with_notes(self): + # Merge the taxlots + url = reverse("api:v3:taxlots-merge") + f"?organization_id={self.org.pk}" + post_params = json.dumps( + { + "taxlot_view_ids": [self.view_2.pk, self.view_1.pk] # priority given to state_1 + } + ) + self.client.post(url, post_params, content_type="application/json") + view = TaxLotView.objects.first() # There's only one PropertyView + + # add note + note_factory = FakeNoteFactory(organization=self.org, user=self.user) + note1 = note_factory.get_note(name="non_default_name_1") + view.notes.add(note1) + + # Unmerge the taxlots + url = reverse("api:v3:taxlots-unmerge", args=[view.id]) + f"?organization_id={self.org.pk}" + self.client.post(url, content_type="application/json") + view_1, view_2 = TaxLotView.objects.all() + + assert view_1.notes.count() == 1 + assert view_2.notes.count() == 2 + assert view_2.notes.first().text == f"This TaxLotView was unmerged from TaxLotView id {view_1.id}" + def test_unmerge_results_in_the_persistence_of_old_canonical_state_if_related_to_any_views(self): # Merge the taxlots url = reverse("api:v3:taxlots-merge") + f"?organization_id={self.org.pk}" @@ -814,7 +839,7 @@ def test_unmerge_results_in_the_persistence_of_old_canonical_state_if_related_to self.client.post(url, content_type="application/json") self.assertTrue(TaxLot.objects.filter(pk=view.taxlot_id).exists()) - self.assertEqual(TaxLot.objects.count(), 3) + self.assertEqual(TaxLot.objects.count(), 2) class TaxLotUpdateCausesMergesAndLinkTests(AccessLevelBaseTestCase): diff --git a/seed/utils/inventory_filter.py b/seed/utils/inventory_filter.py index cbc41891eb..26163168ef 100644 --- a/seed/utils/inventory_filter.py +++ b/seed/utils/inventory_filter.py @@ -25,7 +25,7 @@ TaxLotView, ) from seed.serializers.pint import apply_display_unit_preferences -from seed.utils.search import FilterError, build_related_model_filters_and_sorts, build_view_filters_and_sorts +from seed.utils.search import FilterError, build_view_filters_and_sorts def get_filtered_results(request: Request, inventory_type: Literal["property", "taxlot"], profile_id: int) -> JsonResponse: @@ -37,8 +37,6 @@ def get_filtered_results(request: Request, inventory_type: Literal["property", " # check if there is a query parameter for the profile_id. If so, then use that one profile_id = request.query_params.get("profile_id", profile_id) shown_column_ids = request.query_params.get("shown_column_ids") - goal_id = request.data.get("goal_id") - related_model_sort = request.data.get("related_model_sort") if not org_id: return JsonResponse( @@ -103,15 +101,10 @@ def get_filtered_results(request: Request, inventory_type: Literal["property", " only_used=False, include_related=include_related, ) - try: - # Sorts initiated from Portfolio Summary that contain related model names (goal_note, historical_note) require custom handling - if related_model_sort: - filters, annotations, order_by = build_related_model_filters_and_sorts(request.query_params, columns_from_database) - else: - filters, annotations, order_by = build_view_filters_and_sorts( - request.query_params, columns_from_database, inventory_type, org.access_level_names - ) + filters, annotations, order_by = build_view_filters_and_sorts( + request.query_params, columns_from_database, inventory_type, org.access_level_names + ) except FilterError as e: return JsonResponse({"status": "error", "message": f"Error filtering: {e!s}"}, status=status.HTTP_400_BAD_REQUEST) @@ -238,7 +231,7 @@ def get_filtered_results(request: Request, inventory_type: Literal["property", " show_columns = None try: - related_results = TaxLotProperty.serialize(views, show_columns, columns_from_database, include_related, goal_id) + related_results = TaxLotProperty.serialize(views, show_columns, columns_from_database, include_related) except DataError as e: return JsonResponse( { diff --git a/seed/utils/search.py b/seed/utils/search.py index 93248bf586..7735c92038 100644 --- a/seed/utils/search.py +++ b/seed/utils/search.py @@ -509,39 +509,44 @@ def build_view_filters_and_sorts( return new_filters, annotations, order_by -def build_related_model_filters_and_sorts(filters: QueryDict, columns: list[dict]) -> tuple[Q, AnnotationDict, list[str]]: - """Primarily used for sorting the Portfolio Summary on related columns like goal_notes and historical_notes""" - order_by = [] - annotations = {} - columns_by_name = {} - for column in columns: - if column["related"]: - continue - columns_by_name[column["name"]] = column - - column_name = filters.get("order_by") - if not column_name: - return Q(), {}, ["id"] - - direction = "-" if column_name.startswith("-") else "" - column_name = column_name.lstrip("-") - - if "goal_note" in column_name: - column_name = column_name.replace("goal_note", "goalnote") +def filter_views_on_related(views1, goal, filters, cycle1): + p_ids = views1.values_list("property_id", flat=True) + order_by = filters.get("order_by").replace("property__", "") + direction = "-" if order_by.startswith("-") else "" + order_by = order_by.lstrip("-") + goal_note = "goal_note" in order_by + historical_note = "historical_note" in order_by + order_by = order_by.replace("goal_note__", "").replace("historical_note__", "") + boolean_column = order_by in ["passed_checks", "new_or_acquired"] + target = False if boolean_column else "" + blanks_last = Case(When(**{order_by: target}, then=Value(1)), default=Value(0), output_field=IntegerField()) + + views = [] + if goal_note: + goal_notes = ( + goal.goalnote_set.filter(property__in=p_ids) + .annotate(custom_order=blanks_last) + .order_by(direction + "custom_order", direction + order_by) + ) + for goal_note in goal_notes: + view = goal_note.property.views.filter(cycle=cycle1).first() + if view: + views.append(view) + + elif historical_note: + from seed.models.notes import HistoricalNote + + historical_notes = ( + HistoricalNote.objects.filter(property__in=p_ids) + .annotate(custom_order=blanks_last) + .order_by(direction + "custom_order", direction + order_by) + ) + for historical_note in historical_notes: + view = historical_note.property.views.filter(cycle=cycle1).first() + if view: + views.append(view) - boolean_column = column_name in ["property__goalnote__passed_checks", "property__goalnote__new_or_acquired"] - target: Union[bool, str] - if boolean_column: - target = False else: - target = "" - - related_model = Case(When(**{column_name: target}, then=Value(1)), default=Value(0), output_field=IntegerField()) - parsed_annotations = {"related_model": related_model} - parsed_sort = [direction + "related_model", direction + column_name] - - if parsed_sort is not None: - order_by.extend(parsed_sort) - annotations.update(parsed_annotations) + views = views1 - return Q(), annotations, order_by + return views diff --git a/seed/views/v3/analyses.py b/seed/views/v3/analyses.py index 195640623b..517cbfa209 100644 --- a/seed/views/v3/analyses.py +++ b/seed/views/v3/analyses.py @@ -6,8 +6,7 @@ import json import logging -from django.db import connection -from django.db.models import Count, F +from django.db.models import F from django.http import JsonResponse from drf_yasg.utils import swagger_auto_schema from rest_framework import serializers, status, viewsets @@ -315,33 +314,14 @@ def stats(self, request): property__access_level_instance__lft__gte=access_level_instance.lft, property__access_level_instance__rgt__lte=access_level_instance.rgt, ).values_list("state_id", flat=True) - states = PropertyState.objects.filter(id__in=state_ids) columns = ( Column.objects.filter(organization_id=org_id, derived_column=None, table_name="PropertyState") .exclude(column_name__in=EXCLUDED_API_FIELDS) .only("is_extra_data", "column_name") ) - num_of_nonnulls_by_column_name = {c.column_name: 0 for c in columns} - canonical_columns = [c.column_name for c in columns if not c.is_extra_data] extra_data_columns = [c.column_name for c in columns if c.is_extra_data] - - # add non-null counts for extra_data columns - with connection.cursor() as cursor: - non_null_extra_data_counts_query = ( - f'SELECT key, COUNT(*)\n' - f'FROM seed_propertystate, LATERAL JSONB_EACH_TEXT(extra_data) AS each_entry(key, value)\n' - f'WHERE id IN ({", ".join(map(str, state_ids))})\n' - f' AND value IS NOT NULL\n' - f'GROUP BY key;' - ) - cursor.execute(non_null_extra_data_counts_query) - extra_data_counts = dict(cursor.fetchall()) - num_of_nonnulls_by_column_name.update(extra_data_counts) - - # add non-null counts for canonical columns - canonical_counts = states.aggregate(**{col: Count(col) for col in canonical_columns}) - num_of_nonnulls_by_column_name.update(canonical_counts) + num_of_nonnulls_by_column_name = Column.get_num_of_nonnulls_by_column_name(state_ids, PropertyState, columns) return JsonResponse( { @@ -376,30 +356,11 @@ def used_columns(self, request): ).values_list("state_id", flat=True) if state_ids: - states = PropertyState.objects.filter(id__in=state_ids) - columns = Column.objects.filter(organization_id=org_id, derived_column=None, table_name="PropertyState").exclude( column_name__in=EXCLUDED_API_FIELDS ) - num_of_nonnulls_by_column_name = {c.column_name: 0 for c in columns} - canonical_columns = [c.column_name for c in columns if not c.is_extra_data] - - # add non-null counts for extra_data columns - with connection.cursor() as cursor: - non_null_extra_data_counts_query = ( - f'SELECT key, COUNT(*)\n' - f'FROM seed_propertystate, LATERAL JSONB_EACH_TEXT(extra_data) AS each_entry(key, value)\n' - f'WHERE id IN ({", ".join(map(str, state_ids))})\n' - f' AND value IS NOT NULL\n' - f'GROUP BY key;' - ) - cursor.execute(non_null_extra_data_counts_query) - extra_data_counts = dict(cursor.fetchall()) - num_of_nonnulls_by_column_name.update(extra_data_counts) - - # add non-null counts for canonical columns - canonical_counts = states.aggregate(**{col: Count(col) for col in canonical_columns}) - num_of_nonnulls_by_column_name.update(canonical_counts) + + num_of_nonnulls_by_column_name = Column.get_num_of_nonnulls_by_column_name(state_ids, PropertyState, columns) # Taxlots tstate_ids = TaxLotView.objects.filter( @@ -410,29 +371,10 @@ def used_columns(self, request): # add non-null counts for extra_data columns if tstate_ids: - tstates = TaxLotState.objects.filter(id__in=tstate_ids) - tcolumns = Column.objects.filter(organization_id=org_id, derived_column=None, table_name="TaxLotState").exclude( column_name__in=EXCLUDED_API_FIELDS ) - tnum_of_nonnulls_by_column_name = {c.column_name: 0 for c in tcolumns} - tcanonical_columns = [c.column_name for c in tcolumns if not c.is_extra_data] - - with connection.cursor() as cursor: - tnon_null_extra_data_counts_query = ( - f'SELECT key, COUNT(*)\n' - f'FROM seed_taxlotstate, LATERAL JSONB_EACH_TEXT(extra_data) AS each_entry(key, value)\n' - f'WHERE id IN ({", ".join(map(str, tstate_ids))})\n' - f' AND value IS NOT NULL\n' - f'GROUP BY key;' - ) - cursor.execute(tnon_null_extra_data_counts_query) - extra_data_counts = dict(cursor.fetchall()) - tnum_of_nonnulls_by_column_name.update(extra_data_counts) - - # add non-null counts for canonical columns - tcanonical_counts = tstates.aggregate(**{col: Count(col) for col in tcanonical_columns}) - tnum_of_nonnulls_by_column_name.update(tcanonical_counts) + num_of_nonnulls_by_column_name = Column.get_num_of_nonnulls_by_column_name(state_ids, TaxLotState, columns) # properties and taxlots together num_of_nonnulls_by_column_name.update(tnum_of_nonnulls_by_column_name) diff --git a/seed/views/v3/column_list_profiles.py b/seed/views/v3/column_list_profiles.py index 0d40c50426..5b3065c5b6 100644 --- a/seed/views/v3/column_list_profiles.py +++ b/seed/views/v3/column_list_profiles.py @@ -9,6 +9,7 @@ from django.utils.decorators import method_decorator from drf_yasg.utils import swagger_auto_schema from rest_framework import status +from rest_framework.decorators import action from rest_framework.response import Response from seed.filters import ColumnListProfileFilterBackend @@ -21,7 +22,12 @@ Column, ColumnListProfile, Organization, + PropertyState, + PropertyView, + TaxLotState, + TaxLotView, ) +from seed.models.columns import EXCLUDED_API_FIELDS from seed.serializers.column_list_profiles import ColumnListProfileSerializer from seed.utils.api import OrgValidateMixin from seed.utils.api_schema import AutoSchemaHelper, swagger_auto_schema_org_query_param @@ -183,3 +189,34 @@ def list_comstock_columns(org_id): ) return results + + @has_perm_class("requires_root_member_access") + @action(detail=True, methods=["PUT"]) + def show_populated(self, request, pk): + column_list_profile = ColumnListProfile.objects.get(pk=pk) + org_id = self.get_organization(self.request) + cycle_id = request.data.get("cycle_id") + inventory_type = request.data.get("inventory_type") + StateTable = PropertyState if inventory_type == "Property" else TaxLotState + ViewTable = PropertyView if inventory_type == "Property" else TaxLotView + + # get all the columns and states we need to query + all_columns = ( + Column.objects.filter(organization_id=org_id, derived_column=None, table_name=StateTable.__name__) + .exclude(column_name__in=EXCLUDED_API_FIELDS) + .only("is_extra_data", "column_name") + ) + state_ids = ViewTable.objects.filter( + cycle_id=cycle_id, + ).values_list("state_id", flat=True) + + # filter for only the populated columns + num_of_nonnulls_by_column_name = Column.get_num_of_nonnulls_by_column_name(state_ids, StateTable, all_columns) + needed_column_names = [col for col, count in num_of_nonnulls_by_column_name.items() if count > 0] + needed_columns = Column.objects.filter(column_name__in=needed_column_names, table_name=StateTable.__name__, organization=org_id) + + # set needed columns in there + column_list_profile.columns.set(needed_columns) + column_list_profile.save() + + return JsonResponse({"status": "success", "data": self.get_serializer(column_list_profile).data}, status=status.HTTP_200_OK) diff --git a/seed/views/v3/goals.py b/seed/views/v3/goals.py index 5ae4595f4d..a13a7fb7e6 100644 --- a/seed/views/v3/goals.py +++ b/seed/views/v3/goals.py @@ -3,19 +3,26 @@ See also https://github.com/SEED-platform/seed/blob/main/LICENSE.md """ +import math + +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.db.utils import DataError from django.http import JsonResponse from django.utils.decorators import method_decorator +from pint import Quantity from rest_framework import status from rest_framework.decorators import action from seed.decorators import ajax_request_class from seed.lib.superperms.orgs.decorators import has_hierarchy_access, has_perm_class -from seed.models import AccessLevelInstance, Goal, GoalNote, HistoricalNote, Organization, Property +from seed.models import AccessLevelInstance, Column, Goal, GoalNote, HistoricalNote, Organization, Property, TaxLotProperty from seed.serializers.goals import GoalSerializer +from seed.serializers.pint import apply_display_unit_preferences from seed.utils.api import OrgMixin from seed.utils.api_schema import swagger_auto_schema_org_query_param from seed.utils.goal_notes import get_permission_data from seed.utils.goals import get_or_create_goal_notes, get_portfolio_summary +from seed.utils.search import FilterError, build_view_filters_and_sorts, filter_views_on_related from seed.utils.viewsets import ModelViewSetWithoutPatch @@ -149,3 +156,177 @@ def bulk_update_goal_notes(self, request, pk): result = goal_notes.update(**data) return JsonResponse({"status": "success", "message": f"Updated {result} properties"}) + + @ajax_request_class + @swagger_auto_schema_org_query_param + @has_perm_class("requires_viewer") + @has_hierarchy_access(goal_id_kwarg="pk") + @action(detail=True, methods=["PUT"]) + def data(self, request, pk): + """ + Gets goal data for the main grid + """ + # Init a bunch of values + org_id = int(self.get_organization(request)) + try: + org = Organization.objects.get(pk=org_id) + goal = Goal.objects.get(pk=pk) + except (Organization.DoesNotExist, Goal.DoesNotExist): + return JsonResponse({"status": "error", "message": "No such resource."}) + page = request.data.get("page") + per_page = request.data.get("per_page") + baseline_first = request.data.get("baseline_first") + access_level_instance_id = request.data.get("access_level_instance_id") + related_model_sort = request.data.get("related_model_sort") + inventory_type = "property" + access_level_instance = AccessLevelInstance.objects.get(pk=access_level_instance_id) + columns_from_database = Column.retrieve_all( + org_id=org_id, + inventory_type=inventory_type, + only_used=False, + include_related=False, + ) + show_columns = list(Column.objects.filter(organization_id=org_id).values_list("id", flat=True)) + key1, key2 = ("baseline", "current") if baseline_first else ("current", "baseline") + + cycle1 = getattr(goal, f"{key1}_cycle") + cycle2 = getattr(goal, f"{key2}_cycle") + views1 = cycle1.propertyview_set.filter( + property__access_level_instance__lft__gte=access_level_instance.lft, + property__access_level_instance__rgt__lte=access_level_instance.rgt, + ).select_related("property") + + try: + # Sorts initiated from Portfolio Summary that contain related model names (goal_note, historical_note) require custom handling + if related_model_sort: + views1 = filter_views_on_related(views1, goal, request.query_params, cycle1) + else: + filters, annotations, order_by = build_view_filters_and_sorts( + request.query_params, columns_from_database, inventory_type, org.access_level_names + ) + views1 = views1.annotate(**annotations).filter(filters).order_by(*order_by) + except FilterError as e: + return JsonResponse({"status": "error", "message": f"Error filtering: {e!s}"}, status=status.HTTP_400_BAD_REQUEST) + except ValueError as e: + return JsonResponse({"status": "error", "message": f"Error filtering: {e!s}"}, status=status.HTTP_400_BAD_REQUEST) + + # Paginate results + paginator = Paginator(views1, per_page) + try: + views1 = paginator.page(page) + page = int(page) + except PageNotAnInteger: + views1 = paginator.page(1) + page = 1 + except EmptyPage: + views1 = paginator.page(paginator.num_pages) + page = paginator.num_pages + except DataError as e: + return JsonResponse( + { + "status": "error", + "message": f"Error filtering - your data might not match the column settings data type: {e!s}", + }, + status=status.HTTP_400_BAD_REQUEST, + ) + except IndexError as e: + return JsonResponse( + {"status": "error", "message": f"Error filtering - Clear filters and try again: {e!s}"}, status=status.HTTP_400_BAD_REQUEST + ) + + property_ids = [v.property_id for v in views1] + # fetch cycle 2 properties + views2 = cycle2.propertyview_set.filter( + property__id__in=property_ids, + property__access_level_instance__lft__gte=access_level_instance.lft, + property__access_level_instance__rgt__lte=access_level_instance.rgt, + ) + + properties1 = TaxLotProperty.serialize(views1, show_columns, columns_from_database, False, pk) + properties2 = TaxLotProperty.serialize(views2, show_columns, columns_from_database, False, pk) + # collapse pint Qunatity units to their magnitudes + properties1 = [apply_display_unit_preferences(org, x) for x in properties1] + properties2 = [apply_display_unit_preferences(org, x) for x in properties2] + + area_name = f"{goal.area_column.column_name}_{goal.area_column.id}" + eui_columns = [f"{col.column_name}_{col.id}" for col in goal.eui_columns()] + + # lookup for pv.id to p.id + property_lookup = {} + for p in properties1 + properties2: + property_lookup[p["property_view_id"]] = p["id"] + + properties = [] + for p1 in properties1: + p2 = next((p for p in properties2 if p["id"] == p1["id"]), None) + property = combine_properties(p1, p2) + + sqft1 = p1.get(area_name) + sqft2 = p2.get(area_name) if p2 else None + + # add cycle specific and aggregated goal stats + property[f"{key1}_cycle"] = cycle1.name + property[f"{key2}_cycle"] = cycle2.name + property[f"{key1}_sqft"] = convert_quantity(sqft1) + property[f"{key2}_sqft"] = convert_quantity(sqft2) + property[f"{key1}_eui"] = get_preferred(p1, eui_columns) + property[f"{key2}_eui"] = get_preferred(p2, eui_columns) + property["baseline_kbtu"] = get_kbtu(property, "baseline") + property["current_kbtu"] = get_kbtu(property, "current") + property["sqft_change"] = percentage(property["current_sqft"], property["baseline_sqft"]) + property["eui_change"] = percentage(property["baseline_eui"], property["current_eui"]) + + properties.append(property) + + return JsonResponse( + { + "pagination": { + "page": page, + "start": paginator.page(page).start_index(), + "end": paginator.page(page).end_index(), + "num_pages": paginator.num_pages, + "has_next": paginator.page(page).has_next(), + "has_previous": paginator.page(page).has_previous(), + "total": paginator.count, + }, + "properties": properties, + "property_lookup": property_lookup, + } + ) + + +def combine_properties(p1, p2): + if not p2: + return p1 + combined = p1.copy() + for key, value in p2.items(): + if value is not None: + combined[key] = value + return combined + + +def percentage(a, b): + if not a or b is None: + return None + value = round(((a - b) / a) * 100) + return None if math.isnan(value) else value + + +def get_preferred(prop, columns): + if not prop: + return + for col in columns: + quantity = convert_quantity(prop[col]) + if quantity is not None: + return quantity + + +def convert_quantity(value): + if isinstance(value, Quantity): + value = value.m + return value + + +def get_kbtu(prop, key): + if prop[f"{key}_sqft"] is not None and prop[f"{key}_eui"] is not None: + return round(prop[f"{key}_sqft"] * prop[f"{key}_eui"]) diff --git a/seed/views/v3/organizations.py b/seed/views/v3/organizations.py index 0367424072..2a7830edeb 100644 --- a/seed/views/v3/organizations.py +++ b/seed/views/v3/organizations.py @@ -886,10 +886,10 @@ def get_raw_report_data(self, organization_id, cycles, all_property_views, field data = [apply_display_unit_preferences(organization, d) for d in property_views.values("id", *fields.keys(), "yr_e")] # count before and after we prune the empty ones + # watch out not to prune boolean fields count_total = len(data) - data = [d for d in data if d["x"] and d["y"]] + data = [d for d in data if (d["x"] or d["x"] is False or d["x"] is True) and (d["y"] or d["y"] is False or d["y"] is True)] count_with_data = len(data) - result = { "cycle_id": cycle.pk, "chart_data": data, @@ -1020,10 +1020,16 @@ def report_aggregated(self, request, pk=None): chart_data = [] property_counts = [] - # set bins and choose agg type + # set bins and choose agg type. treat booleans as discrete ys = [building["y"] for datum in data for building in datum["chart_data"] if building["y"] is not None] - if ys and isinstance(ys[0], Number): + if ys and isinstance(ys[0], Number) and ys[0] is not True and ys[0] is not False: bins = np.histogram_bin_edges(ys, bins=5) + + # special case for year built: make bins integers + # year built is in x axis, but it shows up in y_var variable + if params["y_var"] == "year_built": + bins = bins.astype(int) + aggregate_data = self.continuous_aggregate_data else: bins = list(set(ys)) @@ -1211,7 +1217,9 @@ def report_export(self, request, pk=None): return response def get_axis_stats(self, organization, cycle, axis, axis_var, views, ali): - """returns axis_name, access_level_instance name, sum, mean, min, max, 5%, 25%, 50%, 75%, 99%""" + """returns axis_name, access_level_instance name, sum, mean, min, max, 5%, 25%, 50%, 75%, 99% + exclude categorical and boolean from stats + """ filtered_properties = views.filter( property__access_level_instance__lft__gte=ali.lft, property__access_level_instance__rgt__lte=ali.rgt, cycle_id=cycle.id @@ -1220,7 +1228,7 @@ def get_axis_stats(self, organization, cycle, axis, axis_var, views, ali): data = [ d[axis] for d in [apply_display_unit_preferences(organization, d) for d in filtered_properties.values(axis)] - if axis in d and d[axis] is not None and isinstance(d[axis], (int, float)) + if axis in d and d[axis] is not None and d[axis] is not True and d[axis] is not False and isinstance(d[axis], (int, float)) ] if len(data) > 0: @@ -1243,15 +1251,19 @@ def get_axis_stats(self, organization, cycle, axis, axis_var, views, ali): return [axis_var, ali.name, 0, 0, 0, 0, 0, 0, 0, 0, 0] def get_axis_data(self, organization_id, access_level_instance, cycles, x_var, y_var, all_property_views, fields): - axis_data = [] + axis_data = {} axes = {"x": x_var, "y": y_var} organization = Organization.objects.get(pk=organization_id) + # initialize + for cycle in cycles: + axis_data[cycle.name] = {} + for axis in axes: if axes[axis] != "Count": - columns = Column.objects.filter(organization_id=organization_id, column_name=axes[axis]) + columns = Column.objects.filter(organization_id=organization_id, column_name=axes[axis], table_name="PropertyState") if not columns: - return [] + return {} column = columns[0] if not column.data_type or column.data_type == "None": @@ -1266,19 +1278,24 @@ def get_axis_data(self, organization_id, access_level_instance, cycles, x_var, y name_to_display = ( serialized_column["display_name"] if serialized_column["display_name"] != "" else serialized_column["column_name"] ) - axis_name = name_to_display + f" ({cycle.name})" + axis_data[cycle.name][name_to_display] = {} stats = self.get_axis_stats(organization, cycle, axis, axes[axis], all_property_views, access_level_instance) - axis_data.append(self.clean_axis_data(axis_name, data_type, stats)) - for child_ali in access_level_instance.get_children(): - stats = self.get_axis_stats(organization, cycle, axis, axes[axis], all_property_views, child_ali) - axis_data.append(self.clean_axis_data(axis_name, data_type, stats)) + axis_data[cycle.name][name_to_display]["values"] = self.clean_axis_data(data_type, stats) + + children = access_level_instance.get_children() + if len(children): + axis_data[cycle.name][name_to_display]["children"] = {} + for child_ali in children: + stats = self.get_axis_stats(organization, cycle, axis, axes[axis], all_property_views, child_ali) + axis_data[cycle.name][name_to_display]["children"][child_ali.name] = self.clean_axis_data(data_type, stats) + return axis_data - def clean_axis_data(self, column_name, data_type, data): + def clean_axis_data(self, data_type, data): if data_type == "float": - return [column_name] + data[1:3] + np.round(data[3:], decimals=2).tolist() + return data[1:3] + np.round(data[3:], decimals=2).tolist() elif data_type == "integer": - return [column_name] + data[1:3] + np.round(data[3:]).tolist() + return data[1:3] + np.round(data[3:]).tolist() @has_perm_class("requires_member") @ajax_request_class diff --git a/seed/views/v3/properties.py b/seed/views/v3/properties.py index 2a4e165e44..e7504a4f2e 100644 --- a/seed/views/v3/properties.py +++ b/seed/views/v3/properties.py @@ -587,23 +587,15 @@ def unmerge(self, request, pk=None): # Clone the property record twice, then copy over meters old_property = old_view.property - new_property = old_property - new_property.id = None - new_property.save() - new_property_2 = Property.objects.get(pk=new_property.id) + new_property_2 = Property.objects.get(pk=old_property.id) new_property_2.id = None new_property_2.save() - Property.objects.get(pk=new_property.id).copy_meters(old_view.property_id) Property.objects.get(pk=new_property_2.id).copy_meters(old_view.property_id) - # If canonical Property is NOT associated to a different -View, delete it - if not PropertyView.objects.filter(property_id=old_view.property_id).exclude(id=old_view.id).exists(): - Property.objects.get(pk=old_view.property_id).delete() - # Create the views - new_view1 = PropertyView(cycle_id=cycle_id, property_id=new_property.id, state=state1) + new_view1 = PropertyView(cycle_id=cycle_id, property_id=old_property.id, state=state1) new_view2 = PropertyView(cycle_id=cycle_id, property_id=new_property_2.id, state=state2) # Mark the merged state as deleted @@ -655,6 +647,15 @@ def unmerge(self, request, pk=None): # Correct the created and updated times to match the original note Note.objects.filter(id__in=ids).update(created=created, updated=updated) + Note.objects.create( + organization=old_property.organization, + note_type=Note.LOG, + name="Unmerged Property", + text=f"This PropertyView was unmerged from PropertyView id {new_view1.id}", + user=request.user, + property_view=new_view2, + ) + for paired_view_id in paired_view_ids: TaxLotProperty(primary=True, cycle_id=cycle_id, property_view_id=new_view1.id, taxlot_view_id=paired_view_id).save() TaxLotProperty(primary=True, cycle_id=cycle_id, property_view_id=new_view2.id, taxlot_view_id=paired_view_id).save() @@ -1986,7 +1987,7 @@ def mode(field, extra_data=False): # Number of Buildings Metered for Water data["Number of Buildings Metered for Water"] = sum([v.has_water_meters for v in views]) # Annual Facility Energy Use - data["Annual Facility Energy Use"] = sum([v.state.extra_data.get("Total of Modeled/MDMS Total Energy Usage", 0) for v in views]) + data["Annual Facility Energy Use"] = sum([v.state.extra_data.get("Sum of Modeled/MDMS Total Energy Usage", 0) for v in views]) # Annual Facility Water Use data["Annual Facility Energy Use"] = sum([v.state.extra_data.get("Sum of Modeled/MDMS Total Water Usage", 0) for v in views]) diff --git a/seed/views/v3/tax_lot_properties.py b/seed/views/v3/tax_lot_properties.py index 3ad873ab11..8b4d15af37 100644 --- a/seed/views/v3/tax_lot_properties.py +++ b/seed/views/v3/tax_lot_properties.py @@ -674,8 +674,8 @@ def update_derived_data(self, request): derived_column_ids = list(derived_columns.values_list("id", flat=True)) # update - update_state_derived_data( + result = update_state_derived_data( property_state_ids=property_state_ids, taxlot_state_ids=taxlot_state_ids, derived_column_ids=derived_column_ids ) - return JsonResponse({"result": "success"}) + return JsonResponse(result) diff --git a/seed/views/v3/taxlots.py b/seed/views/v3/taxlots.py index 0dcb8d3a98..c7eb638354 100644 --- a/seed/views/v3/taxlots.py +++ b/seed/views/v3/taxlots.py @@ -297,20 +297,13 @@ def unmerge(self, request, pk=None): # Clone the taxlot record twice old_taxlot = old_view.taxlot - new_taxlot = old_taxlot - new_taxlot.id = None - new_taxlot.save() - new_taxlot_2 = TaxLot.objects.get(pk=new_taxlot.pk) + new_taxlot_2 = TaxLot.objects.get(pk=old_taxlot.pk) new_taxlot_2.id = None new_taxlot_2.save() - # If the canonical TaxLot is NOT associated to another -View - if not TaxLotView.objects.filter(taxlot_id=old_view.taxlot_id).exclude(pk=old_view.id).exists(): - TaxLot.objects.get(pk=old_view.taxlot_id).delete() - # Create the views - new_view1 = TaxLotView(cycle_id=cycle_id, taxlot_id=new_taxlot.id, state=state1) + new_view1 = TaxLotView(cycle_id=cycle_id, taxlot_id=old_taxlot.id, state=state1) new_view2 = TaxLotView(cycle_id=cycle_id, taxlot_id=new_taxlot_2.id, state=state2) # Mark the merged state as deleted @@ -362,6 +355,15 @@ def unmerge(self, request, pk=None): # Correct the created and updated times to match the original note Note.objects.filter(id__in=ids).update(created=created, updated=updated) + Note.objects.create( + organization=old_taxlot.organization, + note_type=Note.LOG, + name="Unmerged Taxlot", + text=f"This TaxLotView was unmerged from TaxLotView id {new_view1.id}", + user=request.user, + taxlot_view=new_view2, + ) + for paired_view_id in paired_view_ids: TaxLotProperty(primary=True, cycle_id=cycle_id, taxlot_view_id=new_view1.id, property_view_id=paired_view_id).save() TaxLotProperty(primary=True, cycle_id=cycle_id, taxlot_view_id=new_view2.id, property_view_id=paired_view_id).save() diff --git a/vendors/package-lock.json b/vendors/package-lock.json index 7608df5d65..6c3568f381 100644 --- a/vendors/package-lock.json +++ b/vendors/package-lock.json @@ -479,12 +479,14 @@ "node_modules/@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -496,11 +498,12 @@ "optional": true }, "node_modules/@types/node": { - "version": "20.14.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.5.tgz", - "integrity": "sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.8" } }, "node_modules/@uirouter/core": { @@ -520,6 +523,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -754,6 +758,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" } @@ -1231,9 +1236,10 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1248,6 +1254,7 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -1844,16 +1851,17 @@ } }, "node_modules/engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -1864,19 +1872,21 @@ } }, "node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/engine.io/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1888,9 +1898,10 @@ } }, "node_modules/engine.io/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/ent": { "version": "2.2.0", @@ -2846,9 +2857,10 @@ } }, "node_modules/karma": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", - "integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "license": "MIT", "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -3204,6 +3216,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -3834,15 +3847,16 @@ "dev": true }, "node_modules/socket.io": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", - "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.2", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, @@ -4316,9 +4330,10 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" }, "node_modules/universalify": { "version": "0.1.2", @@ -4404,6 +4419,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" }