diff --git a/docker/nwm_gui/app_server/entrypoint.sh b/docker/nwm_gui/app_server/entrypoint.sh
index a2d8a531c..7a67b3cb5 100755
--- a/docker/nwm_gui/app_server/entrypoint.sh
+++ b/docker/nwm_gui/app_server/entrypoint.sh
@@ -28,13 +28,13 @@ echo "Starting dmod app"
#python manage.py migrate
#########
-# Execute the migration scripts on the designated database
-#python manage.py migrate
-
#Extract the DB secrets into correct ENV variables
POSTGRES_SECRET_FILE="/run/secrets/${DOCKER_SECRET_POSTGRES_PASS:?}"
export SQL_PASSWORD="$(cat ${POSTGRES_SECRET_FILE})"
+# Execute the migration scripts on the designated database
+python manage.py migrate
+
# Handle for debugging when appropriate
if [ "$(echo "${PYCHARM_REMOTE_DEBUG_ACTIVE:-false}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" == "true" ]; then
# Set the timeout to longer when debugging
diff --git a/docker/nwm_gui/docker-compose.yml b/docker/nwm_gui/docker-compose.yml
index d2455a6e9..9dfe329d5 100644
--- a/docker/nwm_gui/docker-compose.yml
+++ b/docker/nwm_gui/docker-compose.yml
@@ -63,6 +63,9 @@ services:
- SQL_PORT=5432
- DATABASE=postgres
- DOCKER_SECRET_POSTGRES_PASS=postgres_password
+ - DEBUG=${DOCKER_GUI_DEV_MODE:-true}
+ # Should be a comma-delimited string if needing more than one
+ - TRUSTED_ORIGINS=${DOCKER_GUI_TRUSTED_ORIGINS:-http://127.0.0.1:${DOCKER_GUI_WEB_SERVER_HOST_PORT:-8081}}
volumes:
- ${DMOD_APP_STATIC:?}:/usr/maas_portal/static
- ${DMOD_SSL_DIR}/request-service:/usr/maas_portal/ssl
diff --git a/python/gui/MaaS/cbv/DMODProxy.py b/python/gui/MaaS/cbv/DMODProxy.py
index fc0fcb7a1..28a7551c8 100644
--- a/python/gui/MaaS/cbv/DMODProxy.py
+++ b/python/gui/MaaS/cbv/DMODProxy.py
@@ -11,8 +11,9 @@
import logging
logger = logging.getLogger("gui_log")
-from dmod.communication import Distribution, get_available_models, get_available_outputs, get_request, get_parameters, \
- NWMRequestJsonValidator, NWMRequest, ExternalRequest, ExternalRequestResponse, ModelExecRequestClient, Scalar, MessageEventType
+from dmod.communication import (Distribution, get_available_models, get_available_outputs, get_request, get_parameters,
+ NWMRequestJsonValidator, NWMRequest, ExternalRequest, ExternalRequestClient,
+ ExternalRequestResponse, Scalar, MessageEventType)
from pathlib import Path
from typing import List, Optional, Tuple, Type
@@ -200,7 +201,7 @@ def maas_request(self) -> ExternalRequest:
return self._maas_request
-class PostFormRequestClient(ModelExecRequestClient):
+class PostFormRequestClient(ExternalRequestClient):
"""
A client for websocket interaction with the MaaS request handler as initiated by a POST form HTTP request.
"""
@@ -251,7 +252,7 @@ def _acquire_session_info(self, use_current_values: bool = True, force_new: bool
return self._session_id and self._session_secret and self._session_created
else:
logger.info("Session from {}: force_new={}".format(self.__class__.__name__, force_new))
- tmp = self._acquire_new_session()
+ tmp = self._auth_client._acquire_session()
logger.info("Session Info Return: {}".format(tmp))
return tmp
@@ -259,7 +260,7 @@ def _init_maas_job_request(self):
pass
def generate_request(self, form_proc_class: Type[RequestFormProcessor]) -> ExternalRequest:
- self.form_proc = form_proc_class(post_request=self.http_request, maas_secret=self.session_secret)
+ self.form_proc = form_proc_class(post_request=self.http_request, maas_secret=self._auth_client._session_secret)
return self.form_proc.maas_request
@property
diff --git a/python/gui/MaaS/cbv/EditView.py b/python/gui/MaaS/cbv/EditView.py
index 780c7e8e9..0bab07b61 100644
--- a/python/gui/MaaS/cbv/EditView.py
+++ b/python/gui/MaaS/cbv/EditView.py
@@ -42,30 +42,32 @@ def humanize(words: str) -> str:
models = list(communication.get_available_models().keys())
domains = ['example-domain-A', 'example-domain-B'] #FIXME map this from supported domains
- outputs = list()
- distribution_types = list()
-
- # Create a mapping between each output type and a friendly representation of it
- for output in maas_request.get_available_outputs():
- output_definition = dict()
- output_definition['name'] = humanize(output)
- output_definition['value'] = output
- outputs.append(output_definition)
-
- # Create a mapping between each distribution type and a friendly representation of it
- for distribution_type in maas_request.get_distribution_types():
- type_definition = dict()
- type_definition['name'] = humanize(distribution_type)
- type_definition['value'] = distribution_type
- distribution_types.append(type_definition)
+ #outputs = list()
+ #distribution_types = list()
+
+ ### Note that these are now broken, and also probably no longer applicable
+ #
+ # # Create a mapping between each output type and a friendly representation of it
+ # for output in maas_request.get_available_outputs():
+ # output_definition = dict()
+ # output_definition['name'] = humanize(output)
+ # output_definition['value'] = output
+ # outputs.append(output_definition)
+ #
+ # # Create a mapping between each distribution type and a friendly representation of it
+ # for distribution_type in maas_request.get_distribution_types():
+ # type_definition = dict()
+ # type_definition['name'] = humanize(distribution_type)
+ # type_definition['value'] = distribution_type
+ # distribution_types.append(type_definition)
# Package everything up to be rendered for the client
payload = {
'models': models,
'domains': domains,
- 'outputs': outputs,
- 'parameters': maas_request.get_parameters(),
- 'distribution_types': distribution_types,
+ #'outputs': outputs,
+ #'parameters': maas_request.get_parameters(),
+ #'distribution_types': distribution_types,
'errors': errors,
'info': info,
'warnings': warnings
diff --git a/python/gui/MaaS/cbv/MapView.py b/python/gui/MaaS/cbv/MapView.py
index 40e62b847..e2db3a101 100644
--- a/python/gui/MaaS/cbv/MapView.py
+++ b/python/gui/MaaS/cbv/MapView.py
@@ -17,12 +17,14 @@
from pathlib import Path
from .. import datapane
from .. import configuration
+from dmod.modeldata.hydrofabric import GeoPackageHydrofabric
import logging
logger = logging.getLogger("gui_log")
_resolution_regex = re.compile("(.+) \((.+)\)")
+
def _build_fabric_path(fabric, type):
"""
build a qualified path from the hydrofabric name and type
@@ -34,52 +36,100 @@ def _build_fabric_path(fabric, type):
resolution = resolution_match.group(2)
else:
name = fabric
- resolution=''
+ resolution = ''
+
+ hyfab_data_dir = Path(PROJECT_ROOT, 'static', 'ngen', 'hydrofabric', name, resolution)
+
+ geojson_file = hyfab_data_dir.joinpath(f"{type}_data.geojson")
+ if geojson_file.exists():
+ return geojson_file
+
+ if hyfab_data_dir.joinpath("hydrofabric.gpkg").exists():
+ geopackage_file = hyfab_data_dir.joinpath("hydrofabric.gpkg")
+ elif hyfab_data_dir.joinpath(f"{name}.gpkg").exists():
+ geopackage_file = hyfab_data_dir.joinpath(f"{name}.gpkg")
+ else:
+ logger.error(f"Can't build fabric path: can't find hydrofabric data file in directory {hyfab_data_dir!s}")
+ return None
+
+ return geopackage_file
- path = Path(PROJECT_ROOT, 'static', 'ngen', 'hydrofabric', name, resolution, type+'_data.geojson')
- return path
class Fabrics(APIView):
def get(self, request: HttpRequest, fabric: str = None) -> typing.Optional[JsonResponse]:
if fabric is None:
- fabric = 'example'
+ fabric = 'example_fabric_name'
type = request.GET.get('fabric_type', 'catchment')
if not type:
- type="catchment"
+ type = "catchment"
+
+ id_only = request.GET.get("id_only", "false")
+ if isinstance(id_only, str):
+ id_only = id_only.strip().lower() == "true"
+ else:
+ id_only = bool(id_only)
path = _build_fabric_path(fabric, type)
if path is None:
return None
+ elif path.name == f"{type}_data.geojson":
+ with open(path) as fp:
+ data = json.load(fp)
+ if id_only:
+ return JsonResponse(sorted([feature["id"] for feature in data["features"]]), safe=False)
+ else:
+ return JsonResponse(data)
+ elif path.name[-5:] == ".gpkg":
+ hf = GeoPackageHydrofabric.from_file(geopackage_file=path)
+ if id_only:
+ if type == "catchment":
+ return JsonResponse(sorted(hf.get_all_catchment_ids()), safe=False)
+ elif type == "nexus":
+ return JsonResponse(sorted(hf.get_all_nexus_ids()), safe=False)
+ else:
+ logger.error(f"Unsupported fabric type '{type}' for id_only geopackage in Fabrics API view")
+ return None
+ else:
+ if type == "catchment":
+ df = hf._dataframes[hf._DIVIDES_LAYER_NAME]
+ elif type == "nexus":
+ df = hf._dataframes[hf._NEXUS_LAYER_NAME]
+ else:
+ logger.error(f"Unsupported fabric type '{type}' for geopackage in Fabrics API view")
+ return None
+ return JsonResponse(json.loads(df.to_json()))
+ else:
+ logger.error(f"Can't make API request for hydrofabric '{fabric!s}'")
+ return None
- with open(path) as fp:
- data = json.load(fp)
- return JsonResponse(data)
class FabricNames(APIView):
- _fabric_dir = Path(PROJECT_ROOT, 'static', 'ngen', 'hydrofabric')
+ _fabrics_root_dir = Path(PROJECT_ROOT, 'static', 'ngen', 'hydrofabric')
def get(self, request: HttpRequest) -> JsonResponse:
names = []
- for f_name in self._fabric_dir.iterdir():
- if f_name.is_dir():
+ for fabric_subdir in self._fabrics_root_dir.iterdir():
+ if fabric_subdir.is_dir():
#Check for sub dirs/resolution
sub = False
- for r_name in f_name.iterdir():
+ for r_name in fabric_subdir.iterdir():
if r_name.is_dir():
- names.append( '{} ({})'.format(f_name.name, r_name.name))
+ names.append(f'{fabric_subdir.name} ({r_name.name})')
sub = True
if not sub:
- names.append( '{}'.format(f_name.name) )
+ names.append(f'{fabric_subdir.name}')
return JsonResponse(data={
"fabric_names": names
})
+
class FabricTypes(APIView):
def get(self, rquest: HttpRequest) -> JsonResponse:
return JsonResponse( data={
"fabric_types": ['catchment', 'flowpath', 'nexus']
- })
+ })
+
class ConnectedFeatures(APIView):
def get(self, request: HttpRequest) -> JsonResponse:
@@ -142,3 +192,56 @@ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Return the rendered page
return render(request, 'maas/map.html', payload)
+
+
+class DomainView(View):
+
+ """
+ A view used to render the map
+ """
+ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
+ """
+ The handler for 'get' requests. This will render the 'map.html' template
+
+ :param HttpRequest request: The request asking to render this page
+ :param args: An ordered list of arguments
+ :param kwargs: A dictionary of named arguments
+ :return: A rendered page
+ """
+ # If a list of error messages wasn't passed, create one
+ if 'errors' not in kwargs:
+ errors = list()
+ else:
+ # Otherwise continue to use the passed in list
+ errors = kwargs['errors'] # type: list
+
+ # If a list of warning messages wasn't passed create one
+ if 'warnings' not in kwargs:
+ warnings = list()
+ else:
+ # Otherwise continue to use the passed in list
+ warnings = kwargs['warnings'] # type: list
+
+ # If a list of basic messages wasn't passed, create one
+ if 'info' not in kwargs:
+ info = list()
+ else:
+ # Otherwise continue to us the passed in list
+ info = kwargs['info'] # type: list
+
+ framework_selector = datapane.Input("framework", "select", "The framework within which to run models")
+ for editor in configuration.get_editors():
+ framework_selector.add_choice(editor['name'], editor['description'], editor['friendly_name'])
+
+ pprint(framework_selector.__dict__)
+
+ # Package everything up to be rendered for the client
+ payload = {
+ 'errors': errors,
+ 'info': info,
+ 'warnings': warnings,
+ 'pane_inputs': [framework_selector]
+ }
+
+ # Return the rendered page
+ return render(request, 'maas/domain.html', payload)
\ No newline at end of file
diff --git a/python/gui/MaaS/client.py b/python/gui/MaaS/client.py
index f13a57208..456bd6799 100644
--- a/python/gui/MaaS/client.py
+++ b/python/gui/MaaS/client.py
@@ -13,7 +13,7 @@
from dmod.communication import ExternalRequest
from dmod.communication import ExternalRequestResponse
-from dmod.communication import ModelExecRequestClient
+from dmod.communication import ExternalRequestClient
from . import utilities
from .processors.processor import BaseProcessor
@@ -21,7 +21,7 @@
logger = logging.getLogger("gui_log")
-class JobRequestClient(ModelExecRequestClient):
+class JobRequestClient(ExternalRequestClient):
"""
A client for websocket interaction with the MaaS request handler, specifically for performing a job request based on
details provided in a particular HTTP POST request (i.e., with form info on the parameters of the job execution).
@@ -36,7 +36,7 @@ def __init__(
if ssl_dir is None:
ssl_dir = Path(__file__).resolve().parent.parent.parent.joinpath('ssl')
ssl_dir = Path('/usr/maas_portal/ssl') #Fixme
- logger.debug("endpoing_uri: {}".format(endpoint_uri))
+ logger.debug("endpoint_uri: {}".format(endpoint_uri))
super().__init__(endpoint_uri=endpoint_uri, ssl_directory=ssl_dir)
self._processor = processor
self._cookies = None
@@ -74,7 +74,7 @@ def _acquire_session_info(self, use_current_values: bool = True, force_new: bool
return self._session_id and self._session_secret and self._session_created
else:
logger.info("Session from JobRequestClient: force_new={}".format(force_new))
- tmp = self._acquire_new_session()
+ tmp = self._auth_client._acquire_session()
logger.info("Session Info Return: {}".format(tmp))
return tmp
diff --git a/python/gui/MaaS/migrations/0001_initial.py b/python/gui/MaaS/migrations/0001_initial.py
index 26e05aaba..b6a7ab59e 100644
--- a/python/gui/MaaS/migrations/0001_initial.py
+++ b/python/gui/MaaS/migrations/0001_initial.py
@@ -10,9 +10,9 @@ class Migration(migrations.Migration):
def create_superuser(apps, schema_editor):
from django.contrib.auth.models import User
- SU_NAME = os.environ.get('DMOD_SU_NAME')
- SU_EMAIL = os.environ.get('DMOD_SU_EMAIL')
- SU_PASSWORD = os.environ.get('DMOD_SU_PASSWORD')
+ SU_NAME = os.environ.get('DMOD_SU_NAME', "dmod_admin")
+ SU_EMAIL = os.environ.get('DMOD_SU_EMAIL', "dmod_admin@noaa.gov")
+ SU_PASSWORD = os.environ.get('DMOD_SU_PASSWORD', f"{SU_NAME}{os.environ.get('SQL_PASSWORD')}")
superuser = User.objects.create_superuser(
username=SU_NAME,
diff --git a/python/gui/MaaS/static/common/js/domain.js b/python/gui/MaaS/static/common/js/domain.js
new file mode 100644
index 000000000..ad558dad9
--- /dev/null
+++ b/python/gui/MaaS/static/common/js/domain.js
@@ -0,0 +1,204 @@
+
+function titleCase(str) {
+ return str.replaceAll("_", " ").toLowerCase().split(' ').map(function(word) {
+ return word.replace(word[0], word[0].toUpperCase());
+ }).join(' ');
+}
+
+function loadFabricNames() {
+ $.ajax(
+ {
+ url: "fabric/names",
+ type: 'GET',
+ error: function(xhr,status,error) {
+ console.error(error);
+ },
+ success: function(result,status,xhr) {
+ if (result != null) {
+ result['fabric_names'].forEach(function(name) {
+ $("#fabric-selector").append("");
+ });
+ $("#fabric-selector option")[0].setAttribute("selected", "");
+ loadFabricDomain();
+ }
+ }
+ }
+ );
+}
+
+function loadFabricTypes() {
+ $.ajax(
+ {
+ url: "fabric/types",
+ type: 'GET',
+ error: function(xhr,status,error) {
+ console.error(error);
+ },
+ success: function(result,status,xhr) {
+ if (result != null) {
+ result['fabric_types'].forEach(function(name) {
+ $("#fabric-type-selector").append("");
+ });
+ $("#fabric-type-selector option")[0].setAttribute("selected", "");
+ loadFabricDomain();
+ }
+ }
+ }
+ );
+}
+
+function insertOptionInOrder(option, newParentSelect) {
+ let next_index;
+ let current_index = 0;
+ let next_size = 200;
+
+ next_index = current_index + next_size;
+ while (next_index < newParentSelect.options.length) {
+ if (parseInt(option.value) < parseInt(newParentSelect.options[next_index].value)) {
+ break;
+ }
+ else {
+ current_index = next_index;
+ next_index = current_index + next_size;
+ }
+ }
+
+ for (current_index; current_index < newParentSelect.options.length; current_index++) {
+ if (parseInt(option.value) < parseInt(newParentSelect.options[current_index].value)) {
+ newParentSelect.options.add(option, newParentSelect.options[current_index]);
+ return;
+ }
+ }
+ newParentSelect.appendChild(option);
+}
+
+function addDomainChoicesOption(values) {
+ let select = document.getElementById('domainChoices');
+ for (let optionIndex = 0; optionIndex < values.length; optionIndex++) {
+ const option = document.createElement('option');
+ option.value = values[optionIndex].substring(4);
+ option.innerHTML = values[optionIndex];
+ insertOptionInOrder(option, select);
+ }
+}
+
+function controlSelectAdd() {
+ let choices = document.getElementById('domainChoices');
+ let selected = document.getElementById('domainSelections');
+
+ for (let optionIndex = choices.options.length - 1; optionIndex >=0; optionIndex--) {
+ let opt = choices.options[optionIndex];
+ if (opt.selected) {
+ opt.selected = false;
+ choices.removeChild(opt);
+ insertOptionInOrder(opt, selected);
+ }
+ }
+}
+
+function controlSelectRemove() {
+ let choices = document.getElementById('domainChoices'),
+ selected = document.getElementById('domainSelections'),
+ opt;
+
+ for (let optionIndex = selected.options.length - 1; optionIndex >=0; optionIndex--) {
+ opt = selected.options[optionIndex];
+ if (opt.selected) {
+ opt.selected = false;
+ selected.removeChild(opt);
+ insertOptionInOrder(opt, choices);
+ }
+ }
+}
+
+function controlSelectAll() {
+ let choices = document.getElementById('domainChoices');
+ let selected = document.getElementById('domainSelections');
+ let opt;
+
+ for (let optionIndex = choices.options.length - 1; optionIndex >= 0 ; optionIndex--) {
+ opt = choices.options[optionIndex];
+ if (opt.selected) {
+ opt.selected = false;
+ }
+ choices.removeChild(opt);
+ insertOptionInOrder(opt, selected);
+ }
+}
+
+function controlSelectClear() {
+ let choices = document.getElementById('domainChoices');
+ let selected = document.getElementById('domainSelections');
+ let opt;
+
+ for (let optionIndex = selected.options.length - 1; optionIndex >= 0 ; optionIndex--) {
+ opt = selected.options[optionIndex];
+ if (opt.selected) {
+ opt.selected = false;
+ }
+ selected.removeChild(opt);
+ insertOptionInOrder(opt, choices);
+ }
+}
+
+function loadFabricDomain(event) {
+ let name = $("#fabric-selector").val();
+ let type = "catchment"; //$("#fabric-type-selector").val(),
+ let catLists = [document.getElementById('domainChoices'),
+ document.getElementById('domainSelections')];
+ let loadingOverDiv = document.getElementById('loadCatsOverlay');
+
+ catLists[0].style.display = "none";
+ loadingOverDiv.style.display = "block";
+
+ $("input[name=fabric]").val(name);
+
+ // Clear any existing