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