diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..8ed41f353 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ +## Overview + +Brief description of what this PR does, and why it is needed. + +Connects #XXX + +### Demo + +Optional. Screenshots, `curl` examples, etc. + +### Notes + +Optional. Ancillary topics, caveats, alternative strategies that didn't work out, anything else. + +## Testing Instructions + + * How to test this PR + * Prefer bulleted description + * Start after checking out this branch + * Include any setup required, such as bundling scripts, restarting services, etc. + * Include test case, and expected output diff --git a/Vagrantfile b/Vagrantfile index 4923dd2fe..e2ebfa8c2 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -90,9 +90,8 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| worker.vm.synced_folder "src/mmw", "/opt/app/" - if ENV["VAGRANT_ENV"].nil? || ENV["VAGRANT_ENV"] != "TEST" - worker.vm.synced_folder "/opt/rwd-data", "/opt/rwd-data" - end + # Path to RWD data (ex. /media/passport/rwd-nhd) + worker.vm.synced_folder ENV.fetch("RWD_DATA", "/tmp"), "/opt/rwd-data" # Docker worker.vm.network "forwarded_port", { diff --git a/deployment/ansible/roles/model-my-watershed.rwd/defaults/main.yml b/deployment/ansible/roles/model-my-watershed.rwd/defaults/main.yml index 78e38ac8e..ca67b1610 100644 --- a/deployment/ansible/roles/model-my-watershed.rwd/defaults/main.yml +++ b/deployment/ansible/roles/model-my-watershed.rwd/defaults/main.yml @@ -2,4 +2,8 @@ rwd_data_path: "/opt/rwd-data" rwd_host: "localhost" rwd_port: 5000 -rwd_docker_image: "quay.io/wikiwatershed/rwd:1.1.1" +rwd_docker_image: "quay.io/wikiwatershed/rwd:1.2.1" + +app_config: + RWD_HOST: "{{ rwd_host }}" + RWD_PORT: "{{ rwd_port }}" diff --git a/deployment/ansible/roles/model-my-watershed.rwd/tasks/app.yml b/deployment/ansible/roles/model-my-watershed.rwd/tasks/app.yml new file mode 100644 index 000000000..8bf935507 --- /dev/null +++ b/deployment/ansible/roles/model-my-watershed.rwd/tasks/app.yml @@ -0,0 +1,16 @@ +--- +- name: Create RWD data directory + file: path="{{ rwd_data_path }}" + state=directory + +- name: Pull RWD docker container image + command: /usr/bin/docker pull {{ rwd_docker_image }} + +- name: Configure RWD service definition + template: src=upstart-mmw-rwd.conf.j2 + dest=/etc/init/mmw-rwd.conf + notify: + - Restart mmw-rwd + +- name: Ensure service is running + service: name=mmw-rwd state=started diff --git a/deployment/ansible/roles/model-my-watershed.rwd/tasks/configuration.yml b/deployment/ansible/roles/model-my-watershed.rwd/tasks/configuration.yml new file mode 100644 index 000000000..c8126fe8e --- /dev/null +++ b/deployment/ansible/roles/model-my-watershed.rwd/tasks/configuration.yml @@ -0,0 +1,10 @@ +--- +- name: Configure application + copy: content="{{ item.value }}" + dest="{{ envdir_home }}/{{ item.key }}" + owner=root + group=mmw + mode=0750 + with_dict: "{{ app_config }}" + notify: + - Restart Celery diff --git a/deployment/ansible/roles/model-my-watershed.rwd/tasks/main.yml b/deployment/ansible/roles/model-my-watershed.rwd/tasks/main.yml index 8bf935507..2fe6804c3 100644 --- a/deployment/ansible/roles/model-my-watershed.rwd/tasks/main.yml +++ b/deployment/ansible/roles/model-my-watershed.rwd/tasks/main.yml @@ -1,16 +1,3 @@ --- -- name: Create RWD data directory - file: path="{{ rwd_data_path }}" - state=directory - -- name: Pull RWD docker container image - command: /usr/bin/docker pull {{ rwd_docker_image }} - -- name: Configure RWD service definition - template: src=upstart-mmw-rwd.conf.j2 - dest=/etc/init/mmw-rwd.conf - notify: - - Restart mmw-rwd - -- name: Ensure service is running - service: name=mmw-rwd state=started +- { include: configuration.yml } +- { include: app.yml } diff --git a/deployment/cfn/application.py b/deployment/cfn/application.py index f22920ee4..73ca5006e 100644 --- a/deployment/cfn/application.py +++ b/deployment/cfn/application.py @@ -173,7 +173,7 @@ def set_up_stack(self): self.app_server_auto_scaling_schedule_end_recurrence = self.add_parameter( # NOQA Parameter( 'AppServerAutoScalingScheduleEndRecurrence', Type='String', - Default='0 23 * * *', + Default='0 1 * * *', Description='Application server ASG schedule end recurrence' ), 'AppServerAutoScalingScheduleEndRecurrence') diff --git a/deployment/cfn/tiler.py b/deployment/cfn/tiler.py index 97a068f88..ce4992421 100644 --- a/deployment/cfn/tiler.py +++ b/deployment/cfn/tiler.py @@ -164,7 +164,7 @@ def set_up_stack(self): self.tile_server_auto_scaling_schedule_end_recurrence = self.add_parameter( # NOQA Parameter( 'TileServerAutoScalingScheduleEndRecurrence', Type='String', - Default='0 23 * * *', + Default='0 1 * * *', Description='Tile server ASG schedule end recurrence' ), 'TileServerAutoScalingScheduleEndRecurrence') diff --git a/deployment/cfn/utils/constants.py b/deployment/cfn/utils/constants.py index d7999a1f1..7b1f93fcd 100644 --- a/deployment/cfn/utils/constants.py +++ b/deployment/cfn/utils/constants.py @@ -2,7 +2,8 @@ 't2.micro', 't2.small', 't2.medium', - 't2.large' + 't2.large', + 'r4.large' ] RDS_INSTANCE_TYPES = [ diff --git a/deployment/cfn/worker.py b/deployment/cfn/worker.py index dff482f1f..db458bde4 100644 --- a/deployment/cfn/worker.py +++ b/deployment/cfn/worker.py @@ -170,7 +170,7 @@ def set_up_stack(self): self.worker_auto_scaling_schedule_end_recurrence = self.add_parameter( # NOQA Parameter( 'WorkerAutoScalingScheduleEndRecurrence', Type='String', - Default='0 23 * * *', + Default='0 1 * * *', Description='Worker ASG schedule end recurrence' ), 'WorkerAutoScalingScheduleEndRecurrence') @@ -331,6 +331,7 @@ def create_auto_scaling_resources(self, worker_security_group, worker_lb): worker_launch_config = self.add_resource( asg.LaunchConfiguration( worker_launch_config_name, + EbsOptimized=True, ImageId=Ref(self.worker_ami), IamInstanceProfile=Ref(self.worker_instance_profile), InstanceType=Ref(self.worker_instance_type), @@ -416,7 +417,10 @@ def get_cloud_config(self): ' - path: /etc/mmw.d/env/ROLLBAR_SERVER_SIDE_ACCESS_TOKEN\n', ' permissions: 0750\n', ' owner: root:mmw\n', - ' content: ', self.get_input('RollbarServerSideAccessToken')] # NOQA + ' content: ', self.get_input('RollbarServerSideAccessToken'), # NOQA + '\n', + 'runcmd:\n', + ' - /opt/model-my-watershed/scripts/aws/ebs-warmer.sh'] def create_cloud_watch_resources(self, worker_auto_scaling_group): self.add_resource(cw.Alarm( diff --git a/deployment/default.yml.example b/deployment/default.yml.example index a9924aca5..36364e840 100644 --- a/deployment/default.yml.example +++ b/deployment/default.yml.example @@ -29,8 +29,8 @@ AppServerAutoScalingScheduleStartCapacity: '1' # 8AM ET AppServerAutoScalingScheduleStartRecurrence: '0 13 * * 1-5' AppServerAutoScalingScheduleEndCapacity: '0' -# 6PM ET -AppServerAutoScalingScheduleEndRecurrence: '0 23 * * *' +# 8PM ET +AppServerAutoScalingScheduleEndRecurrence: '0 1 * * *' SSLCertificateARN: 'arn:aws:iam...' BackwardCompatSSLCertificateARN: 'arn:aws:iam...' TileServerInstanceType: 't2.micro' @@ -45,8 +45,8 @@ TileServerAutoScalingScheduleStartCapacity: '1' # 8AM ET TileServerAutoScalingScheduleStartRecurrence: '0 13 * * 1-5' TileServerAutoScalingScheduleEndCapacity: '0' -# 6PM ET -TileServerAutoScalingScheduleEndRecurrence: '0 23 * * *' +# 8PM ET +TileServerAutoScalingScheduleEndRecurrence: '0 1 * * *' WorkerInstanceType: 't2.micro' # Leaving this commented dynamically looks up the # most recent AMI for this type. @@ -59,8 +59,8 @@ WorkerAutoScalingScheduleStartCapacity: '2' # 8AM ET WorkerAutoScalingScheduleStartRecurrence: '0 13 * * 1-5' WorkerAutoScalingScheduleEndCapacity: '0' -# 6PM ET -WorkerAutoScalingScheduleEndRecurrence: '0 23 * * *' +# 8PM ET +WorkerAutoScalingScheduleEndRecurrence: '0 1 * * *' ITSIBaseURL: '' ITSISecretKey: '' RollbarServerSideAccessToken: '' diff --git a/deployment/packer/template.js b/deployment/packer/template.js index e7feeb50d..6ca73e0b4 100644 --- a/deployment/packer/template.js +++ b/deployment/packer/template.js @@ -63,7 +63,7 @@ "ami_block_device_mappings": [ { "device_name": "/dev/sdf", - "snapshot_id": "snap-090ac799996dba0a4", + "snapshot_id": "snap-0211cbbff8a81266f", "volume_type": "gp2", "delete_on_termination": true } diff --git a/scripts/aws/ebs-warmer.sh b/scripts/aws/ebs-warmer.sh new file mode 100755 index 000000000..bfc4eb2a1 --- /dev/null +++ b/scripts/aws/ebs-warmer.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Ref: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-initialize.html + +function log() { + echo "[$(date --rfc-3339=seconds)] $1" +} + +log "Warming up RWD EBS volume..." + +find /opt/rwd-data/nhd/Main_Watershed/* -type f -print0 \ + | xargs -0 -P0 -L1 -t cat >/dev/null + +find /opt/rwd-data/drb/Main_Watershed/* -type f -print0 \ + | xargs -0 -P0 -L1 -t cat >/dev/null + +log "Done" diff --git a/scripts/aws/setupdb.sh b/scripts/aws/setupdb.sh index 000bbe908..0a9dc5cfc 100755 --- a/scripts/aws/setupdb.sh +++ b/scripts/aws/setupdb.sh @@ -15,6 +15,7 @@ where: \n -m load/reload mapshed data\n -p load/reload DEP data\n -q load/reload water quality data\n + -x purge s3 cache for given path\n " # HTTP accessible storage for initial app data @@ -25,7 +26,7 @@ load_stream=false load_mapshed=false load_water_quality=false -while getopts ":hbsdpmqf:" opt; do +while getopts ":hbsdpmqf:x:" opt; do case $opt in h) echo -e $usage @@ -44,6 +45,8 @@ while getopts ":hbsdpmqf:" opt; do load_water_quality=true ;; f) file_to_load=$OPTARG ;; + x) + path_to_purge=$OPTARG ;; \?) echo "invalid option: -$OPTARG" exit ;; @@ -81,6 +84,11 @@ if [ ! -z "$file_to_load" ] ; then download_and_load $FILES fi +if [ ! -z "$path_to_purge" ] ; then + PATHS=("$path_to_purge") + purge_tile_cache $PATHS +fi + if [ "$load_dep" = "true" ] ; then # Fetch DEP layers FILES=("dep_urban_areas.sql.gz" "dep_municipalities.sql.gz") diff --git a/src/mmw/apps/core/templates/base.html b/src/mmw/apps/core/templates/base.html index 344752258..ce66d94fe 100644 --- a/src/mmw/apps/core/templates/base.html +++ b/src/mmw/apps/core/templates/base.html @@ -1,31 +1,6 @@ {% include 'head.html' %} {% load staticfiles %} - - -
- {% block metatitle %} -+ Average annual loads + from 30-years of daily fluxes + simulated by the GWLF-E (MapShed) model. +
diff --git a/src/mmw/js/src/modeling/gwlfe/runoff/templates/result.html b/src/mmw/js/src/modeling/gwlfe/runoff/templates/result.html index 233917277..ad0ebb5d6 100644 --- a/src/mmw/js/src/modeling/gwlfe/runoff/templates/result.html +++ b/src/mmw/js/src/modeling/gwlfe/runoff/templates/result.html @@ -1,3 +1,8 @@ ++ Average monthly water fluxes in centimeters + from 30-years of daily water balance + simulated by the GWLF-E (MapShed) model. +
diff --git a/src/mmw/js/src/modeling/tests.js b/src/mmw/js/src/modeling/tests.js index 9837ef573..add0276e0 100644 --- a/src/mmw/js/src/modeling/tests.js +++ b/src/mmw/js/src/modeling/tests.js @@ -675,14 +675,14 @@ describe('Modeling', function() { }); model.updateModificationHash(); - assert.equal(model.get('modification_hash'), '65af065d7205cd998aeb0bf15c41f256'); + assert.equal(model.get('modification_hash'), '582b6178186440159bd4ea25f0260892'); var mod = new models.ModificationModel(mocks.modifications.sample2); model.get('modifications').add(mod); - assert.equal(model.get('modification_hash'), 'ae69e823f926824a2fc22d9a5f1ea62c'); + assert.equal(model.get('modification_hash'), 'd95d0c983f0e3d0b171aaa0a84540205'); model.get('modifications').remove(mod); - assert.equal(model.get('modification_hash'), '65af065d7205cd998aeb0bf15c41f256'); + assert.equal(model.get('modification_hash'), '582b6178186440159bd4ea25f0260892'); }); it('is called when the modifications for a scenario changes', function() { @@ -963,6 +963,49 @@ describe('Modeling', function() { assert.isTrue(spyAlert.calledOnce); assert.equal(collection.at(1).get('name'), 'New Scenario 1'); }); + + it('will not show error when trying to save name as is', function() { + var collection = getTestScenarioCollection(), + view = new views.ScenarioTabPanelsView({ collection: collection }); + + view.render(); + var childViews = _.values(view.children._views); + + childViews[1].ui.rename.trigger('click'); + childViews[1].ui.nameField.text('New Scenario 1'); + childViews[1].ui.nameField.trigger('blur'); + + assert.isFalse(spyAlert.calledOnce); + assert.equal(collection.at(1).get('name'), 'New Scenario 1'); + }); + + it('resets name if set to empty string', function() { + var collection = getTestScenarioCollection(), + view = new views.ScenarioTabPanelsView({ collection: collection }); + + view.render(); + var childViews = _.values(view.children._views); + + childViews[1].ui.rename.trigger('click'); + childViews[1].ui.nameField.text(''); + childViews[1].ui.nameField.trigger('blur'); + + assert.equal(collection.at(1).get('name'), 'New Scenario 1'); + }); + + it('resets name if set to just spaces', function() { + var collection = getTestScenarioCollection(), + view = new views.ScenarioTabPanelsView({ collection: collection }); + + view.render(); + var childViews = _.values(view.children._views); + + childViews[1].ui.rename.trigger('click'); + childViews[1].ui.nameField.text(' '); + childViews[1].ui.nameField.trigger('blur'); + + assert.equal(collection.at(1).get('name'), 'New Scenario 1'); + }); }); describe('#duplicateScenario', function() { diff --git a/src/mmw/js/src/modeling/tr55/quality/templates/result.html b/src/mmw/js/src/modeling/tr55/quality/templates/result.html index 1b9351752..72808f68d 100644 --- a/src/mmw/js/src/modeling/tr55/quality/templates/result.html +++ b/src/mmw/js/src/modeling/tr55/quality/templates/result.html @@ -1,2 +1,6 @@ ++ Total loads delivered in a 24-hour hypothetical storm event + as simulated by EPA's STEP-L model algorithms. +
diff --git a/src/mmw/js/src/modeling/tr55/runoff/templates/result.html b/src/mmw/js/src/modeling/tr55/runoff/templates/result.html index 011f052a6..658e05d86 100644 --- a/src/mmw/js/src/modeling/tr55/runoff/templates/result.html +++ b/src/mmw/js/src/modeling/tr55/runoff/templates/result.html @@ -1,2 +1,6 @@ ++ Results of a 24-hour hypothetical storm event + as simulated by SLAMM and TR-55 model algorithms. +
diff --git a/src/mmw/js/src/modeling/views.js b/src/mmw/js/src/modeling/views.js index dd0a5cc1b..92bc3aaaf 100644 --- a/src/mmw/js/src/modeling/views.js +++ b/src/mmw/js/src/modeling/views.js @@ -196,7 +196,7 @@ var ProjectMenuView = Marionette.ItemView.extend({ .fail(function() { var alertView = new modalViews.AlertView({ model: new modalModels.AlertModel({ - alertMessage: 'Could not delete this project.', + alertMessage: 'Could not delete this project.', alertType: modalModels.AlertTypes.error }) }); @@ -399,7 +399,9 @@ var ScenarioTabPanelView = Marionette.ItemView.extend({ return model.get('name').toLowerCase() === newName.toLowerCase(); }); - if (match) { + if (model.get('name') === newName || !newName){ + return false; + } else if (match) { console.log('This name is already in use.'); var alertView = new modalViews.AlertView({ model: new modalModels.AlertModel({ @@ -411,17 +413,27 @@ var ScenarioTabPanelView = Marionette.ItemView.extend({ alertView.render(); return false; - } else if (model.get('name') !== newName) { + } else { return model.set('name', newName); } }, - setScenarioName = function(name) { - if (!updateScenarioName(self.model, name)) { - self.render(); // resets view state - } - }; + setScenarioName = function(name) { + if (!updateScenarioName(self.model, name)) { + self.render(); // resets view state + } + }, + + selectScenarioTabText = function() { + // Select scenario name's text + var range = document.createRange(), + selection = window.getSelection(); + range.selectNodeContents(self.ui.nameField[0]); + selection.removeAllRanges(); + selection.addRange(range); + }; + selectScenarioTabText(); this.ui.nameField.attr('contenteditable', true).focus(); this.ui.nameField.on('keyup', function(e) { @@ -441,6 +453,12 @@ var ScenarioTabPanelView = Marionette.ItemView.extend({ } }); + this.ui.nameField.on('click', function(e) { + // Don't let the outer swallow clicks and exit rename mode + // Allows selecting text on double click + e.stopImmediatePropagation(); + }); + this.ui.nameField.on('blur', function() { setScenarioName($(this).text()); }); diff --git a/src/mmw/js/src/user/views.js b/src/mmw/js/src/user/views.js index 6d6339b9e..8941397f6 100644 --- a/src/mmw/js/src/user/views.js +++ b/src/mmw/js/src/user/views.js @@ -246,7 +246,7 @@ var LoginModalView = ModalBaseView.extend({ var loginURL = '/user/itsi/login?next=/' + Backbone.history.getFragment(); if (window.clientSettings.itsi_embed) { - loginURL = '/?itsi_embed=true&next=/' + Backbone.history.getFragment(); + loginURL += '&itsi_embed=true'; } window.location.href = loginURL; } diff --git a/src/mmw/mmw/settings/base.py b/src/mmw/mmw/settings/base.py index 7d0095383..3c088e0c6 100644 --- a/src/mmw/mmw/settings/base.py +++ b/src/mmw/mmw/settings/base.py @@ -12,7 +12,8 @@ from os.path import abspath, basename, dirname, join, normpath from sys import path -from layer_settings import LAYERS, VIZER_URLS, NHD_REGION2_PERIMETER, DRB_PERIMETER # NOQA +from layer_settings import (LAYERS, VIZER_URLS, VIZER_IGNORE, VIZER_NAMES, + NHD_REGION2_PERIMETER, DRB_PERIMETER) # NOQA from gwlfe_settings import (GWLFE_DEFAULTS, GWLFE_CONFIG, SOIL_GROUP, # NOQA SOILP, CURVE_NUMBER) # NOQA diff --git a/src/mmw/mmw/settings/layer_settings.py b/src/mmw/mmw/settings/layer_settings.py index 7c6b074cb..edb5806c9 100644 --- a/src/mmw/mmw/settings/layer_settings.py +++ b/src/mmw/mmw/settings/layer_settings.py @@ -41,6 +41,7 @@ NHD_REGION2_PERIMETER = json.load(nhd_region2_simple_perimeter_file) +NEW_LINE_AND_TAB = '