From ae7df770ed5ffdfb2ed499946473f4b0cabe19dd Mon Sep 17 00:00:00 2001 From: jgu2 Date: Wed, 17 Jul 2024 21:06:56 -0600 Subject: [PATCH 01/30] Add notification for mapbox weekly report --- calliope_app/calliope_app/settings/base.py | 3 +- calliope_app/notification/__init__.py | 0 calliope_app/notification/apps.py | 11 ++++ .../notification/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/report_mapbox_usage.py | 10 ++++ calliope_app/notification/tasks.py | 54 +++++++++++++++++++ 7 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 calliope_app/notification/__init__.py create mode 100644 calliope_app/notification/apps.py create mode 100644 calliope_app/notification/management/__init__.py create mode 100644 calliope_app/notification/management/commands/__init__.py create mode 100644 calliope_app/notification/management/commands/report_mapbox_usage.py create mode 100644 calliope_app/notification/tasks.py diff --git a/calliope_app/calliope_app/settings/base.py b/calliope_app/calliope_app/settings/base.py index 3c5e1e13..b607ec46 100644 --- a/calliope_app/calliope_app/settings/base.py +++ b/calliope_app/calliope_app/settings/base.py @@ -47,7 +47,8 @@ 'client.apps.ClientConfig', 'geophires.apps.GeophiresConfig', 'taskmeta.apps.TaskmetaConfig', - 'template.apps.TemplateConfig' + 'template.apps.TemplateConfig', + 'notification.apps.NotificationConfig' ] INSTALLED_APPS = DJANGO_APPS + LOCAL_APPS diff --git a/calliope_app/notification/__init__.py b/calliope_app/notification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/calliope_app/notification/apps.py b/calliope_app/notification/apps.py new file mode 100644 index 00000000..73602afd --- /dev/null +++ b/calliope_app/notification/apps.py @@ -0,0 +1,11 @@ +import os +from django.apps import AppConfig +from django.conf import settings + + +class NotificationConfig(AppConfig): + name = "notification" + + def ready(self): + if not os.path.exists(settings.DATA_STORAGE): + os.makedirs(settings.DATA_STORAGE, exist_ok=True) diff --git a/calliope_app/notification/management/__init__.py b/calliope_app/notification/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/calliope_app/notification/management/commands/__init__.py b/calliope_app/notification/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/calliope_app/notification/management/commands/report_mapbox_usage.py b/calliope_app/notification/management/commands/report_mapbox_usage.py new file mode 100644 index 00000000..063523d3 --- /dev/null +++ b/calliope_app/notification/management/commands/report_mapbox_usage.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand +from notification.tasks import send_mapbox_usage_email + + +class Command(BaseCommand): + help = 'Read NREL AD and sync people to Lex' + + def handle(self, *args, **kwargs): + """Report Mapbox usage information""" + send_mapbox_usage_email() diff --git a/calliope_app/notification/tasks.py b/calliope_app/notification/tasks.py new file mode 100644 index 00000000..70b745df --- /dev/null +++ b/calliope_app/notification/tasks.py @@ -0,0 +1,54 @@ +import logging +from datetime import datetime + +from django.core.mail import EmailMultiAlternatives +from django.conf import settings +from django.contrib.auth.models import User + +from api.models.engage import RequestRateLimit + +logger = logging.getLogger(__name__) + + +def send_mapbox_usage_email(): + + now = datetime.now() + month = now.strftime('%b') + + try: + limit = RequestRateLimit.objects.get( + year=now.year, + month=now.month, + ) + except RequestRateLimit.DoesNotExist: + logger.error('Failed to query mapbox usage - %s, %s !', month, now.year) + return + + subject=f'Engage Mapbox Usage [{month}, {now.year}]' + from_email = settings.AWS_SES_FROM_EMAIL + recipient_list = [from_email] + recipient_list.extend([admin.email for admin in User.objects.filter(is_superuser=True)]) + + text_content = f""" + Hello Engage Admin, + + This is a weekly update email about Mapbox usage of current month! + Mapbox loads used till today in {month} {now.year} is "{limit.total}/50,000" + + Map loads exceed 50,000 will be charged, please check Mapbox pricing for more information. + + --Engage System + """ + + msg = EmailMultiAlternatives( + subject=subject, + body=text_content, + from_email=from_email, + to=recipient_list + ) + msg.send() + + logger.info( + 'Mapbox usage email sent! Used loads %s / 50,0000 in %s, %s !', + limit.total, month, now.year + ) From fec72825afc8ce9a596d818bfef325ce91e73cfb Mon Sep 17 00:00:00 2001 From: jgu2 Date: Wed, 17 Jul 2024 21:09:24 -0600 Subject: [PATCH 02/30] Update email command help text --- .../notification/management/commands/report_mapbox_usage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calliope_app/notification/management/commands/report_mapbox_usage.py b/calliope_app/notification/management/commands/report_mapbox_usage.py index 063523d3..58790642 100644 --- a/calliope_app/notification/management/commands/report_mapbox_usage.py +++ b/calliope_app/notification/management/commands/report_mapbox_usage.py @@ -3,7 +3,7 @@ class Command(BaseCommand): - help = 'Read NREL AD and sync people to Lex' + help = 'Query Mapbox usage and send email' def handle(self, *args, **kwargs): """Report Mapbox usage information""" From 83dd9d570a91f868554cde3889ebfa05f2daf8d2 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Wed, 17 Jul 2024 21:39:24 -0600 Subject: [PATCH 03/30] Bump version to v1.1.1 --- calliope_app/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calliope_app/version.py b/calliope_app/version.py index 6849410a..a82b376d 100644 --- a/calliope_app/version.py +++ b/calliope_app/version.py @@ -1 +1 @@ -__version__ = "1.1.0" +__version__ = "1.1.1" From d013c4367a6f1be6e570f41c0d253e943a09a587 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Thu, 18 Jul 2024 13:54:06 -0600 Subject: [PATCH 04/30] Update recipient list of mapbox ussage --- calliope_app/notification/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calliope_app/notification/tasks.py b/calliope_app/notification/tasks.py index 70b745df..bbd2f632 100644 --- a/calliope_app/notification/tasks.py +++ b/calliope_app/notification/tasks.py @@ -26,7 +26,7 @@ def send_mapbox_usage_email(): subject=f'Engage Mapbox Usage [{month}, {now.year}]' from_email = settings.AWS_SES_FROM_EMAIL - recipient_list = [from_email] + recipient_list = ["engage@nrel.gov"] recipient_list.extend([admin.email for admin in User.objects.filter(is_superuser=True)]) text_content = f""" From 24bd12afe7a2e2356eeeaf1e2a4d523d06780cf1 Mon Sep 17 00:00:00 2001 From: hfields Date: Mon, 22 Jul 2024 10:34:18 -0600 Subject: [PATCH 05/30] fix copying of tech lhs and rhs issue --- calliope_app/client/static/js/scenarios.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/calliope_app/client/static/js/scenarios.js b/calliope_app/client/static/js/scenarios.js index baa1e03b..1558fc1b 100644 --- a/calliope_app/client/static/js/scenarios.js +++ b/calliope_app/client/static/js/scenarios.js @@ -254,16 +254,16 @@ function updateDialogObject() { delete dialogObj[constraint].techs; } - techsInput = $("#" + constraintId + "_techs_lhs").val(); + var techsInputLhs = $("#" + constraintId + "_techs_lhs").val(); if (techsInput && techsInput.length > 0) { - dialogObj[constraint].techs_lhs = techsInput; + dialogObj[constraint].techs_lhs = techsInputLhs; } else { delete dialogObj[constraint].techs_lhs; } - techsInput = $("#" + constraintId + "_techs_rhs").val(); + var techsInputRhs = $("#" + constraintId + "_techs_rhs").val(); if (techsInput && techsInput.length > 0) { - dialogObj[constraint].techs_rhs = techsInput; + dialogObj[constraint].techs_rhs = techsInputRhs; } else { delete dialogObj[constraint].techs_rhs; } @@ -276,16 +276,16 @@ function updateDialogObject() { delete dialogObj[constraint].locs; } - locsInput = $("#" + constraintId + "_locs_lhs").val(); + var locsInputLhs = $("#" + constraintId + "_locs_lhs").val(); if (locsInput && locsInput.length > 0) { - dialogObj[constraint].locs_lhs = locsInput; + dialogObj[constraint].locs_lhs = locsInputLhs; } else { delete dialogObj[constraint].locs_lhs; } - locsInput = $("#" + constraintId + "_locs_rhs").val(); + var locsInputRhs = $("#" + constraintId + "_locs_rhs").val(); if (locsInput && locsInput.length > 0) { - dialogObj[constraint].locs_rhs = locsInput; + dialogObj[constraint].locs_rhs = locsInputRhs; } else { delete dialogObj[constraint].locs_rhs; } From 81bd8e4cf7f7c464df63f5a6901d5dd7dc12c9bd Mon Sep 17 00:00:00 2001 From: hfields Date: Thu, 25 Jul 2024 13:41:56 -0600 Subject: [PATCH 06/30] make dialog wider --- calliope_app/client/static/css/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calliope_app/client/static/css/main.css b/calliope_app/client/static/css/main.css index c9d1418a..7b3a6c66 100644 --- a/calliope_app/client/static/css/main.css +++ b/calliope_app/client/static/css/main.css @@ -27,7 +27,7 @@ th.sticky { margin-top: 150px; padding: 20px; border: 1px solid #888; - width: 800px; + width: 1000px; } #splitter { height: 15px; From dc183b8283dc13b9b99db0290c255c4b55c989af Mon Sep 17 00:00:00 2001 From: hfields Date: Thu, 25 Jul 2024 13:42:42 -0600 Subject: [PATCH 07/30] switch out .change for .on('change' --- calliope_app/client/static/js/scenarios.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/calliope_app/client/static/js/scenarios.js b/calliope_app/client/static/js/scenarios.js index 1558fc1b..69bef9be 100644 --- a/calliope_app/client/static/js/scenarios.js +++ b/calliope_app/client/static/js/scenarios.js @@ -358,7 +358,7 @@ function updateDialogGroupConstraints(initialLoad) { $('#' + constraintId + '_techs').append(''); } } - $("#" + constraintId + "_techs").change(function () { + $("#" + constraintId + "_techs").on('change', function () { updateDialogObject(); }); @@ -372,7 +372,7 @@ function updateDialogGroupConstraints(initialLoad) { $('#' + constraintId + '_techs_lhs').append(''); } } - $("#" + constraintId + "_techs_lhs").change(function () { + $("#" + constraintId + "_techs_lhs").on('change', function () { updateDialogObject(); }); @@ -386,7 +386,7 @@ function updateDialogGroupConstraints(initialLoad) { $('#' + constraintId + '_techs_rhs').append(''); } } - $("#" + constraintId + "_techs_rhs").change(function () { + $("#" + constraintId + "_techs_rhs").on('change', function () { updateDialogObject(); }); @@ -400,7 +400,7 @@ function updateDialogGroupConstraints(initialLoad) { for (var l in locations) { $('#' + constraintId + '_locs').append(''); } - $("#" + constraintId + "_locs").change(function () { + $("#" + constraintId + "_locs").on('change', function () { updateDialogObject(); }); @@ -410,7 +410,7 @@ function updateDialogGroupConstraints(initialLoad) { for (var l in locations) { $('#' + constraintId + '_locs_lhs').append(''); } - $("#" + constraintId + "_locs_lhs").change(function () { + $("#" + constraintId + "_locs_lhs").on('change', function () { updateDialogObject(); }); @@ -420,7 +420,7 @@ function updateDialogGroupConstraints(initialLoad) { for (var l in locations) { $('#' + constraintId + '_locs_rhs').append(''); } - $("#" + constraintId + "_locs_rhs").change(function () { + $("#" + constraintId + "_locs_rhs").on('change', function () { updateDialogObject(); }); @@ -516,7 +516,7 @@ function updateConstraintTypes(constraint, constraintId, constraintContent) { } } - $("#" + constraintId + fieldKey + "-key").change(function () { + $("#" + constraintId + fieldKey + "-key").on('change', function () { updateDialogObject(); }); @@ -526,7 +526,7 @@ function updateConstraintTypes(constraint, constraintId, constraintContent) { $(constraintFields).append( ""); $(constraintFields).append( "

" ); } - $("#" + constraintId + fieldKey + "-val").change(function () { + $("#" + constraintId + fieldKey + "-val").on('change', function () { updateDialogObject(); }); } @@ -551,7 +551,7 @@ function updateConstraintTypes(constraint, constraintId, constraintContent) { updateDialogGroupConstraints(); }); - $("#new-constraint-dropdown-" + constraintId).change(function () { + $("#new-constraint-dropdown-" + constraintId).on('change', function(e) { let con = this.id.replace("new-constraint-dropdown-", ""); if (this.value && this.value.length > 0 ) { $("#new_constraint_btn_" + con).removeAttr("disabled"); @@ -559,6 +559,7 @@ function updateConstraintTypes(constraint, constraintId, constraintContent) { $('#new_constraint_btn_' + con).attr("disabled", true); } }); + } function safeHTMLId(id) { From 63aa53b466b01935371b5fbfff6f57583f55c531 Mon Sep 17 00:00:00 2001 From: hfields Date: Thu, 25 Jul 2024 17:36:57 -0600 Subject: [PATCH 08/30] fix expander logic --- calliope_app/client/static/js/scenarios.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/calliope_app/client/static/js/scenarios.js b/calliope_app/client/static/js/scenarios.js index 69bef9be..761ea4cd 100644 --- a/calliope_app/client/static/js/scenarios.js +++ b/calliope_app/client/static/js/scenarios.js @@ -573,13 +573,13 @@ function setGroupConstraintClassLogic() { if ($(this).hasClass('hiding_rows')) { rows.removeClass('hide'); $(this).removeClass('hiding_rows'); - $(this).find('.fa-caret-up').css('display', 'none'); - $(this).find('.fa-caret-down').css('display', 'inline'); + $(this).find('.fa-caret-up').css('display', 'inline'); + $(this).find('.fa-caret-down').css('display', 'none'); } else { rows.addClass('hide'); $(this).addClass('hiding_rows'); - $(this).find('.fa-caret-up').css('display', 'inline'); - $(this).find('.fa-caret-down').css('display', 'none'); + $(this).find('.fa-caret-up').css('display', 'none'); + $(this).find('.fa-caret-down').css('display', 'inline'); } }); } From 731fb62164aa85effbb406ee94c6a421b7005330 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Fri, 26 Jul 2024 12:02:34 -0600 Subject: [PATCH 09/30] Clean up & remove git --- calliope_app/compose/Dockerfile | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/calliope_app/compose/Dockerfile b/calliope_app/compose/Dockerfile index 6233d883..b99c1482 100644 --- a/calliope_app/compose/Dockerfile +++ b/calliope_app/compose/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-slim-bullseye as app +FROM python:3.8-slim-bookworm as app # set environment variables ENV LC_ALL=C.UTF-8 @@ -23,20 +23,19 @@ RUN apt-get update -y --fix-missing \ liblapack-dev \ libmetis-dev \ coinor-cbc \ + git \ && rm -rf /var/lib/apt/lists/* # install python packages WORKDIR /www COPY requirements.txt requirements-dev.txt /www/ -RUN apt-get update && apt-get install -y git RUN pip install --upgrade pip -RUN pip install https://github.com/NREL/GEOPHIRES-X/archive/main.zip -RUN pip install -r requirements.txt -RUN pip install -r requirements-dev.txt -RUN pip list +RUN pip install -r requirements.txt && pip install -r requirements-dev.txt # Install calliope without dependencies, as already installed in requirements -RUN pip install calliope==0.6.8 --no-deps -RUN pip install flower>=1.1.0 +RUN pip install calliope==0.6.8 --no-deps && pip install flower>=1.1.0 + +# Uninstall git +RUN apt remove git -y && apt autoremove -y COPY . . From 332254e7ac04de21dc4b1120c5724beef93615ed Mon Sep 17 00:00:00 2001 From: hfields Date: Fri, 26 Jul 2024 12:30:19 -0600 Subject: [PATCH 10/30] remove amsify --- .../client/static/css/amsify.select.css | 156 ------ .../client/static/js/jquery.amsifyselect.js | 449 ------------------ 2 files changed, 605 deletions(-) delete mode 100644 calliope_app/client/static/css/amsify.select.css delete mode 100644 calliope_app/client/static/js/jquery.amsifyselect.js diff --git a/calliope_app/client/static/css/amsify.select.css b/calliope_app/client/static/css/amsify.select.css deleted file mode 100644 index 18da0367..00000000 --- a/calliope_app/client/static/css/amsify.select.css +++ /dev/null @@ -1,156 +0,0 @@ -.amsify-selection-area -.amsify-selection-label-default { - cursor: pointer; - border: 1px solid #cccccc; - min-height: 20px; - padding: 8px 5px; -} - -.amsify-selection-area -.amsify-selection-label-material { - cursor: pointer; - border: 1px solid #cccccc; - min-height: 36px; - padding: 5px 5px; -} - -.amsify-selection-area -.amsify-selection-label -.amsify-label { - float: left; - padding: 0px 4px; -} - -.amsify-selection-area -.amsify-selection-label { - cursor: pointer; - height: 33px; - max-width: 700px; - border-radius: 0.25rem; -} - -.amsify-toggle-selection { - float: right; - cursor: pointer; -} - -.amsify-selection-area .amsify-selection-list { - display: none; - position: absolute; - background: white; - border: 1px solid #dedede; - z-index: 1; -} - -.amsify-selection-area -.amsify-selection-list -ul.amsify-list { - list-style: none; - padding: 3px 0px; - max-height: 150px; - overflow-y: auto; -} - -.amsify-selection-area -.amsify-selection-list -ul.amsify-list -li.amsify-list-item { - text-align: left; - cursor: pointer; - padding: 0px 10px; -} - -.amsify-selection-area -.amsify-selection-list -ul.amsify-list -li.amsify-list-group { - text-align: left; - padding: 0px 10px; - font-weight: bold; -} - -.amsify-selection-area -.amsify-selection-list -ul.amsify-list -li.amsify-item-pad { - padding-left: 30px; -} - -.amsify-selection-area -.amsify-selection-list -ul.amsify-list -li.amsify-item-noresult { - display: none; - color: #ff6060; - font-weight: bold; - text-align: center; -} - -.amsify-selection-area -.amsify-selection-list -ul.amsify-list -li.amsify-list-item:hover { - background: #bbbbbb; -} - -.amsify-selection-area -.amsify-selection-list -ul.amsify-list -li.amsify-list-item:active { - background: #717171; - color: white; - -moz-box-shadow: inset 0 0 10px #000000; - -webkit-box-shadow: inset 0 0 10px #000000; - box-shadow: inset 0 0 10px #000000; -} - -.amsify-selection-area -.amsify-selection-list -.amsify-select-input { - display: none; -} - -.amsify-selection-area -.amsify-selection-list -ul.amsify-list -li.active { - background: #bbbbbb; - font-weight: bold; -} - -.amsify-selection-area -.amsify-selection-list -ul.amsify-list -li.amsify-item-pad.active { - font-weight: normal; -} - -.amsify-selection-area -.amsify-selection-list -.amsify-select-search-area { - padding: 3px 5px; - border: 1px solid #dedede; - text-align: center; -} - -.amsify-selection-area -.amsify-selection-list -.amsify-select-search-area -.amsify-selection-search { - width: 98%; - padding: 3px 7px; - border: 1px solid #d4d4d4; -} - -.amsify-selection-area -.amsify-selection-list -.amsify-select-operations { - padding: 3px 5px; - border: 1px solid #dedede; - text-align: right; -} - -.amsify-select-clear, -.amsify-select-close { - margin: 0px 4px; -} \ No newline at end of file diff --git a/calliope_app/client/static/js/jquery.amsifyselect.js b/calliope_app/client/static/js/jquery.amsifyselect.js deleted file mode 100644 index 257cf906..00000000 --- a/calliope_app/client/static/js/jquery.amsifyselect.js +++ /dev/null @@ -1,449 +0,0 @@ -/** - * Jquery Amsifyselect - * https://github.com/amsify42/jquery.amsify.select - * http://www.amsify42.com - * https://www.jqueryscript.net/form/dropdown-search-box-amsify-select.html - */ - -(function(factory){ - if(typeof module === 'object' && typeof module.exports === 'object') { - factory(require('jquery'), window, document); - } else { - factory(jQuery, window, document); - } -}(function($, window, document, undefined){ - /** - * AmsifySelect properties - * @param object - */ - AmsifySelect = function(selector) { - this.selector = selector; - this.method = ''; - this.settings = { - type : 'bootstrap', - searchable : false, - limit: 30, - labelLimit: 3, - classes: { - clear : '', - close : '' - }, - consoleValue: false - }; - this.name = null; - this.id = null; - this.defaultLabel = 'Select'; - this.classes = { - selectArea : '.amsify-selection-area', - labelArea : '.amsify-selection-label', - labelDefault : '.amsify-selection-label-default', - labelMaterial : '.amsify-selection-label-material', - label : '.amsify-label', - toggle : '.amsify-toggle-selection', - listArea : '.amsify-selection-list', - searchArea : '.amsify-select-search-area', - search : '.amsify-selection-search', - list : '.amsify-list', - listGroup : '.amsify-list-group', - listItem : '.amsify-list-item', - itemPad : '.amsify-item-pad', - noResult : '.amsify-item-noresult', - inputType : '.amsify-select-input', - operations : '.amsify-select-operations', - clear : '.amsify-select-clear', - close : '.amsify-select-close', - }; - this.defaultClass = { - bootstrap : { - clear : 'btn btn-default', - close : 'btn btn-default', - }, - materialize : { - clear : 'btn waves-effect waves-light', - close : 'btn waves-effect waves-light', - }, - amsify : { - clear : '', - close : '', - } - }; - this.selectors = { - selectArea : null, - labelArea : null, - label : null, - toggle : null, - listArea : null, - searchArea : null, - search : null, - list : null, - operations : null, - clear : null, - close : null, - }; - this.options = []; - this.selected = []; - this.isMultiple = false; - this.isOptGroup = false; - this.isSearchable = false; - this.isHideButtons = null; - this.clearClass = null; - this.closeClass = null; - }; - - AmsifySelect.prototype = { - - _settings : function(settings) { - this.settings = $.extend(this.settings, settings); - }, - - _setMethod : function(method) { - this.method = method; - }, - - /** - * Executing all the required settings - * @param {selector} form - */ - _init : function() { - if(this.checkMethod()) { - this.name = $(this.selector).attr('name')? $(this.selector).attr('name')+'_amsify': 'amsify_selection'; - this.id = ($(this.selector).attr('id') !== undefined) ? $(this.selector).attr('id')+'_amsify_selection_area': null; - this.defaultLabel = ($(this.selector).attr('name') !== undefined) ? $(this.selector).attr('name'): "Select"; - this.isSearchable = ($(this.selector).attr('searchable') !== undefined)? true: this.settings.searchable; - this.isHideButtons = true; - this.clearClass = (this.settings.classes.clear)? this.settings.classes.clear: this.defaultClass[this.settings.type].clear; - this.closeClass = (this.settings.classes.close)? this.settings.classes.close: this.defaultClass[this.settings.type].close; - this.extractData(); - this.createHTML(); - this.setEvents(); - $(this.selector).hide(); - } - }, - - extractData : function() { - var _self = this; - - this.isMultiple = !!$(this.selector).prop('multiple'); - this.isOptGroup = !!$(this.selector).find('optgroup').length; - this.options = []; - - if(this.isOptGroup) { - $firstItem = $(this.selector).find('option:first'); - - if($firstItem.length) { - _self.options.push({ - type : 'option', - label : $firstItem.text(), - }); - } - - for (let optgroup of Array.from(this.selector.childNodes.values()).filter(e => e.tagName === 'OPTGROUP')) { - _self.options.push({ - type : 'optgroup', - label : $(optgroup).attr('label'), - }); - - for (let opt of optgroup.childNodes) { - _self.addOption(opt) - } - } - } else { - for (let opt of this.selector.childNodes) { - _self.addOption(opt); - } - } - }, - - addOption: function(option) { - const data = { - type : 'option', - value : $(option).val(), - label : $(option).text(), - selected : ($(option).attr('selected') !== undefined)? 1 : 0 - }; - - this.options.push(data); - }, - - createHTML : function() { - var HTML = '
'; - this.selectors.selectArea = $(HTML).insertAfter(this.selector); - var labelHTML = '
'; - this.selectors.labelArea = $(labelHTML).appendTo(this.selectors.selectArea); - - this.defaultLabel = (this.options[0].value)? this.defaultLabel: this.options[0].label; - var label = '
'+this.defaultLabel+'
'; - this.selectors.label = $(label).appendTo(this.selectors.labelArea); - this.selectors.toggle = $(this.toggleIcon()).appendTo(this.selectors.labelArea); - - var listArea = '
'; - this.selectors.listArea = $(listArea).appendTo(this.selectors.selectArea); - $(this.selectors.listArea).width($(this.selectors.selectArea).width()-3); - - /** - * If searchable - */ - if(this.isSearchable) { - var searchArea = '
'; - this.selectors.searchArea = $(searchArea).appendTo(this.selectors.listArea); - - var search = ''; - this.selectors.search = $(search).appendTo(this.selectors.searchArea); - } - - var list = '
    '; - this.selectors.list = $(list).appendTo(this.selectors.listArea); - - if (!this.isHideButtons) { - var operations = '
    '; - this.selectors.operations = $(operations).appendTo(this.selectors.listArea); - } - - var clear = ''; - this.selectors.clear = $(clear).appendTo(this.selectors.operations); - - var close = ''; - this.selectors.close = $(close).appendTo(this.selectors.operations); - $(this.createList()).appendTo(this.selectors.list); - this.fixCSS(); - }, - - setEvents : function() { - var _self = this; - $(this.selectors.labelArea).attr('style', $(this.selector).attr('style')).addClass($(this.selector).attr('class')); - $(this.selectors.labelArea).click(function(e){ - e.stopPropagation(); - $this = $(this).parent().find(_self.classes.listArea); - $(_self.classes.listArea).not($this).hide(); - $this.toggle(); - }); - $(document).click(function(e) { - var isGroup = $(e.target).hasClass(_self.classes.listGroup.substring(1)); - var isItem = $(e.target).hasClass(_self.classes.listItem.substring(1)); - var isClear = $(e.target).hasClass(_self.classes.clear.substring(1)); - var isSearch = $(e.target).hasClass(_self.classes.search.substring(1)); - if(!isGroup && !isItem && !isClear && !isSearch) { - $(_self.selectors.listArea).hide(); - } - }); - $(this.selectors.close).click(function(){ - $(this).closest(_self.classes.listArea).hide(); - }); - $(this.selectors.list).find(this.classes.listItem).click(function(){ - $(_self.selectors.list).find(_self.classes.listItem).removeClass('active'); - $input = $(this).find(_self.classes.inputType); - var checked = !($input.is(':checked')); - $input.prop('checked', checked); - var values = $('input[name="'+_self.name+'"]:checked').map(function(){ - return $(this).val(); - }).get(); - if(values.length > _self.settings.limit) { - alert('You cannot select more than '+_self.settings.limit); - $input.prop('checked', false); - $(this).removeClass('active'); - return false; - }; - _self.setValue(values); - }); - /** - * If searchable - */ - if(this.isSearchable) { - $(this.selectors.search).keyup(function(){ - var value = $.trim($(this).val().toLowerCase()); - _self.filterList(value); - }); - } - $(this.selectors.clear).click(function(){ - _self.clearInputs(); - }); - $(window).resize(function(){ - $(_self.selectors.listArea).width($(_self.selectors.selectArea).width()-3); - }); - this.loadExisting(); - }, - - loadExisting : function() { - var _self = this; - if(this.selected.length) { - var selected = false; - $(this.selectors.list).find(this.classes.listItem).each(function(){ - $input = $(this).find(_self.classes.inputType); - var isSelected = $.inArray($input.val(), _self.selected); - if((isSelected !== -1 && _self.isMultiple) || (isSelected !== -1 && !selected)) { - $input.prop('checked', true); - selected = true; - } - }); - this.setValue(this.selected); - } - }, - - setValue : function(values) { - var _self = this; - $(this.selector).find('option').attr('selected', false); - var label = (values.length)? '' : this.defaultLabel+', '; - $.each(this.options, function(index, option){ - if($.inArray(option.value, values) !== -1){ - label += option.label+', '; - $(_self.selector).find('[value="'+option.value+'"]').attr('selected', true); - $(_self.selectors.list).find(_self.classes.inputType).each(function(){ - if($(this).is(':checked')) { - $(this).closest(_self.classes.listItem).addClass('active'); - } - }); - if(!_self.isMultiple) { - return false; - } - } - }); - label = (values.length >= this.settings.labelLimit)? values.length+' selected': label.slice(0, -2); - $(this.selectors.label).text(label); - $(this.selector).change(); - if(!_self.isMultiple) { - $(this.selectors.listArea).hide(); - } - if(this.settings.consoleValue) { - console.info($(this.selector).val()); - } - }, - - toggleIcon : function() { - if(this.settings.type === 'bootstrap') { - return ''; - } else if(this.settings.type === 'materialize') { - return 'arrow_drop_down'; - } else { - return ''; - } - }, - - createList : function() { - var _self = this; - var listHTML = ''; - var selected = false; - $.each(this.options, function(index, option){ - if(option.type === 'optgroup') { - listHTML += '
  • '+option.label+'
  • '; - } else if(option.value) { - var isActive = ((option.selected && _self.isMultiple) || (option.selected && !selected))? 'active': ''; - isActive += (_self.isOptGroup)? ' '+_self.classes.itemPad.substring(1): ''; - listHTML += '
  • '+_self.getInputType(option.value)+' '+option.label+'
  • '; - if(option.selected) { - _self.selected.push(option.value); - selected = true; - } - } - }); - if(this.isSearchable) { - listHTML += '
  • No matching options
  • '; - } - return listHTML; - }, - - getInputType : function(value) { - var type = (this.isMultiple)? 'checkbox': 'radio'; - var inputClass = this.classes.inputType.substring(1); - return ''; - }, - - filterList : function(value) { - const _self = this; - - $(this.selectors.list).find(this.classes.noResult).hide(); - - if(this.isOptGroup) { - $(this.selectors.list).find(this.classes.listGroup).hide(); - } - - let lis = $(this.selectors.list).find(this.classes.listItem) - let matchingLisIndices = [] - - lis.each(function(index, el){ - if(el.innerText.toLowerCase().indexOf(value) !== -1) { - matchingLisIndices.push(index) - } - }); - - lis.each(function(index, el){ - el.style.display = 'none'; - }); - - for (let i of matchingLisIndices) { - const el = lis[i]; - el.style.display = 'list-item'; - if (_self.isOptGroup) { - $(el).prevAll(_self.classes.listGroup + ':first').show(); - } - } - - if(!matchingLisIndices.length) { - $(this.selectors.list).find(this.classes.noResult).show(); - } - }, - - clearInputs : function() { - $(this.selector).find(':selected').attr('selected', false); - $(this.selectors.list).find(this.classes.listItem).removeClass('active'); - $(this.selectors.list).find(this.classes.inputType).prop('checked', false); - $(this.selectors.label).text(this.defaultLabel); - /** - * If searchable - */ - if(this.isSearchable) { - $(this.selectors.search).val(''); - } - this.filterList(''); - }, - - fixCSS : function() { - if(this.settings.type === 'materialize') { - $(this.selectors.labelArea).addClass(this.classes.labelMaterial.substring(1)); - $(this.selectors.searchArea).css('max-height', '46px'); - $(this.selectors.search).css('max-height', '28px'); - } else if(this.settings.type === 'bootstrap') { - $(this.selectors.search).css('width', '100%'); - } else { - $(this.selectors.labelArea).addClass(this.classes.labelDefault.substring(1)); - } - }, - - getSelectionAreaId : function() { - return this.id ? 'id="' + this.id + '"' : '' - }, - - refresh : function() { - this._setMethod('refresh'); - this._init(); - }, - - destroy : function() { - this._setMethod('destroy'); - this._init(); - }, - - checkMethod : function() { - $findArea = $(this.selector).next(this.classes.selectArea); - if($findArea.length) { - $findArea.remove(); - } - $(this.selector).show(); - return !(this.method === 'destroy') - }, - - }; - - $.fn.amsifySelect = function(settings, method) { - /** - * Initializing each instance of selector - * @return {object} - */ - return this.each(function() { - var amsifySelect = new AmsifySelect(this); - amsifySelect._settings(settings); - amsifySelect._setMethod(method); - amsifySelect._init(); - }); - }; - -})); From 923bb7230f3e6695288a20ae1a152f6646e1d269 Mon Sep 17 00:00:00 2001 From: hfields Date: Fri, 26 Jul 2024 12:30:40 -0600 Subject: [PATCH 11/30] add bootstrap dropdown select --- calliope_app/client/templates/base.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/calliope_app/client/templates/base.html b/calliope_app/client/templates/base.html index b8e1594d..fa6411b2 100644 --- a/calliope_app/client/templates/base.html +++ b/calliope_app/client/templates/base.html @@ -21,6 +21,8 @@ + + {% if mapbox_token %} From 66233987846ca0c3358a70d3a93ca780881f6757 Mon Sep 17 00:00:00 2001 From: hfields Date: Fri, 26 Jul 2024 13:17:58 -0600 Subject: [PATCH 12/30] add bootstrap dropdown to frontend --- calliope_app/client/static/css/main.css | 5 + calliope_app/client/static/js/scenarios.js | 194 ++++++++++-------- .../scenario_constraints_json_form.html | 5 - 3 files changed, 110 insertions(+), 94 deletions(-) diff --git a/calliope_app/client/static/css/main.css b/calliope_app/client/static/css/main.css index 7b3a6c66..bdb4c8a2 100644 --- a/calliope_app/client/static/css/main.css +++ b/calliope_app/client/static/css/main.css @@ -305,6 +305,11 @@ select { padding: 0 !important; } +button.multiselect.dropdown-toggle { + border-radius: 5px; + border: 1px solid #ced4da; +} + .nav-dropdown, .select2, .select2-selection { background-color: #ccd0d6 !important; font-size: 1.2em; diff --git a/calliope_app/client/static/js/scenarios.js b/calliope_app/client/static/js/scenarios.js index 761ea4cd..5d8d6091 100644 --- a/calliope_app/client/static/js/scenarios.js +++ b/calliope_app/client/static/js/scenarios.js @@ -255,14 +255,14 @@ function updateDialogObject() { } var techsInputLhs = $("#" + constraintId + "_techs_lhs").val(); - if (techsInput && techsInput.length > 0) { + if (techsInputLhs && techsInputLhs.length > 0) { dialogObj[constraint].techs_lhs = techsInputLhs; } else { delete dialogObj[constraint].techs_lhs; } var techsInputRhs = $("#" + constraintId + "_techs_rhs").val(); - if (techsInput && techsInput.length > 0) { + if (techsInputRhs && techsInputRhs.length > 0) { dialogObj[constraint].techs_rhs = techsInputRhs; } else { delete dialogObj[constraint].techs_rhs; @@ -277,14 +277,14 @@ function updateDialogObject() { } var locsInputLhs = $("#" + constraintId + "_locs_lhs").val(); - if (locsInput && locsInput.length > 0) { + if (locsInputLhs && locsInputLhs.length > 0) { dialogObj[constraint].locs_lhs = locsInputLhs; } else { delete dialogObj[constraint].locs_lhs; } var locsInputRhs = $("#" + constraintId + "_locs_rhs").val(); - if (locsInput && locsInput.length > 0) { + if (locsInputRhs && locsInputRhs.length > 0) { dialogObj[constraint].locs_rhs = locsInputRhs; } else { delete dialogObj[constraint].locs_rhs; @@ -429,8 +429,12 @@ function updateDialogGroupConstraints(initialLoad) { } updateConstraintTypes(constraint, constraintId, constraintContent); - $('#' + constraintId + '_locs, #' + constraintId + '_locs_lhs, #' + constraintId + '_locs_rhs, #' + constraintId + '_techs, #' + constraintId + '_techs_lhs, #' + constraintId + '_techs_rhs').amsifySelect({ - type : 'amsify', + // Initialize Selects for each dropdown + $('#' + constraintId + '_locs, #' + constraintId + '_locs_lhs, #' + constraintId + '_locs_rhs, #' + constraintId + '_techs, #' + constraintId + '_techs_lhs, #' + constraintId + '_techs_rhs').multiselect({ + includeSelectAllOption: false, + enableFiltering: true, + enableCaseInsensitiveFiltering: true, + buttonWidth: '550px' }); if (!dialogObj[constraint].techs_rhs && !dialogObj[constraint].techs_lhs) { @@ -636,7 +640,7 @@ function activate_scenario_settings() { // Constraint Group Modal $('.scenario-constraints-dialog-btn').on('click', function() { - + // display dialog $('#pvwatts_form').hide(); $('#wtk_form').hide(); @@ -651,90 +655,13 @@ function activate_scenario_settings() { dialogInputValue = dialogInputValue.replace(/'/g, '"'); dialogObj = parseJSON(dialogInputValue, {}); - Object.keys(dialogObj).forEach(constraint => { - Object.keys(dialogObj[constraint]).forEach(fieldKey => { - // Technologies - if (fieldKey === "techs") { - var newTechs = ""; - if (dialogObj[constraint].techs && dialogObj[constraint].techs.length > 0) { - for (var tech in dialogObj[constraint].techs) { - newTechs += dialogObj[constraint].techs[tech]; - if (Number(tech) !== dialogObj[constraint].techs.length-1) { - newTechs += "," - } - } - } - dialogObj[constraint].techs = newTechs; - } - if (fieldKey === "techs_lhs") { - var newTechs = ""; - if (dialogObj[constraint].techs_lhs && dialogObj[constraint].techs_lhs.length > 0) { - for (var tech in dialogObj[constraint].techs_lhs) { - newTechs += dialogObj[constraint].techs_lhs[tech]; - if (Number(tech) !== dialogObj[constraint].techs_lhs.length-1) { - newTechs += "," - } - } - } - dialogObj[constraint].techs_lhs = newTechs; - } - if (fieldKey === "techs_rhs") { - var newTechs = ""; - if (dialogObj[constraint].techs_rhs && dialogObj[constraint].techs_rhs.length > 0) { - for (var tech in dialogObj[constraint].techs_rhs) { - newTechs += dialogObj[constraint].techs_rhs[tech]; - if (Number(tech) !== dialogObj[constraint].techs_rhs.length-1) { - newTechs += "," - } - } - } - dialogObj[constraint].techs_rhs = newTechs; - } - - // Locations - if (fieldKey === "locs") { - var newLocs = ""; - if (dialogObj[constraint].locs && dialogObj[constraint].locs.length > 0) { - for (var loc in dialogObj[constraint].locs) { - newLocs += dialogObj[constraint].locs[loc]; - if (Number(loc) !== dialogObj[constraint].locs.length-1) { - newLocs += "," - } - } - } - dialogObj[constraint].locs = newLocs; - } - if (fieldKey === "locs_lhs") { - var newLocs = ""; - if (dialogObj[constraint].locs_lhs && dialogObj[constraint].locs_lhs.length > 0) { - for (var loc in dialogObj[constraint].locs_lhs) { - newLocs += dialogObj[constraint].locs_lhs[loc]; - if (Number(loc) !== dialogObj[constraint].locs_lhs.length-1) { - newLocs += "," - } - } - } - dialogObj[constraint].locs_lhs = newLocs; - } - if (fieldKey === "locs_rhs") { - var newLocs = ""; - if (dialogObj[constraint].locs_rhs && dialogObj[constraint].locs_rhs.length > 0) { - for (var loc in dialogObj[constraint].locs_rhs) { - newLocs += dialogObj[constraint].locs_rhs[loc]; - if (Number(loc) !== dialogObj[constraint].locs_rhs.length-1) { - newLocs += "," - } - } - } - dialogObj[constraint].locs_rhs = newLocs; - } - }); - + processDialogObj(dialogObj).then(() => { + getModelCarriers(); + updateDialogGroupConstraints(true); + }).catch(error => { + console.error("Error processing dialogObj:", error); }); - getModelCarriers(); - updateDialogGroupConstraints(true); - $('#tabs li a:not(:first)').addClass('inactive'); $('.tab-container').hide(); $('.tab-container:first').show(); @@ -757,6 +684,95 @@ function activate_scenario_settings() { }); + function processDialogObj(dialogObj) { + return new Promise((resolve, reject) => { + try { + Object.keys(dialogObj).forEach(constraint => { + Object.keys(dialogObj[constraint]).forEach(fieldKey => { + // Technologies + if (fieldKey === "techs") { + var newTechs = ""; + if (dialogObj[constraint].techs && dialogObj[constraint].techs.length > 0) { + for (var tech in dialogObj[constraint].techs) { + newTechs += dialogObj[constraint].techs[tech]; + if (Number(tech) !== dialogObj[constraint].techs.length - 1) { + newTechs += ","; + } + } + } + dialogObj[constraint].techs = newTechs; + } + if (fieldKey === "techs_lhs") { + var newTechs = ""; + if (dialogObj[constraint].techs_lhs && dialogObj[constraint].techs_lhs.length > 0) { + for (var tech in dialogObj[constraint].techs_lhs) { + newTechs += dialogObj[constraint].techs_lhs[tech]; + if (Number(tech) !== dialogObj[constraint].techs_lhs.length - 1) { + newTechs += ","; + } + } + } + dialogObj[constraint].techs_lhs = newTechs; + } + if (fieldKey === "techs_rhs") { + var newTechs = ""; + if (dialogObj[constraint].techs_rhs && dialogObj[constraint].techs_rhs.length > 0) { + for (var tech in dialogObj[constraint].techs_rhs) { + newTechs += dialogObj[constraint].techs_rhs[tech]; + if (Number(tech) !== dialogObj[constraint].techs_rhs.length - 1) { + newTechs += ","; + } + } + } + dialogObj[constraint].techs_rhs = newTechs; + } + + // Locations + if (fieldKey === "locs") { + var newLocs = ""; + if (dialogObj[constraint].locs && dialogObj[constraint].locs.length > 0) { + for (var loc in dialogObj[constraint].locs) { + newLocs += dialogObj[constraint].locs[loc]; + if (Number(loc) !== dialogObj[constraint].locs.length - 1) { + newLocs += ","; + } + } + } + dialogObj[constraint].locs = newLocs; + } + if (fieldKey === "locs_lhs") { + var newLocs = ""; + if (dialogObj[constraint].locs_lhs && dialogObj[constraint].locs_lhs.length > 0) { + for (var loc in dialogObj[constraint].locs_lhs) { + newLocs += dialogObj[constraint].locs_lhs[loc]; + if (Number(loc) !== dialogObj[constraint].locs_lhs.length - 1) { + newLocs += ","; + } + } + } + dialogObj[constraint].locs_lhs = newLocs; + } + if (fieldKey === "locs_rhs") { + var newLocs = ""; + if (dialogObj[constraint].locs_rhs && dialogObj[constraint].locs_rhs.length > 0) { + for (var loc in dialogObj[constraint].locs_rhs) { + newLocs += dialogObj[constraint].locs_rhs[loc]; + if (Number(loc) !== dialogObj[constraint].locs_rhs.length - 1) { + newLocs += ","; + } + } + } + dialogObj[constraint].locs_rhs = newLocs; + } + }); + }); + resolve(); + } catch (error) { + reject(error); + } + }); + } + $('#settings_import_data').on('click', function() { updateDialogObject(); $('textarea[name="edit' + dialogInputId + '"]').text(JSON.stringify(dialogObj, undefined, 2)); diff --git a/calliope_app/client/templates/scenario_constraints_json_form.html b/calliope_app/client/templates/scenario_constraints_json_form.html index d6262f8b..7ee3e572 100644 --- a/calliope_app/client/templates/scenario_constraints_json_form.html +++ b/calliope_app/client/templates/scenario_constraints_json_form.html @@ -99,11 +99,6 @@ padding-top: 20px; } -.amsify-label { - width: 650px; - padding-top: 10px; -} - .cateogry-expander { font-size: 24px; background-color: #ccd0d6; From 7b4095af1c73cdf3cef92b29a16525d4e0c17919 Mon Sep 17 00:00:00 2001 From: hfields Date: Fri, 26 Jul 2024 13:43:32 -0600 Subject: [PATCH 13/30] Fix tag bug --- calliope_app/client/static/js/scenarios.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/calliope_app/client/static/js/scenarios.js b/calliope_app/client/static/js/scenarios.js index 5d8d6091..b9c146c6 100644 --- a/calliope_app/client/static/js/scenarios.js +++ b/calliope_app/client/static/js/scenarios.js @@ -353,7 +353,7 @@ function updateDialogGroupConstraints(initialLoad) { ""); for (var t in technologies) { if (technologies[t].tag){ - $('#' + constraintId + '_techs').append(''); + $('#' + constraintId + '_techs').append(''); } else { $('#' + constraintId + '_techs').append(''); } @@ -367,7 +367,7 @@ function updateDialogGroupConstraints(initialLoad) { ""); for (var t in technologies) { if (technologies[t].tag){ - $('#' + constraintId + '_techs_lhs').append(''); + $('#' + constraintId + '_techs_lhs').append(''); } else { $('#' + constraintId + '_techs_lhs').append(''); } @@ -381,7 +381,7 @@ function updateDialogGroupConstraints(initialLoad) { ""); for (var t in technologies) { if (technologies[t].tag){ - $('#' + constraintId + '_techs_rhs').append(''); + $('#' + constraintId + '_techs_rhs').append(''); } else { $('#' + constraintId + '_techs_rhs').append(''); } From 468ba92fd89916bfc8f4ead84fbc8da74b969050 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Fri, 26 Jul 2024 16:51:51 -0600 Subject: [PATCH 14/30] Bump version to v1.1.2 --- calliope_app/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calliope_app/version.py b/calliope_app/version.py index a82b376d..72f26f59 100644 --- a/calliope_app/version.py +++ b/calliope_app/version.py @@ -1 +1 @@ -__version__ = "1.1.1" +__version__ = "1.1.2" From cba08c6721acbff7b98290596d219f7e721119fe Mon Sep 17 00:00:00 2001 From: hfields Date: Mon, 29 Jul 2024 14:48:09 -0600 Subject: [PATCH 15/30] Remove remaining amsify references --- calliope_app/client/static/js/scenarios.js | 12 ++++++------ calliope_app/client/templates/base.html | 2 -- .../client/templates/scenario_weights_json_form.html | 5 ----- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/calliope_app/client/static/js/scenarios.js b/calliope_app/client/static/js/scenarios.js index b9c146c6..2dad7329 100644 --- a/calliope_app/client/static/js/scenarios.js +++ b/calliope_app/client/static/js/scenarios.js @@ -349,7 +349,7 @@ function updateDialogGroupConstraints(initialLoad) { //Display techs and locs first dialogObj[constraint].techs = dialogObj[constraint].techs ? dialogObj[constraint].techs : ""; - $(constraintContent).append("

    " + + $(constraintContent).append("

    " + "
    "); for (var t in technologies) { if (technologies[t].tag){ @@ -363,7 +363,7 @@ function updateDialogGroupConstraints(initialLoad) { }); dialogObj[constraint].techs_lhs = dialogObj[constraint].techs_lhs ? dialogObj[constraint].techs_lhs : ""; - $(constraintContent).append("

    " + + $(constraintContent).append("

    " + "
    "); for (var t in technologies) { if (technologies[t].tag){ @@ -377,7 +377,7 @@ function updateDialogGroupConstraints(initialLoad) { }); dialogObj[constraint].techs_rhs = dialogObj[constraint].techs_rhs ? dialogObj[constraint].techs_rhs : ""; - $(constraintContent).append("

    " + + $(constraintContent).append("

    " + "
    "); for (var t in technologies) { if (technologies[t].tag){ @@ -395,7 +395,7 @@ function updateDialogGroupConstraints(initialLoad) { } dialogObj[constraint].locs = dialogObj[constraint].locs ? dialogObj[constraint].locs : ""; - $(constraintContent).append("

    " + + $(constraintContent).append("

    " + "
    "); for (var l in locations) { $('#' + constraintId + '_locs').append(''); @@ -405,7 +405,7 @@ function updateDialogGroupConstraints(initialLoad) { }); dialogObj[constraint].locs_lhs = dialogObj[constraint].locs_lhs ? dialogObj[constraint].locs_lhs : ""; - $(constraintContent).append("

    " + + $(constraintContent).append("

    " + "
    "); for (var l in locations) { $('#' + constraintId + '_locs_lhs').append(''); @@ -415,7 +415,7 @@ function updateDialogGroupConstraints(initialLoad) { }); dialogObj[constraint].locs_rhs = dialogObj[constraint].locs_rhs ? dialogObj[constraint].locs_rhs : ""; - $(constraintContent).append("

    " + + $(constraintContent).append("

    " + "
    "); for (var l in locations) { $('#' + constraintId + '_locs_rhs').append(''); diff --git a/calliope_app/client/templates/base.html b/calliope_app/client/templates/base.html index fa6411b2..25b9d740 100644 --- a/calliope_app/client/templates/base.html +++ b/calliope_app/client/templates/base.html @@ -15,8 +15,6 @@ - - diff --git a/calliope_app/client/templates/scenario_weights_json_form.html b/calliope_app/client/templates/scenario_weights_json_form.html index 9b6a99e3..4534e03c 100644 --- a/calliope_app/client/templates/scenario_weights_json_form.html +++ b/calliope_app/client/templates/scenario_weights_json_form.html @@ -99,11 +99,6 @@ padding-top: 20px; } -.amsify-label { - width: 650px; - padding-top: 10px; -} - .cateogry-expander { font-size: 24px; background-color: #ccd0d6; From 5e023c11627c54b1a51e36ee80f7b9a1ebf41fe6 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Mon, 29 Jul 2024 16:47:22 -0600 Subject: [PATCH 16/30] Add ctx and etx based on locs --- calliope_app/api/models/outputs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/calliope_app/api/models/outputs.py b/calliope_app/api/models/outputs.py index 3219b2b7..c1f62fae 100644 --- a/calliope_app/api/models/outputs.py +++ b/calliope_app/api/models/outputs.py @@ -272,18 +272,21 @@ def get_static_values(self, meta, metric, location, if not df.empty: df = df.groupby('techs').sum() df = df['values'].to_dict() + # Process Max Bounds Context (ctx) if ctx is not None: ctx = ctx.replace(np.inf, np.nan).dropna() if location: ctx = ctx[ctx['locs'] == location] if etx is not None: - etx = etx[etx['techs'].isin(ctx['techs'])] - ctx['values'] = ctx['values'].add(etx['values'],fill_value=0) + merged = pd.merge(ctx, etx, on='locs', how='left', suffix=('_ctx', '_etx')) + merged.fillna(0, inplace=True) + ctx['values'] = merged["values_ctx"] + merged["values_etx"] ctx = ctx.groupby('techs').sum() ctx = ctx['values'].to_dict() else: ctx = {} + # Viz Layers layers = [{'key': key, 'name': meta['names'][key] if key in meta['names'] else key, From 1bd14a6ee653ece0485189c58128e34f47e5a781 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Tue, 30 Jul 2024 09:22:28 -0600 Subject: [PATCH 17/30] Bump the version to 1.1.3 --- calliope_app/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calliope_app/version.py b/calliope_app/version.py index 72f26f59..0b2f79db 100644 --- a/calliope_app/version.py +++ b/calliope_app/version.py @@ -1 +1 @@ -__version__ = "1.1.2" +__version__ = "1.1.3" From cfe35d70e253f0d78d6779d3aada5185d6753342 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Tue, 30 Jul 2024 10:57:27 -0600 Subject: [PATCH 18/30] Typo fix on pandas merge --- calliope_app/api/models/outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calliope_app/api/models/outputs.py b/calliope_app/api/models/outputs.py index c1f62fae..01112624 100644 --- a/calliope_app/api/models/outputs.py +++ b/calliope_app/api/models/outputs.py @@ -279,7 +279,7 @@ def get_static_values(self, meta, metric, location, if location: ctx = ctx[ctx['locs'] == location] if etx is not None: - merged = pd.merge(ctx, etx, on='locs', how='left', suffix=('_ctx', '_etx')) + merged = pd.merge(ctx, etx, on='locs', how='left', suffixes=('_ctx', '_etx')) merged.fillna(0, inplace=True) ctx['values'] = merged["values_ctx"] + merged["values_etx"] ctx = ctx.groupby('techs').sum() From bd17feac7b4c3ffa5a59c5d4b4e994927b5d74f6 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Tue, 30 Jul 2024 18:39:08 -0600 Subject: [PATCH 19/30] Continue to fix max constraint issue --- calliope_app/api/models/outputs.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/calliope_app/api/models/outputs.py b/calliope_app/api/models/outputs.py index 01112624..030a41be 100644 --- a/calliope_app/api/models/outputs.py +++ b/calliope_app/api/models/outputs.py @@ -279,11 +279,15 @@ def get_static_values(self, meta, metric, location, if location: ctx = ctx[ctx['locs'] == location] if etx is not None: - merged = pd.merge(ctx, etx, on='locs', how='left', suffixes=('_ctx', '_etx')) + ctx_g = ctx.groupby('techs').sum(["values"]) + etx_g = etx.groupby('techs').sum(["values"]) + merged = pd.merge(ctx_g, etx_g, on='techs', how='left', suffixes=('_ctx_g', '_etx_g')) merged.fillna(0, inplace=True) - ctx['values'] = merged["values_ctx"] + merged["values_etx"] - ctx = ctx.groupby('techs').sum() - ctx = ctx['values'].to_dict() + merged['values'] = merged["values_ctx_g"] + merged["values_etx_g"] + ctx = merged['values'].to_dict() + else: + ctx = ctx.groupby('techs').sum() + ctx = ctx['values'].to_dict() else: ctx = {} From 2365eea2c87c3ea0aa8d0472584d589690615ef3 Mon Sep 17 00:00:00 2001 From: hfields Date: Thu, 8 Aug 2024 23:48:54 -0600 Subject: [PATCH 20/30] fix scroll on multiselect group constraint dropdowns --- calliope_app/client/static/css/main.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/calliope_app/client/static/css/main.css b/calliope_app/client/static/css/main.css index bdb4c8a2..94112ea3 100644 --- a/calliope_app/client/static/css/main.css +++ b/calliope_app/client/static/css/main.css @@ -310,6 +310,12 @@ button.multiselect.dropdown-toggle { border: 1px solid #ced4da; } +.multiselect-container.dropdown-menu.show { + overflow-y: scroll; + max-height: 250px; + min-width: 525px; +} + .nav-dropdown, .select2, .select2-selection { background-color: #ccd0d6 !important; font-size: 1.2em; From 28e93008a6d5ac12ed1aba1a1978eaa2dae9dca2 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Wed, 28 Aug 2024 08:59:58 -0600 Subject: [PATCH 21/30] Upgrade Django to 4.2.15 --- calliope_app/account/urls.py | 12 ++++++------ calliope_app/calliope_app/settings/local.py | 2 +- calliope_app/calliope_app/settings/prod.py | 2 +- calliope_app/calliope_app/settings/test.py | 2 +- calliope_app/requirements.txt | 2 +- calliope_app/template/urls.py | 3 --- 6 files changed, 10 insertions(+), 13 deletions(-) diff --git a/calliope_app/account/urls.py b/calliope_app/account/urls.py index fe81ac49..2747e3c9 100644 --- a/calliope_app/account/urls.py +++ b/calliope_app/account/urls.py @@ -1,6 +1,6 @@ from django.contrib.auth import views as auth_views from django.conf import settings -from django.conf.urls import url +from django.urls import re_path from django.urls import path from account import views @@ -26,8 +26,8 @@ views.set_timezone, name='set_timezone' ), - - url( + + re_path( r'^password_reset/$', auth_views.PasswordResetView.as_view( template_name='registration/pw_reset_form.html', @@ -37,14 +37,14 @@ ), name='password_reset' ), - url( + re_path( r'^password_reset/done/$', auth_views.PasswordResetDoneView.as_view( template_name='registration/pw_reset_done.html' ), name='password_reset_done' ), - url( + re_path( r'^reset/\ (?P[0-9A-Za-z_\-]+)/\ (?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,40})/$', @@ -53,7 +53,7 @@ ), name='password_reset_confirm' ), - url( + re_path( r'^reset/done/$', auth_views.PasswordResetCompleteView.as_view( template_name='registration/pw_reset_complete.html' diff --git a/calliope_app/calliope_app/settings/local.py b/calliope_app/calliope_app/settings/local.py index 1bfb2f01..26934ec6 100644 --- a/calliope_app/calliope_app/settings/local.py +++ b/calliope_app/calliope_app/settings/local.py @@ -39,7 +39,7 @@ # -------------------------------------------------------------------------------- DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'HOST': env.str('POSTGRES_HOST'), 'PORT': env.str('POSTGRES_PORT'), 'USER': env.str('POSTGRES_USER'), diff --git a/calliope_app/calliope_app/settings/prod.py b/calliope_app/calliope_app/settings/prod.py index b314e0a2..99b4a1f4 100644 --- a/calliope_app/calliope_app/settings/prod.py +++ b/calliope_app/calliope_app/settings/prod.py @@ -17,7 +17,7 @@ # -------------------------------------------------------------------------------- DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'HOST': env.str('POSTGRES_HOST'), 'PORT': env.str('POSTGRES_PORT'), 'USER': env.str('POSTGRES_USER'), diff --git a/calliope_app/calliope_app/settings/test.py b/calliope_app/calliope_app/settings/test.py index 7e066b35..cb118f32 100644 --- a/calliope_app/calliope_app/settings/test.py +++ b/calliope_app/calliope_app/settings/test.py @@ -23,7 +23,7 @@ # -------------------------------------------------------------------------------- DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'HOST': env.str('POSTGRES_HOST'), 'PORT': env.str('POSTGRES_PORT'), 'USER': env.str('POSTGRES_USER'), diff --git a/calliope_app/requirements.txt b/calliope_app/requirements.txt index daf9d475..b99711d2 100644 --- a/calliope_app/requirements.txt +++ b/calliope_app/requirements.txt @@ -2,7 +2,7 @@ django_ratelimit==4.1.0 git+https://github.com/NREL/GEOPHIRES-X.git#egg=geophires-x boto3==1.24.37 celery[redis]==5.3.0 -django==3.2.25 +django==4.2.15 django-crispy-forms==1.14.0 django-environ>=0.4.5 django-modeltranslation==0.18.12 diff --git a/calliope_app/template/urls.py b/calliope_app/template/urls.py index 1e410db0..b0753de3 100644 --- a/calliope_app/template/urls.py +++ b/calliope_app/template/urls.py @@ -1,6 +1,3 @@ -from django.contrib.auth import views as auth_views -from django.conf import settings -from django.conf.urls import url from django.urls import path from template import views From d5918bf4e88cfbfc8400e7d56905fb18ad6a4c4f Mon Sep 17 00:00:00 2001 From: jgu2 Date: Wed, 28 Aug 2024 09:24:41 -0600 Subject: [PATCH 22/30] Update django imports --- calliope_app/account/urls.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/calliope_app/account/urls.py b/calliope_app/account/urls.py index 2747e3c9..13335979 100644 --- a/calliope_app/account/urls.py +++ b/calliope_app/account/urls.py @@ -1,7 +1,6 @@ from django.contrib.auth import views as auth_views from django.conf import settings -from django.urls import re_path -from django.urls import path +from django.urls import path, re_path from account import views From 0a65d54852fa157105b94f4713b6a081e0beff42 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Wed, 28 Aug 2024 13:21:16 -0600 Subject: [PATCH 23/30] Bump version to v1.1.4 --- calliope_app/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calliope_app/version.py b/calliope_app/version.py index 0b2f79db..c72e3798 100644 --- a/calliope_app/version.py +++ b/calliope_app/version.py @@ -1 +1 @@ -__version__ = "1.1.3" +__version__ = "1.1.4" From 6faf124d7f7e1c2a96ffd455454def6e47da1201 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Mon, 16 Sep 2024 23:43:20 -0600 Subject: [PATCH 24/30] Add solver list --- calliope_app/api/admin.py | 4 +- calliope_app/api/forms.py | 15 + ...move_computeenvironment_solver_and_more.py | 23 + calliope_app/api/models/engage.py | 8 +- calliope_app/api/urls.py | 2 + calliope_app/api/views/outputs.py | 27 +- calliope_app/client/static/js/add_run.js | 442 +++++++++--------- calliope_app/client/templates/add_run.html | 46 +- calliope_app/client/widgets.py | 44 ++ calliope_app/compose/Dockerfile | 2 +- 10 files changed, 373 insertions(+), 240 deletions(-) create mode 100644 calliope_app/api/forms.py create mode 100644 calliope_app/api/migrations/0068_remove_computeenvironment_solver_and_more.py create mode 100644 calliope_app/client/widgets.py diff --git a/calliope_app/api/admin.py b/calliope_app/api/admin.py index da0b5c06..276c9cec 100644 --- a/calliope_app/api/admin.py +++ b/calliope_app/api/admin.py @@ -9,11 +9,13 @@ Scenario_Loc_Tech, Scenario_Param, Job_Meta, Carrier from api.models.outputs import Run from api.models.engage import User_Profile, ComputeEnvironment +from api.forms import ComputeEnvironmentModelForm class ComputeEnvironmentAdmin(admin.ModelAdmin): + form = ComputeEnvironmentModelForm filter_horizontal = ("users",) - list_display = ['id', 'name', 'full_name', 'is_default', 'solver', 'ncpu', 'memory', 'type', '_users'] + list_display = ['id', 'name', 'full_name', 'is_default', 'solvers', 'ncpu', 'memory', 'type', '_users'] @staticmethod def _users(instance): diff --git a/calliope_app/api/forms.py b/calliope_app/api/forms.py new file mode 100644 index 00000000..d300bf2b --- /dev/null +++ b/calliope_app/api/forms.py @@ -0,0 +1,15 @@ + +from django import forms + +from client.widgets import JSONEditorWidget + +from api.models.engage import ComputeEnvironment + + +class ComputeEnvironmentModelForm(forms.ModelForm): + class Meta: + model = ComputeEnvironment + fields = '__all__' + widgets = { + 'solvers': JSONEditorWidget() + } diff --git a/calliope_app/api/migrations/0068_remove_computeenvironment_solver_and_more.py b/calliope_app/api/migrations/0068_remove_computeenvironment_solver_and_more.py new file mode 100644 index 00000000..4d548fd6 --- /dev/null +++ b/calliope_app/api/migrations/0068_remove_computeenvironment_solver_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.15 on 2024-09-17 03:26 + +import api.models.engage +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0067_auto_20240613_1706'), + ] + + operations = [ + migrations.RemoveField( + model_name='computeenvironment', + name='solver', + ), + migrations.AddField( + model_name='computeenvironment', + name='solvers', + field=models.JSONField(default=api.models.engage.default_solvers), + ), + ] diff --git a/calliope_app/api/models/engage.py b/calliope_app/api/models/engage.py index e0b19b64..dc1bd103 100644 --- a/calliope_app/api/models/engage.py +++ b/calliope_app/api/models/engage.py @@ -102,6 +102,10 @@ def activate(cls, activation_uuid): return True +def default_solvers(): + return {"appsi_highs": "HiGHS", "cbc": "CBC"} + + class ComputeEnvironment(models.Model): ENV_TYPES = [ @@ -114,11 +118,11 @@ class ComputeEnvironment(models.Model): full_name = models.CharField(max_length=120) is_default = models.BooleanField(default=False) type = models.CharField(max_length=60, choices=ENV_TYPES) - solver = models.CharField(max_length=60, null=True, blank=True) ncpu = models.PositiveSmallIntegerField(null=True, blank=True) memory = models.PositiveSmallIntegerField(null=True, blank=True) cmd = models.TextField(blank=True, null=True) users = models.ManyToManyField(User, related_name="compute_environments", blank=True) + solvers = models.JSONField(default=default_solvers) class Meta: db_table = "compute_environments" @@ -141,4 +145,4 @@ class Meta: verbose_name_plural = "[Admin] Request Rate Limits" def __str__(self): - return f"{self.year}, {self.month}, {self.total}" \ No newline at end of file + return f"{self.year}, {self.month}, {self.total}" diff --git a/calliope_app/api/urls.py b/calliope_app/api/urls.py index e8dfb753..8431adb9 100644 --- a/calliope_app/api/urls.py +++ b/calliope_app/api/urls.py @@ -133,6 +133,8 @@ path('upload_outputs/', outputs_views.upload_outputs, name='upload_outputs'), + path('solvers/', outputs_views.solvers, + name="solvers"), # Bulk Data path('upload_locations/', diff --git a/calliope_app/api/views/outputs.py b/calliope_app/api/views/outputs.py index 8515cc73..522e158c 100644 --- a/calliope_app/api/views/outputs.py +++ b/calliope_app/api/views/outputs.py @@ -3,16 +3,13 @@ import io import logging import os -import shutil import zipfile -import sys from re import match from datetime import datetime, timedelta from urllib.parse import urljoin import requests import pandas as pd -import pint from celery import current_app,chain from django.views.decorators.csrf import csrf_protect @@ -30,7 +27,7 @@ from api.models.calliope import Abstract_Tech, Abstract_Tech_Param, Parameter, Run_Parameter from api.models.configuration import ( Model, ParamsManager, User_File, Location, Technology, - Tech_Param, Loc_Tech, Loc_Tech_Param, Timeseries_Meta, Carrier, Scenario_Param + Tech_Param, Loc_Tech, Loc_Tech_Param, Timeseries_Meta, Carrier ) from api.models.engage import ComputeEnvironment @@ -38,11 +35,19 @@ from batch.managers import AWSBatchJobManager from taskmeta.models import CeleryTask, BatchTask, batch_task_status -from calliope_app.celery import app - logger = logging.getLogger(__name__) +@csrf_protect +def solvers(request): + env_name = request.GET.get("env_name", None) + if not env_name: + env_name = "default" + env = ComputeEnvironment.objects.get(name=env_name) + payload = env.solvers + return HttpResponse(json.dumps(payload), content_type="application/json") + + @csrf_protect def build(request): """ @@ -156,13 +161,13 @@ def build(request): ) inputs_path = inputs_path.lower().replace(" ", "-") os.makedirs(inputs_path, exist_ok=True) - + run.run_options = [] for id in parameters.keys(): run_parameter= Run_Parameter.objects.get(pk=int(id)) run.run_options.append({'root':run_parameter.root,'name':run_parameter.name,'value':parameters[id]}) - - # Celery task + + # Celery task async_result = build_model.apply_async( kwargs={ "inputs_path": inputs_path, @@ -300,7 +305,7 @@ def optimize(request): r.batch_job.status = batch_task_status.FAILED r.batch_job.save() r.save() - + if not all_complete: payload = { "status": "BLOCKED", @@ -344,7 +349,7 @@ def optimize(request): else: logger.info("Found a subsequent gradient model for year %s but it was not built.",next_run.year) break - + # Unknown environment, not supported else: raise Exception("Failed to submit job, unknown compute environment") diff --git a/calliope_app/client/static/js/add_run.js b/calliope_app/client/static/js/add_run.js index 10998443..dd42fb63 100644 --- a/calliope_app/client/static/js/add_run.js +++ b/calliope_app/client/static/js/add_run.js @@ -1,209 +1,233 @@ -$(document).ready(function () { - - add_run_precheck(); - - $('#master-cancel').removeClass('hide'); - $('#master-save').removeClass('hide'); - - $('#master-cancel').on('click', function () { - var model_uuid = $('#header').data('model_uuid'); - window.location = '/' + model_uuid + '/runs/'; - }); - - $('#master-save').on('click', function () { - var model_uuid = $('#header').data('model_uuid'), - scenario_id = $("#scenario").data('scenario_id'), - start_date = $('#start_date').val(), - end_date = $('#end_date').val(), - cluster = $('#cluster').is(":checked"), - manual = $('#manual').is(":checked"), - timestep = $('#timestep').val(), - sd = new Date(start_date), - ed = new Date(end_date), - run_env = $('#run-environment option:selected').text(), - years = $('#years').val(), - notes = $('#notes').val(); - - var parameters = {}; - $('#run_parameters .parameter-row').each(function() { - var paramId = $(this).data('param-id'); - var value = $(this).find('.run-parameter-value').val(); - parameters[paramId] = value; - }); - console.log(parameters); - - - // fix timezone issues - sd = new Date(sd.getTime() + sd.getTimezoneOffset() * 60000); - ed = new Date(ed.getTime() + ed.getTimezoneOffset() * 60000); - - var validated = true; - if (scenario_id == undefined) { - alert('Must choose a Scenario') - validated = false; - } else if (!(sd & ed)) { - alert('Must select a date range below.'); - validated = false; - } else if (sd > ed) { - alert('Start date can not be later then the end date.'); - validated = false; - } else if (sd.getFullYear() != ed.getFullYear()) { - alert('Start date and end date must occur within the same year') - validated = false; - }; - - if (validated) { - $('#master-save').prop('disabled', true); - - $.ajax({ - url: '/' + LANGUAGE_CODE + '/api/build/', - contentType: 'application/json', // Specify that you're sending JSON - data: { - 'model_uuid': model_uuid, - 'scenario_id': scenario_id, - 'start_date': start_date, - 'end_date': end_date, - 'cluster': cluster, - 'manual': manual, - 'timestep': timestep, - 'run_env': run_env, - 'years': years, - 'notes': notes, - 'parameters': JSON.stringify(parameters) - }, - dataType: 'json', - success: function (data) { - if (data['status'] == 'Success') { - window.location = '/' + model_uuid + '/runs/'; - } else { - $('#build-error').html(data['message']); - $('#master-save').prop('disabled', false); - }; - } - }); - }; - }); - - // Automatically deactivate clustering if manual is enabled. - $('#manual').on('click', function () { - if ($('#manual').is(":checked")) { - $('#cluster').prop('checked', false); - } - }); - -}); - - -function add_run_precheck() { - var model_uuid = $('#header').data('model_uuid'), - scenario_id = $("#scenario").data('scenario_id'); - $.ajax({ - url: '/' + LANGUAGE_CODE + '/component/add_run_precheck/', - data: { - 'model_uuid': model_uuid, - 'scenario_id': scenario_id, - }, - dataType: 'json', - success: function (data) { - $('#add_run_precheck').html(data['html']); - render_gantt(); - activate_tiles(); - } - }); -}; - -function activate_tiles() { - $('.selection_tile').on('click', function () { - var start_date = $(this).data('start_date'), - end_date = $(this).data('end_date'); - $('.selection_tile').removeClass('btn-outline-primary') - $(this).addClass('btn-outline-primary') - $('#start_date').val(start_date); - $('#end_date').val(end_date); - }) -} - -function render_gantt() { - - var data = $('#timeseries_gantt').data('timeseries'); - - var margin = { top: 40, right: 40, bottom: 20, left: 40 }, - width = $('#timeseries_gantt').width() - margin.left - margin.right, - bar_height = 16 - height = (bar_height + 4) * data.length; - - // Prep data - var parseDate = d3.timeParse("%m/%d/%Y, %H:%M:%S"); - data.forEach(function (d) { - d.node = d[0] - d.parameter = d[1] - d.start_date = parseDate(d[2]); - d.end_date = parseDate(d[3]); - }); - - // X Axis - var start_date = d3.min(data, function (d) { return d.start_date }), - end_date = d3.max(data, function (d) { return d.end_date }); - var x = d3.scaleTime() - .domain([start_date, end_date]) - .range([0, width]); - var xAxis = d3.axisTop() - .scale(x); - - // Y Axis - var y = d3.scaleLinear() - .domain([data.length, 0]) - .range([height, 0]); - - - // Draw - var svg = d3.select("#timeseries_gantt").append("svg") - .attr("width", width + margin.left + margin.right) - .attr("height", height + margin.top + margin.bottom) - .append("g") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); - - // Define the div for the tooltip - var tooltip = d3.select("body").append("div") - .attr("class", "tooltip") - .style("background-color", "white") - .style("border", "solid 3px black") - .style("padding", "5px") - .style("opacity", 0); - - svg.append("g") - .attr("class", "x axis") - .style("font-size", "1.2em") - .call(xAxis) - var g = svg.selectAll() - .data(data).enter().append("g"); - - g.append("rect") - .attr("height", bar_height) - .attr("width", function (d) { return x(end_date) - x(start_date); }) - .attr("x", function (d) { return x(start_date); }) - .attr("y", function (d, i) { return y(i) + (bar_height / 2); }) - .style("fill", "red") - .style("opacity", "0.2"); - - g.append("rect") - .attr("height", bar_height) - .attr("width", function (d) { return x(d.end_date) - x(d.start_date); }) - .attr("x", function (d) { return x(d.start_date); }) - .attr("y", function (d, i) { return y(i) + (bar_height / 2); }) - .style("fill", "green") - .on("mouseover", function (d) { - tooltip.transition() - .duration(200) - .style("opacity", 1); - tooltip.html("" + d.node + "
    " + d.parameter) - .style("left", (d3.event.pageX - 100) + "px") - .style("top", (d3.event.pageY - 50) + "px"); - }) - .on("mouseout", function (d) { - tooltip.transition() - .duration(500) - .style("opacity", 0); - }); - -}; +$(document).ready(function () { + + add_run_precheck(); + + $('#master-cancel').removeClass('hide'); + $('#master-save').removeClass('hide'); + + $('#master-cancel').on('click', function () { + var model_uuid = $('#header').data('model_uuid'); + window.location = '/' + model_uuid + '/runs/'; + }); + + $('#master-save').on('click', function () { + var model_uuid = $('#header').data('model_uuid'), + scenario_id = $("#scenario").data('scenario_id'), + start_date = $('#start_date').val(), + end_date = $('#end_date').val(), + cluster = $('#cluster').is(":checked"), + manual = $('#manual').is(":checked"), + timestep = $('#timestep').val(), + sd = new Date(start_date), + ed = new Date(end_date), + run_env = $('#run-environment option:selected').text(), + years = $('#years').val(), + notes = $('#notes').val(); + + var parameters = {}; + $('#run_parameters .parameter-row').each(function() { + var paramId = $(this).data('param-id'); + var value = $(this).find('.run-parameter-value').val(); + parameters[paramId] = value; + }); + + // fix timezone issues + sd = new Date(sd.getTime() + sd.getTimezoneOffset() * 60000); + ed = new Date(ed.getTime() + ed.getTimezoneOffset() * 60000); + + var validated = true; + if (scenario_id == undefined) { + alert('Must choose a Scenario') + validated = false; + } else if (!(sd & ed)) { + alert('Must select a date range below.'); + validated = false; + } else if (sd > ed) { + alert('Start date can not be later then the end date.'); + validated = false; + } else if (sd.getFullYear() != ed.getFullYear()) { + alert('Start date and end date must occur within the same year') + validated = false; + }; + + if (validated) { + $('#master-save').prop('disabled', true); + + $.ajax({ + url: '/' + LANGUAGE_CODE + '/api/build/', + contentType: 'application/json', // Specify that you're sending JSON + data: { + 'model_uuid': model_uuid, + 'scenario_id': scenario_id, + 'start_date': start_date, + 'end_date': end_date, + 'cluster': cluster, + 'manual': manual, + 'timestep': timestep, + 'run_env': run_env, + 'years': years, + 'notes': notes, + 'parameters': JSON.stringify(parameters) + }, + dataType: 'json', + success: function (data) { + if (data['status'] == 'Success') { + window.location = '/' + model_uuid + '/runs/'; + } else { + $('#build-error').html(data['message']); + $('#master-save').prop('disabled', false); + }; + } + }); + }; + }); + + // Automatically deactivate clustering if manual is enabled. + $('#manual').on('click', function () { + if ($('#manual').is(":checked")) { + $('#cluster').prop('checked', false); + } + }); + + var env_name = $(this).val(); + set_solvers(env_name); + + $("#run-environment").change(function () { + var env_name = $(this).val(); + set_solvers(env_name); + }); + +}); + + +function set_solvers(env_name) { + $.ajax({ + url: '/' + LANGUAGE_CODE + '/api/solvers/', + data: { + 'env_name': env_name, + }, + success: function(data) { + var solvers = $('#run-solvers'); + solvers.empty(); + $.each(data, function(key, value) { + solvers.append($('').attr('value', key).text(value)); + }); + } + }); +} + + +function add_run_precheck() { + var model_uuid = $('#header').data('model_uuid'), + scenario_id = $("#scenario").data('scenario_id'); + $.ajax({ + url: '/' + LANGUAGE_CODE + '/component/add_run_precheck/', + data: { + 'model_uuid': model_uuid, + 'scenario_id': scenario_id, + }, + dataType: 'json', + success: function (data) { + $('#add_run_precheck').html(data['html']); + render_gantt(); + activate_tiles(); + } + }); +}; + + +function activate_tiles() { + $('.selection_tile').on('click', function () { + var start_date = $(this).data('start_date'), + end_date = $(this).data('end_date'); + $('.selection_tile').removeClass('btn-outline-primary') + $(this).addClass('btn-outline-primary') + $('#start_date').val(start_date); + $('#end_date').val(end_date); + }) +} + +function render_gantt() { + + var data = $('#timeseries_gantt').data('timeseries'); + + var margin = { top: 40, right: 40, bottom: 20, left: 40 }, + width = $('#timeseries_gantt').width() - margin.left - margin.right, + bar_height = 16 + height = (bar_height + 4) * data.length; + + // Prep data + var parseDate = d3.timeParse("%m/%d/%Y, %H:%M:%S"); + data.forEach(function (d) { + d.node = d[0] + d.parameter = d[1] + d.start_date = parseDate(d[2]); + d.end_date = parseDate(d[3]); + }); + + // X Axis + var start_date = d3.min(data, function (d) { return d.start_date }), + end_date = d3.max(data, function (d) { return d.end_date }); + var x = d3.scaleTime() + .domain([start_date, end_date]) + .range([0, width]); + var xAxis = d3.axisTop() + .scale(x); + + // Y Axis + var y = d3.scaleLinear() + .domain([data.length, 0]) + .range([height, 0]); + + + // Draw + var svg = d3.select("#timeseries_gantt").append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + // Define the div for the tooltip + var tooltip = d3.select("body").append("div") + .attr("class", "tooltip") + .style("background-color", "white") + .style("border", "solid 3px black") + .style("padding", "5px") + .style("opacity", 0); + + svg.append("g") + .attr("class", "x axis") + .style("font-size", "1.2em") + .call(xAxis) + var g = svg.selectAll() + .data(data).enter().append("g"); + + g.append("rect") + .attr("height", bar_height) + .attr("width", function (d) { return x(end_date) - x(start_date); }) + .attr("x", function (d) { return x(start_date); }) + .attr("y", function (d, i) { return y(i) + (bar_height / 2); }) + .style("fill", "red") + .style("opacity", "0.2"); + + g.append("rect") + .attr("height", bar_height) + .attr("width", function (d) { return x(d.end_date) - x(d.start_date); }) + .attr("x", function (d) { return x(d.start_date); }) + .attr("y", function (d, i) { return y(i) + (bar_height / 2); }) + .style("fill", "green") + .on("mouseover", function (d) { + tooltip.transition() + .duration(200) + .style("opacity", 1); + tooltip.html("" + d.node + "
    " + d.parameter) + .style("left", (d3.event.pageX - 100) + "px") + .style("top", (d3.event.pageY - 50) + "px"); + }) + .on("mouseout", function (d) { + tooltip.transition() + .duration(500) + .style("opacity", 0); + }); + +}; diff --git a/calliope_app/client/templates/add_run.html b/calliope_app/client/templates/add_run.html index 9433d66f..5c5975e4 100644 --- a/calliope_app/client/templates/add_run.html +++ b/calliope_app/client/templates/add_run.html @@ -18,9 +18,9 @@ background-color: #192733; border-radius: 10px; padding: 10px; - left: 0%; + left: 0%; margin-left: 10px; /* Offset it by 10 pixels to the right */ - transform: translateY(10px); + transform: translateY(10px); } .hover-text:hover .tooltip-text { @@ -45,7 +45,7 @@ {% endblock %} {% block config_runs %}
      {% trans "Runs" %}
    {% endblock %} -{% block config_5_sync %}{% endblock %} +{% block config_5_sync %}{% endblock %} {% block config_5_tab %}tab-active{% endblock %} {% block content %} @@ -58,7 +58,7 @@
    {% trans "Select" %} {% trans "a time period" %} {% trans "below" %}

    - +
    {% trans "Start Date" %}    @@ -87,22 +87,22 @@
    {% trans "Gradient Years" %}
    {% trans "Run Options" %}:
    -
    +
    {% trans "Enable Clustered Run:" %}
    -
    +
    {% trans "Enable Manual Run:" %}
    -
    +
    {% trans "Run Timestep" %}
    - +
    - -
    {% trans "Select Run Environment:" %}
    + +
    {% trans "Run Environment" %}
    -
    - {% trans "Run Notes" %}
    - -
    -
    + {% for param in parameters %} + {% if param.run_parameter.name == 'solver' %} +
    +
    +
    {% trans "Run Solver" %}
    +
    +
    + +
    +
    + {% else %}
    @@ -127,7 +134,7 @@
    {% if param.run_parameter.choices %} - {% for choice in param.run_parameter.choices %} {% endfor %} @@ -137,8 +144,15 @@
    + {% trans "Run Notes" %}
    + +
    +

    diff --git a/calliope_app/client/widgets.py b/calliope_app/client/widgets.py new file mode 100644 index 00000000..8111b8d7 --- /dev/null +++ b/calliope_app/client/widgets.py @@ -0,0 +1,44 @@ +import json + +from django import forms +from django.utils.safestring import mark_safe + +class JSONEditorWidget(forms.Textarea): + class Media: + css = { + 'all': ('https://cdn.jsdelivr.net/npm/jsoneditor@9.9.0/dist/jsoneditor.min.css',) + } + js = ('https://cdn.jsdelivr.net/npm/jsoneditor@9.9.0/dist/jsoneditor.min.js',) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def render(self, name, value, attrs=None, renderer=None): + editor_id = attrs.get('id', name) + editor_script = f""" + +
    + + + """ + return mark_safe(editor_script) diff --git a/calliope_app/compose/Dockerfile b/calliope_app/compose/Dockerfile index b99c1482..00f02f50 100644 --- a/calliope_app/compose/Dockerfile +++ b/calliope_app/compose/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-slim-bookworm as app +FROM python:3.8-slim-bookworm AS app # set environment variables ENV LC_ALL=C.UTF-8 From d4a33802b6ac77d8365b3ec70f0cedc95b50e448 Mon Sep 17 00:00:00 2001 From: jgu2 Date: Tue, 17 Sep 2024 17:31:18 -0600 Subject: [PATCH 25/30] Manage sover access via Django admin --- calliope_app/api/models/engage.py | 13 +++++++++++- calliope_app/api/views/outputs.py | 25 ++++++++++++++++++++++-- calliope_app/client/static/js/add_run.js | 4 +++- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/calliope_app/api/models/engage.py b/calliope_app/api/models/engage.py index dc1bd103..49674176 100644 --- a/calliope_app/api/models/engage.py +++ b/calliope_app/api/models/engage.py @@ -103,7 +103,18 @@ def activate(cls, activation_uuid): def default_solvers(): - return {"appsi_highs": "HiGHS", "cbc": "CBC"} + return [ + { + "name": "appsi_highs", + "pretty_name": "HiGHS", + "order": 1 + }, + { + "name": "cbc", + "pretty_name": "CBC", + "order": 2 + } + ] class ComputeEnvironment(models.Model): diff --git a/calliope_app/api/views/outputs.py b/calliope_app/api/views/outputs.py index 522e158c..72eb2166 100644 --- a/calliope_app/api/views/outputs.py +++ b/calliope_app/api/views/outputs.py @@ -43,8 +43,29 @@ def solvers(request): env_name = request.GET.get("env_name", None) if not env_name: env_name = "default" - env = ComputeEnvironment.objects.get(name=env_name) - payload = env.solvers + + flag = True + try: + env = ComputeEnvironment.objects.get(name=env_name) + except Cambium.Environment.DoesNotExist: + flag = False + + if (not flag) or (not env.solvers) or (not isinstance(env.solvers, list)): + payload = [ + { + "name": "appsi_highs", + "pretty_name": "HiGHS", + "order": 1 + }, + { + "name": "cbc", + "pretty_name": "CBC", + "order": 2 + } + ] + else: + payload = sorted(env.solvers, key=lambda x: x["order"]) + return HttpResponse(json.dumps(payload), content_type="application/json") diff --git a/calliope_app/client/static/js/add_run.js b/calliope_app/client/static/js/add_run.js index dd42fb63..cce1ff72 100644 --- a/calliope_app/client/static/js/add_run.js +++ b/calliope_app/client/static/js/add_run.js @@ -109,7 +109,9 @@ function set_solvers(env_name) { success: function(data) { var solvers = $('#run-solvers'); solvers.empty(); - $.each(data, function(key, value) { + $.each(data, function(index, item) { + var key = item.name; + var value = item.pretty_name; solvers.append($('').attr('value', key).text(value)); }); } From 2b2f934e7e455e3da8fa55f0d406cb8633e1072a Mon Sep 17 00:00:00 2001 From: jgu2 Date: Tue, 17 Sep 2024 18:01:01 -0600 Subject: [PATCH 26/30] Update default solver management --- calliope_app/api/engage.py | 22 ++++++++++++++++++++++ calliope_app/api/models/engage.py | 15 +++------------ calliope_app/api/views/outputs.py | 26 ++++++++++++-------------- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/calliope_app/api/engage.py b/calliope_app/api/engage.py index 086d8ed9..3ee87c8d 100644 --- a/calliope_app/api/engage.py +++ b/calliope_app/api/engage.py @@ -6,6 +6,28 @@ from django.core.mail.message import sanitize_address +ENGAGE_SOLVERS = [ + { + "name": "appsi_highs", + "pretty_name": "HiGHS", + "order": 1, + "is_active": True + }, + { + "name": "cbc", + "pretty_name": "CBC", + "order": 2, + "is_active": True + }, + { + "name": "amplxpress", + "pretty_name": "Xpress", + "order": 3, + "is_active": False + } +] + + def aws_ses_configured(): """ Check the configuration of AWS SES settings diff --git a/calliope_app/api/models/engage.py b/calliope_app/api/models/engage.py index 49674176..822998a0 100644 --- a/calliope_app/api/models/engage.py +++ b/calliope_app/api/models/engage.py @@ -9,6 +9,8 @@ from django.urls import reverse from django.utils.html import mark_safe +from api.engage import ENGAGE_SOLVERS + logger = logging.getLogger(__name__) @@ -103,18 +105,7 @@ def activate(cls, activation_uuid): def default_solvers(): - return [ - { - "name": "appsi_highs", - "pretty_name": "HiGHS", - "order": 1 - }, - { - "name": "cbc", - "pretty_name": "CBC", - "order": 2 - } - ] + return ENGAGE_SOLVERS class ComputeEnvironment(models.Model): diff --git a/calliope_app/api/views/outputs.py b/calliope_app/api/views/outputs.py index 72eb2166..5140d102 100644 --- a/calliope_app/api/views/outputs.py +++ b/calliope_app/api/views/outputs.py @@ -31,6 +31,7 @@ ) from api.models.engage import ComputeEnvironment +from api.engage import ENGAGE_SOLVERS from api.utils import zip_folder, initialize_units, convert_units, noconv_units from batch.managers import AWSBatchJobManager from taskmeta.models import CeleryTask, BatchTask, batch_task_status @@ -47,24 +48,21 @@ def solvers(request): flag = True try: env = ComputeEnvironment.objects.get(name=env_name) - except Cambium.Environment.DoesNotExist: + except ComputeEnvironment.DoesNotExist: flag = False if (not flag) or (not env.solvers) or (not isinstance(env.solvers, list)): - payload = [ - { - "name": "appsi_highs", - "pretty_name": "HiGHS", - "order": 1 - }, - { - "name": "cbc", - "pretty_name": "CBC", - "order": 2 - } - ] + solvers = ENGAGE_SOLVERS else: - payload = sorted(env.solvers, key=lambda x: x["order"]) + solvers = env.solvers + + candidates = [] + for solver in solvers: + is_active = solver.get("is_active", "false") + if is_active == "true": + candidates.append(solver) + + payload = sorted(candidates, key=lambda x: x["order"]) return HttpResponse(json.dumps(payload), content_type="application/json") From 489e63a37fcabe90f7018296c668e1915b57a15d Mon Sep 17 00:00:00 2001 From: jgu2 Date: Tue, 17 Sep 2024 18:02:48 -0600 Subject: [PATCH 27/30] Adjust ace editor height --- calliope_app/client/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calliope_app/client/widgets.py b/calliope_app/client/widgets.py index 8111b8d7..962d4df6 100644 --- a/calliope_app/client/widgets.py +++ b/calliope_app/client/widgets.py @@ -21,7 +21,7 @@ def render(self, name, value, attrs=None, renderer=None): background: #79aec8 !important; }} -
    +