Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simple Django based domain selection view #700

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docker/nwm_gui/app_server/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docker/nwm_gui/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions python/gui/MaaS/cbv/DMODProxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
"""
Expand Down Expand Up @@ -251,15 +252,15 @@ 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

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
Expand Down
40 changes: 21 additions & 19 deletions python/gui/MaaS/cbv/EditView.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
133 changes: 118 additions & 15 deletions python/gui/MaaS/cbv/MapView.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be wise to abstract out this logic into another function that would be somewhat "easy" to swap in and out. Won't the fabrics come via the data store in the future?


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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool("false") == True

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a helper function in dmod.core.common that can help identify truthy values from a string. I believe it's called is_true.


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:
Expand Down Expand Up @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A list of errors and stuff wouldn't get passed in here - it would be path variables. If the path to the domain view was something along the lines of /domain/(?P<one>...)/(?P<two>...)/(?P<three>...), I believe the key value pairs for one, two, and three would end up in kwargs. Things like list of errors, warnings, and info would come in through request.GET.

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__)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean for this debug statement to stay in here?


# 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)
8 changes: 4 additions & 4 deletions python/gui/MaaS/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@

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

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).
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions python/gui/MaaS/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', "[email protected]")
SU_PASSWORD = os.environ.get('DMOD_SU_PASSWORD', f"{SU_NAME}{os.environ.get('SQL_PASSWORD')}")

superuser = User.objects.create_superuser(
username=SU_NAME,
Expand Down
Loading
Loading