diff --git a/README.md b/README.md index 99547aef3..cea90b067 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Local Development -A combination of Vagrant 2.2+ and Ansible 2.5+ is used to setup the development environment for this project. The project consists of the following virtual machines: +A combination of Vagrant 2.2+ and Ansible 2.8 is used to setup the development environment for this project. The project consists of the following virtual machines: - `app` - `services` @@ -32,10 +32,16 @@ First, ensure that you have a set of Amazon Web Services (AWS) credentials with $ aws configure --profile mmw-stg ``` +Ensure you have the [vagrant-disksize](https://github.com/sprotheroe/vagrant-disksize) plugin installed: + +```bash +$ vagrant plugin install vagrant-disksize +``` + Next, use the following command to bring up a local development environment: ```bash -$ MMW_ITSI_SECRET_KEY="***" vagrant up +$ vagrant up ``` The application will now be running at [http://localhost:8000](http://localhost:8000). @@ -130,6 +136,12 @@ $ vagrant ssh worker -c 'sudo service celeryd restart' To enable the geoprocessing cache simply set it to `1` and restart the `celeryd` service. +In some cases, it may be necessary to remove all cached values. This can be done with: + +```bash +$ vagrant ssh services -c 'redis-cli -n 1 --raw KEYS ":1:geop_*" | xargs redis-cli -n 1 DEL' +``` + ### Test Mode In order to run the app in test mode, which simulates the production static asset bundle, reprovision with `VAGRANT_ENV=TEST vagrant provision`. diff --git a/Vagrantfile b/Vagrantfile index 8743f579f..060245fa9 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -3,6 +3,16 @@ Vagrant.require_version ">= 2.2" +# Install vagrant-disksize to allow resizing the services VM. +unless Vagrant.has_plugin?("vagrant-disksize") + raise Vagrant::Errors::VagrantError.new, "vagrant-disksize plugin is missing. Please install it using 'vagrant plugin install vagrant-disksize' and rerun 'vagrant up'" +end + +# We need to stay on Ansible 2.8 because the version_compare filter was removed +# in 2.9. +# https://github.com/ansible/ansible/issues/64174#issuecomment-548639160 +ANSIBLE_VERSION = "2.8.*" + if ["up", "provision", "status"].include?(ARGV.first) require_relative "vagrant/ansible_galaxy_helper" @@ -32,40 +42,69 @@ else VAGRANT_NETWORK_OPTIONS = { auto_correct: false } end +MMW_EXTRA_VARS = { + django_test_database: ENV["MMW_TEST_DB_NAME"] || "test_mmw", + services_ip: ENV["MMW_SERVICES_IP"] || "33.33.34.30", + tiler_host: ENV["MMW_TILER_IP"] || "33.33.34.35", + itsi_secret_key: ENV["MMW_ITSI_SECRET_KEY"], + concord_secret_key: ENV["MMW_CONCORD_SECRET_KEY"], + hydroshare_secret_key: ENV["MMW_HYDROSHARE_SECRET_KEY"], + srat_catchment_api_key: ENV["MMW_SRAT_CATCHMENT_API_KEY"], + tilecache_bucket_name: ENV["MMW_TILECACHE_BUCKET"] || "", +} + Vagrant.configure("2") do |config| - config.vm.box = "bento/ubuntu-16.04" + config.vm.box = "bento/ubuntu-20.04" config.vm.define "services" do |services| services.vm.hostname = "services" - services.vm.network "private_network", ip: ENV.fetch("MMW_SERVICES_IP", "33.33.34.30") + services.vm.network "private_network", ip: ENV["MMW_SERVICES_IP"] || "33.33.34.30" + services.disksize.size = '64GB' # PostgreSQL - services.vm.network "forwarded_port", { + services.vm.network "forwarded_port", **{ guest: 5432, host: 5432 }.merge(VAGRANT_NETWORK_OPTIONS) # Redis - services.vm.network "forwarded_port", { + services.vm.network "forwarded_port", **{ guest: 6379, host: 6379 }.merge(VAGRANT_NETWORK_OPTIONS) services.vm.provider "virtualbox" do |v| v.customize ["guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] - v.memory = 2048 + v.memory = 4096 + v.cpus = 4 end - services.vm.provision "ansible" do |ansible| + services.vm.provision "ansible_local" do |ansible| ansible.compatibility_mode = "2.0" + ansible.install_mode = "pip_args_only" + ansible.pip_install_cmd = "sudo apt-get install -y python3-distutils && curl https://bootstrap.pypa.io/get-pip.py | sudo python3" + ansible.pip_args = "ansible==#{ANSIBLE_VERSION}" ansible.playbook = "deployment/ansible/services.yml" ansible.groups = ANSIBLE_GROUPS.merge(ANSIBLE_ENV_GROUPS) ansible.raw_arguments = ["--timeout=60"] + ansible.extra_vars = MMW_EXTRA_VARS + end + + services.vm.provision "shell" do |s| + # The base box we use comes with a ~30GB disk. The physical disk is + # expanded to 64GB above using vagrant-disksize. The logical disk and + # the file system need to be expanded as well to make full use of the + # space. `lvextend` expands the logical disk, and `resize2fs` expands + # the files system. + s.inline = <<-SHELL + sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv + sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv + SHELL end end config.vm.define "worker" do |worker| worker.vm.hostname = "worker" - worker.vm.network "private_network", ip: ENV.fetch("MMW_WORKER_IP", "33.33.34.20") + worker.vm.network "private_network", ip: ENV["MMW_WORKER_IP"] || "33.33.34.20" worker.vm.synced_folder "src/mmw", "/opt/app" # Facilitates the sharing of Django media root directories across virtual machines. @@ -73,18 +112,18 @@ Vagrant.configure("2") do |config| create: true # Path to RWD data (ex. /media/passport/rwd-nhd) - worker.vm.synced_folder ENV.fetch("RWD_DATA", "/tmp"), "/opt/rwd-data" + worker.vm.synced_folder ENV["RWD_DATA"] || "/tmp", "/opt/rwd-data" # AWS worker.vm.synced_folder "~/.aws", "/var/lib/mmw/.aws" # Docker - worker.vm.network "forwarded_port", { + worker.vm.network "forwarded_port", **{ guest: 2375, host: 2375 }.merge(VAGRANT_NETWORK_OPTIONS) # Geoprocessing Service - worker.vm.network "forwarded_port", { + worker.vm.network "forwarded_port", **{ guest: 8090, host: 8090 }.merge(VAGRANT_NETWORK_OPTIONS) @@ -95,17 +134,21 @@ Vagrant.configure("2") do |config| v.cpus = 2 end - worker.vm.provision "ansible" do |ansible| + worker.vm.provision "ansible_local" do |ansible| ansible.compatibility_mode = "2.0" + ansible.install_mode = "pip_args_only" + ansible.pip_install_cmd = "sudo apt-get install -y python3-distutils && curl https://bootstrap.pypa.io/get-pip.py | sudo python3" + ansible.pip_args = "ansible==#{ANSIBLE_VERSION}" ansible.playbook = "deployment/ansible/workers.yml" ansible.groups = ANSIBLE_GROUPS.merge(ANSIBLE_ENV_GROUPS) ansible.raw_arguments = ["--timeout=60"] + ansible.extra_vars = MMW_EXTRA_VARS end end config.vm.define "app" do |app| app.vm.hostname = "app" - app.vm.network "private_network", ip: ENV.fetch("MMW_APP_IP", "33.33.34.10") + app.vm.network "private_network", ip: ENV["MMW_APP_IP"] || "33.33.34.10" app.vm.synced_folder "src/mmw", "/opt/app" # Facilitates the sharing of Django media root directories across virtual machines. @@ -113,17 +156,17 @@ Vagrant.configure("2") do |config| create: true, mount_options: ["dmode=777"] # Django via Nginx/Gunicorn - app.vm.network "forwarded_port", { + app.vm.network "forwarded_port", **{ guest: 80, host: 8000 }.merge(VAGRANT_NETWORK_OPTIONS) # Livereload server - app.vm.network "forwarded_port", { + app.vm.network "forwarded_port", **{ guest: 35729, host: 35729, }.merge(VAGRANT_NETWORK_OPTIONS) # Testem server - app.vm.network "forwarded_port", { + app.vm.network "forwarded_port", **{ guest: 7357, host: 7357 }.merge(VAGRANT_NETWORK_OPTIONS) @@ -135,22 +178,26 @@ Vagrant.configure("2") do |config| v.memory = 2048 end - app.vm.provision "ansible" do |ansible| + app.vm.provision "ansible_local" do |ansible| ansible.compatibility_mode = "2.0" + ansible.install_mode = "pip_args_only" + ansible.pip_install_cmd = "sudo apt-get install -y python3-distutils && curl https://bootstrap.pypa.io/get-pip.py | sudo python3" + ansible.pip_args = "ansible==#{ANSIBLE_VERSION}" ansible.playbook = "deployment/ansible/app-servers.yml" ansible.groups = ANSIBLE_GROUPS.merge(ANSIBLE_ENV_GROUPS) ansible.raw_arguments = ["--timeout=60"] + ansible.extra_vars = MMW_EXTRA_VARS end end config.vm.define "tiler" do |tiler| tiler.vm.hostname = "tiler" - tiler.vm.network "private_network", ip: ENV.fetch("MMW_TILER_IP", "33.33.34.35") + tiler.vm.network "private_network", ip: ENV["MMW_TILER_IP"] || "33.33.34.35" tiler.vm.synced_folder "src/tiler", "/opt/tiler" # Expose the tiler. Tiler is served by Nginx. - tiler.vm.network "forwarded_port", { + tiler.vm.network "forwarded_port", **{ guest: 80, host: 4000 }.merge(VAGRANT_NETWORK_OPTIONS) @@ -159,11 +206,15 @@ Vagrant.configure("2") do |config| v.memory = 1024 end - tiler.vm.provision "ansible" do |ansible| + tiler.vm.provision "ansible_local" do |ansible| ansible.compatibility_mode = "2.0" + ansible.install_mode = "pip_args_only" + ansible.pip_install_cmd = "sudo apt-get install -y python3-distutils && curl https://bootstrap.pypa.io/get-pip.py | sudo python3" + ansible.pip_args = "ansible==#{ANSIBLE_VERSION}" ansible.playbook = "deployment/ansible/tile-servers.yml" ansible.groups = ANSIBLE_GROUPS.merge(ANSIBLE_ENV_GROUPS) ansible.raw_arguments = ["--timeout=60"] + ansible.extra_vars = MMW_EXTRA_VARS end end end diff --git a/deployment/ansible/group_vars/all b/deployment/ansible/group_vars/all index 098924559..c353cfa55 100644 --- a/deployment/ansible/group_vars/all +++ b/deployment/ansible/group_vars/all @@ -1,9 +1,4 @@ --- -pip_get_pip_version: "2.7" -pip_version: "20.3.*" - -django_test_database: "{{ lookup('env', 'MMW_TEST_DB_NAME') | default('test_mmw', true) }}" - redis_port: 6379 postgresql_port: 5432 @@ -19,43 +14,46 @@ postgresql_username: mmw postgresql_password: mmw postgresql_database: mmw -postgresql_version: "9.6" -postgresql_package_version: "9.6.*.pgdg16.04+1" +postgresql_version: "13" +postgresql_package_version: "13.*.pgdg20.04+1" postgresql_support_repository_channel: "main" -postgresql_support_libpq_version: "13.*.pgdg16.04+1" -postgresql_support_psycopg2_version: "2.7" -postgis_version: "2.3" -postgis_package_version: "2.3.*.pgdg16.04+1" +postgresql_support_libpq_version: "13.*.pgdg20.04+1" +postgresql_support_psycopg2_version: "2.8.*" +postgis_version: "3" +postgis_package_version: "3.2*pgdg20.04+1" -daemontools_version: "1:0.76-6ubuntu1" +daemontools_version: "1:0.76-7" -python_version: "2.7.12-1~16.04" +python_version: "3.8.*" +ansible_python_interpreter: "/usr/bin/python3" yarn_version: "1.19.*" app_nodejs_version: "12.11.1" app_nodejs_npm_version: "6.9.0" tiler_nodejs_version: "10.16.0" -tiler_nodejs_npm_version: "6.9.0" +tiler_nodejs_npm_version: "7.20.6" java_version: "8u*" java_major_version: "8" java_flavor: "openjdk" -docker_version: "5:18.*" -docker_compose_version: "1.23.*" +docker_version: "5:19.*" +docker_python_version: "4.4.*" +docker_compose_version: "1.26.*" geop_host: "localhost" geop_port: 8090 -geop_version: "4.0.3" +geop_version: "5.2.0" geop_cache_enabled: 1 +geop_timeout: 200 nginx_cache_dir: "/var/cache/nginx" enabled_features: '' -llvmlite_version: "0.31.0" -numba_version: "0.38.1" +llvmlite_version: "0.37.0" +numba_version: "0.54.0" phantomjs_version: "2.1.*" -redis_version: "2:3.0.6-1ubuntu0.*" +redis_version: "5:5.0.*" diff --git a/deployment/ansible/group_vars/development b/deployment/ansible/group_vars/development index 7743aba81..546c00065 100644 --- a/deployment/ansible/group_vars/development +++ b/deployment/ansible/group_vars/development @@ -9,28 +9,18 @@ postgresql_hba_mapping: - { type: "host", database: "all", user: "all", address: "33.33.34.1/24", method: "md5" } - { type: "host", database: "all", user: "all", address: "10.0.2.0/24", method: "md5" } -services_ip: "{{ lookup('env', 'MMW_SERVICES_IP') | default('33.33.34.30', true) }}" - redis_host: "{{ services_ip }}" postgresql_host: "{{ services_ip }}" -tiler_host: "{{ lookup('env', 'MMW_TILER_IP') | default('33.33.34.35', true) }}" celery_log_level: "DEBUG" celery_number_of_workers: 2 celery_processes_per_worker: 1 itsi_base_url: "https://learn.staging.concord.org/" -itsi_secret_key: "{{ lookup('env', 'MMW_ITSI_SECRET_KEY') }}" - -concord_secret_key: "{{ lookup('env', 'MMW_CONCORD_SECRET_KEY') }}" hydroshare_base_url: "https://beta.hydroshare.org/" -hydroshare_secret_key: "{{ lookup('env', 'MMW_HYDROSHARE_SECRET_KEY') }}" srat_catchment_api_url: "https://802or9kkk2.execute-api.us-east-2.amazonaws.com/prod/SratRunModel_DEV" -srat_catchment_api_key: "{{ lookup('env', 'MMW_SRAT_CATCHMENT_API_KEY') }}" - -tilecache_bucket_name: "{{ lookup('env', 'MMW_TILECACHE_BUCKET') | default('', true) }}" docker_options: "-H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock" diff --git a/deployment/ansible/group_vars/test b/deployment/ansible/group_vars/test index 9a8db0a6d..03da04a25 100644 --- a/deployment/ansible/group_vars/test +++ b/deployment/ansible/group_vars/test @@ -7,25 +7,17 @@ postgresql_listen_addresses: "*" postgresql_hba_mapping: - { type: "host", database: "all", user: "all", address: "33.33.34.1/24", method: "md5" } -services_ip: "{{ lookup('env', 'MMW_SERVICES_IP') | default('33.33.34.30', true) }}" - redis_host: "{{ services_ip }}" postgresql_host: "{{ services_ip }}" -tiler_host: "{{ lookup('env', 'MMW_TILER_IP') | default('33.33.34.35', true) }}" celery_number_of_workers: 2 celery_processes_per_worker: 1 itsi_base_url: "https://learn.staging.concord.org/" -itsi_secret_key: "{{ lookup('env', 'MMW_ITSI_SECRET_KEY') }}" - -concord_secret_key: "{{ lookup('env', 'MMW_CONCORD_SECRET_KEY') }}" hydroshare_base_url: "https://beta.hydroshare.org/" -hydroshare_secret_key: "{{ lookup('env', 'MMW_HYDROSHARE_SECRET_KEY') }}" srat_catchment_api_url: "https://802or9kkk2.execute-api.us-east-2.amazonaws.com/prod/SratRunModel_DEV" -srat_catchment_api_key: "{{ lookup('env', 'MMW_SRAT_CATCHMENT_API_KEY') }}" tilecache_bucket_name: "tile-cache.staging.app.wikiwatershed.org" diff --git a/deployment/ansible/roles.yml b/deployment/ansible/roles.yml index 6926b7e7a..b1bdf8f5c 100644 --- a/deployment/ansible/roles.yml +++ b/deployment/ansible/roles.yml @@ -1,7 +1,5 @@ - src: azavea.ntp version: 0.1.1 -- src: azavea.pip - version: 2.0.1 - src: azavea.nodejs version: 0.3.0 - src: azavea.yarn @@ -9,17 +7,13 @@ - src: azavea.git version: 0.1.0 - src: azavea.nginx - version: 0.3.1 + version: 1.0.0 - src: azavea.daemontools version: 0.1.0 -- src: azavea.postgresql-support - version: 0.3.0 - src: azavea.postgresql version: 1.0.0 - src: azavea.postgis version: 0.3.0 -- src: azavea.python - version: 0.1.0 - src: azavea.redis version: 0.1.0 - src: azavea.phantomjs @@ -27,6 +21,6 @@ - src: azavea.build-essential version: 0.1.0 - src: azavea.java - version: 0.6.2 + version: 0.7.0 - src: azavea.docker version: 6.0.0 diff --git a/deployment/ansible/roles/model-my-watershed.app/defaults/main.yml b/deployment/ansible/roles/model-my-watershed.app/defaults/main.yml index e58189d25..e7bae19df 100644 --- a/deployment/ansible/roles/model-my-watershed.app/defaults/main.yml +++ b/deployment/ansible/roles/model-my-watershed.app/defaults/main.yml @@ -10,7 +10,7 @@ app_config: DJANGO_POSTGIS_VERSION: "{{ app_postgis_version }}" DJANGO_SECRET_KEY: "{{ app_secret_key }}" -app_postgis_version: 2.1.3 +app_postgis_version: 3.1.4 app_secret_key: "{{ postgresql_password | md5 }}" app_static_root: /var/www/mmw/static/ diff --git a/deployment/ansible/roles/model-my-watershed.app/meta/main.yml b/deployment/ansible/roles/model-my-watershed.app/meta/main.yml index d3ddee1ef..f7eb12483 100644 --- a/deployment/ansible/roles/model-my-watershed.app/meta/main.yml +++ b/deployment/ansible/roles/model-my-watershed.app/meta/main.yml @@ -1,11 +1,8 @@ --- dependencies: - { role: "model-my-watershed.base" } - - { role: "azavea.python", python_development: True } - - { role: "azavea.pip" } - { role: "azavea.yarn" } - { role: "azavea.nodejs", nodejs_version: "{{ app_nodejs_version }}", nodejs_npm_version: "{{ app_nodejs_npm_version }}" } - { role: "azavea.phantomjs" } - - { role: "azavea.build-essential" } - { role: "model-my-watershed.celery" } - { role: "azavea.nginx", nginx_delete_default_site: True } diff --git a/deployment/ansible/roles/model-my-watershed.app/tasks/dependencies.yml b/deployment/ansible/roles/model-my-watershed.app/tasks/dependencies.yml index 0ebd955cd..2ad46c371 100644 --- a/deployment/ansible/roles/model-my-watershed.app/tasks/dependencies.yml +++ b/deployment/ansible/roles/model-my-watershed.app/tasks/dependencies.yml @@ -6,7 +6,7 @@ - { name: "numba", version: "{{ numba_version }}" } - name: Install application Python dependencies for development and test - pip: requirements="{{ app_home }}/requirements/{{ item }}.txt" + pip: requirements="{{ app_home }}/requirements/{{ item }}.txt" state=latest with_items: - development - test diff --git a/deployment/ansible/roles/model-my-watershed.app/tasks/dev-and-test-dependencies.yml b/deployment/ansible/roles/model-my-watershed.app/tasks/dev-and-test-dependencies.yml index dc1139e74..f1df9d8d1 100644 --- a/deployment/ansible/roles/model-my-watershed.app/tasks/dev-and-test-dependencies.yml +++ b/deployment/ansible/roles/model-my-watershed.app/tasks/dev-and-test-dependencies.yml @@ -1,6 +1,6 @@ --- - name: Install Firefox for UI tests - apt: pkg="firefox=8*" state=present + apt: pkg="firefox=9*" state=present - name: Install Xvfb for JavaScript tests - apt: pkg="xvfb=2:1.18.*" state=present + apt: pkg="xvfb=2:1.20.*" state=present diff --git a/deployment/ansible/roles/model-my-watershed.base/defaults/main.yml b/deployment/ansible/roles/model-my-watershed.base/defaults/main.yml index 94f3b85a0..57a72e8fa 100644 --- a/deployment/ansible/roles/model-my-watershed.base/defaults/main.yml +++ b/deployment/ansible/roles/model-my-watershed.base/defaults/main.yml @@ -13,6 +13,7 @@ envdir_config: MMW_GEOPROCESSING_HOST: "{{ geop_host }}" MMW_GEOPROCESSING_PORT: "{{ geop_port }}" MMW_GEOPROCESSING_VERSION: "{{ geop_version }}" + MMW_GEOPROCESSING_TIMEOUT: "{{ geop_timeout }}" MMW_ITSI_CLIENT_ID: "{{ itsi_client_id }}" MMW_ITSI_SECRET_KEY: "{{ itsi_secret_key }}" MMW_ITSI_BASE_URL: "{{ itsi_base_url }}" diff --git a/deployment/ansible/roles/model-my-watershed.base/meta/main.yml b/deployment/ansible/roles/model-my-watershed.base/meta/main.yml index 509d7f2e3..129f99906 100644 --- a/deployment/ansible/roles/model-my-watershed.base/meta/main.yml +++ b/deployment/ansible/roles/model-my-watershed.base/meta/main.yml @@ -2,5 +2,7 @@ dependencies: - { role: "azavea.ntp" } - { role: "azavea.git" } + - { role: "azavea.build-essential" } - { role: "azavea.daemontools" } - - { role: "azavea.postgresql-support" } + - { role: "model-my-watershed.python" } + - { role: "model-my-watershed.postgresql-support" } diff --git a/deployment/ansible/roles/model-my-watershed.base/tasks/dependencies.yml b/deployment/ansible/roles/model-my-watershed.base/tasks/dependencies.yml index 972da24a4..ac63e46cb 100644 --- a/deployment/ansible/roles/model-my-watershed.base/tasks/dependencies.yml +++ b/deployment/ansible/roles/model-my-watershed.base/tasks/dependencies.yml @@ -1,15 +1,15 @@ --- - name: Install Geospatial libraries apt: - pkg: ["binutils=2.26*", - "libproj-dev=4.9.*", - "gdal-bin=1.11.*", - "libgdal1-dev=1.11.*"] + pkg: ["binutils=2.34*", + "libproj-dev=6.3.*", + "gdal-bin=3.0.*", + "libgdal-dev=3.0.*"] state: present when: "['tile-servers'] | is_not_in(group_names)" - name: Configure the main PostgreSQL APT repository - apt_repository: repo="deb http://apt.postgresql.org/pub/repos/apt/ {{ ansible_distribution_release}}-pgdg main" + apt_repository: repo="deb [arch=amd64] https://apt-archive.postgresql.org/pub/repos/apt/ {{ ansible_distribution_release }}-pgdg-archive {{ postgresql_support_repository_channel }}" state=present - name: Install PostgreSQL client diff --git a/deployment/ansible/roles/model-my-watershed.base/tasks/papertrail.yml b/deployment/ansible/roles/model-my-watershed.base/tasks/papertrail.yml index de27f9ebf..82eac75f5 100644 --- a/deployment/ansible/roles/model-my-watershed.base/tasks/papertrail.yml +++ b/deployment/ansible/roles/model-my-watershed.base/tasks/papertrail.yml @@ -2,7 +2,7 @@ get_url: url: https://papertrailapp.com/tools/papertrail-bundle.pem dest: /etc/papertrail-bundle.pem - checksum: sha256:79ea479e9f329de7075c40154c591b51eb056d458bc4dff76d9a4b9c6c4f6d0b + checksum: sha256:ae31ecb3c6e9ff3154cb7a55f017090448f88482f0e94ac927c0c67a1f33b9cf - name: Install rsyslog TLS utils apt: name=rsyslog-gnutls diff --git a/deployment/ansible/roles/model-my-watershed.celery-worker/defaults/main.yml b/deployment/ansible/roles/model-my-watershed.celery-worker/defaults/main.yml index b0cd0d5ce..5ef8a270f 100644 --- a/deployment/ansible/roles/model-my-watershed.celery-worker/defaults/main.yml +++ b/deployment/ansible/roles/model-my-watershed.celery-worker/defaults/main.yml @@ -10,7 +10,7 @@ app_config: DJANGO_POSTGIS_VERSION: "{{ app_postgis_version }}" DJANGO_SECRET_KEY: "{{ app_secret_key }}" -app_postgis_version: 2.1.3 +app_postgis_version: 3.1.4 app_secret_key: "{{ postgresql_password | md5 }}" celery_pid_dir: "/run/celery" diff --git a/deployment/ansible/roles/model-my-watershed.celery-worker/meta/main.yml b/deployment/ansible/roles/model-my-watershed.celery-worker/meta/main.yml index ed09e5f8a..4405de2d1 100644 --- a/deployment/ansible/roles/model-my-watershed.celery-worker/meta/main.yml +++ b/deployment/ansible/roles/model-my-watershed.celery-worker/meta/main.yml @@ -1,7 +1,4 @@ --- dependencies: - { role: "model-my-watershed.base" } - - { role: "azavea.python", python_development: True } - - { role: "azavea.pip" } - - { role: "azavea.build-essential" } - { role: "model-my-watershed.celery" } diff --git a/deployment/ansible/roles/model-my-watershed.celery-worker/tasks/dependencies.yml b/deployment/ansible/roles/model-my-watershed.celery-worker/tasks/dependencies.yml index a55296f95..52692ab16 100644 --- a/deployment/ansible/roles/model-my-watershed.celery-worker/tasks/dependencies.yml +++ b/deployment/ansible/roles/model-my-watershed.celery-worker/tasks/dependencies.yml @@ -6,7 +6,7 @@ - { name: "numba", version: "{{ numba_version }}" } - name: Install application Python dependencies for development and test - pip: requirements="{{ app_home }}/requirements/{{ item }}.txt" + pip: requirements="{{ app_home }}/requirements/{{ item }}.txt" state=latest with_items: - development - test diff --git a/deployment/ansible/roles/model-my-watershed.celery/defaults/main.yml b/deployment/ansible/roles/model-my-watershed.celery/defaults/main.yml index 7d8dddc3c..23cdc3f8d 100644 --- a/deployment/ansible/roles/model-my-watershed.celery/defaults/main.yml +++ b/deployment/ansible/roles/model-my-watershed.celery/defaults/main.yml @@ -1,2 +1,2 @@ --- -celery_version: 4.1.0 +celery_version: 5.2.0 diff --git a/deployment/ansible/roles/model-my-watershed.celery/meta/main.yml b/deployment/ansible/roles/model-my-watershed.celery/meta/main.yml deleted file mode 100644 index 27dd85b12..000000000 --- a/deployment/ansible/roles/model-my-watershed.celery/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -dependencies: - - { role: "azavea.pip" } diff --git a/deployment/ansible/roles/model-my-watershed.celery/tasks/main.yml b/deployment/ansible/roles/model-my-watershed.celery/tasks/main.yml index 3908345f4..26213b088 100644 --- a/deployment/ansible/roles/model-my-watershed.celery/tasks/main.yml +++ b/deployment/ansible/roles/model-my-watershed.celery/tasks/main.yml @@ -2,5 +2,4 @@ - name: Install Celery pip: name="{{ item.name }}" version={{ item.version }} state=present with_items: - - { name: "kombu", version: "4.1.0" } - { name: "celery[redis]", version: "{{ celery_version }}" } diff --git a/deployment/ansible/roles/model-my-watershed.geoprocessing/templates/systemd-geoprocessing.service.j2 b/deployment/ansible/roles/model-my-watershed.geoprocessing/templates/systemd-geoprocessing.service.j2 index c4a5f5f98..dbd6ec82b 100644 --- a/deployment/ansible/roles/model-my-watershed.geoprocessing/templates/systemd-geoprocessing.service.j2 +++ b/deployment/ansible/roles/model-my-watershed.geoprocessing/templates/systemd-geoprocessing.service.j2 @@ -4,7 +4,9 @@ After=network.target [Service] {% if ['development', 'test'] | some_are_in(group_names) -%} -Environment=AWS_PROFILE={{ aws_profile }} +Environment=MMW_GEOPROCESSING_TIMEOUT={{ geop_timeout }}s AWS_PROFILE={{ aws_profile }} +{% else %} +Environment=MMW_GEOPROCESSING_TIMEOUT={{ geop_timeout }}s {% endif %} User=mmw WorkingDirectory={{ geop_home }} diff --git a/deployment/ansible/roles/model-my-watershed.postgresql-support/tasks/main.yml b/deployment/ansible/roles/model-my-watershed.postgresql-support/tasks/main.yml new file mode 100644 index 000000000..5646d4d2c --- /dev/null +++ b/deployment/ansible/roles/model-my-watershed.postgresql-support/tasks/main.yml @@ -0,0 +1,20 @@ +--- +- name: Configure the PostgreSQL APT key + apt_key: url=https://www.postgresql.org/media/keys/ACCC4CF8.asc state=present + +- name: Configure the PostgreSQL APT repositories + apt_repository: repo="deb [arch=amd64] https://apt-archive.postgresql.org/pub/repos/apt/ {{ ansible_distribution_release }}-pgdg-archive {{ postgresql_support_repository_channel }}" + state=present + +- name: Install client API libraries for PostgreSQL + apt: + pkg: + - libpq5={{ postgresql_support_libpq_version }} + - libpq-dev={{ postgresql_support_libpq_version }} + state: present + force: true + +- name: Install PostgreSQL driver for Python + pip: name=psycopg2-binary + version={{ postgresql_support_psycopg2_version }} + state=present diff --git a/deployment/ansible/roles/model-my-watershed.postgresql/meta/main.yml b/deployment/ansible/roles/model-my-watershed.postgresql/meta/main.yml index 0bb5b0045..b581fee62 100644 --- a/deployment/ansible/roles/model-my-watershed.postgresql/meta/main.yml +++ b/deployment/ansible/roles/model-my-watershed.postgresql/meta/main.yml @@ -1,5 +1,5 @@ --- dependencies: - - { role: "azavea.postgresql-support" } + - { role: "model-my-watershed.postgresql-support" } - { role: "azavea.postgresql" } - { role: "azavea.postgis" } diff --git a/deployment/ansible/roles/model-my-watershed.postgresql/tasks/main.yml b/deployment/ansible/roles/model-my-watershed.postgresql/tasks/main.yml index ddc94736b..b3d0c0c12 100644 --- a/deployment/ansible/roles/model-my-watershed.postgresql/tasks/main.yml +++ b/deployment/ansible/roles/model-my-watershed.postgresql/tasks/main.yml @@ -1,4 +1,9 @@ --- +- name: Install ACL, required for Ansible Super User + apt: + name: acl + state: present + - name: Create PostgreSQL super user become_user: postgres postgresql_user: name="{{ postgresql_username }}" diff --git a/deployment/ansible/roles/model-my-watershed.python/tasks/main.yml b/deployment/ansible/roles/model-my-watershed.python/tasks/main.yml new file mode 100644 index 000000000..4ea42e361 --- /dev/null +++ b/deployment/ansible/roles/model-my-watershed.python/tasks/main.yml @@ -0,0 +1,11 @@ +- name: Install Python 3 + apt: + pkg: ["python3={{ python_version }}", + "python3-dev={{ python_version }}", + "python3-distutils={{ python_version }}"] + state: present + +- name: Install old setuptools with use_2to3 support + pip: name=setuptools + version=<58 + state=present diff --git a/deployment/ansible/roles/model-my-watershed.rwd/tasks/app.yml b/deployment/ansible/roles/model-my-watershed.rwd/tasks/app.yml index 4c01ecbd1..89c1d0eca 100644 --- a/deployment/ansible/roles/model-my-watershed.rwd/tasks/app.yml +++ b/deployment/ansible/roles/model-my-watershed.rwd/tasks/app.yml @@ -4,7 +4,7 @@ state=directory - name: Configure RWD service definition - docker_service: + docker_compose: project_name: mmw state: present definition: diff --git a/deployment/ansible/roles/model-my-watershed.tiler/tasks/dependencies.yml b/deployment/ansible/roles/model-my-watershed.tiler/tasks/dependencies.yml index 1eab0bc8d..000917582 100644 --- a/deployment/ansible/roles/model-my-watershed.tiler/tasks/dependencies.yml +++ b/deployment/ansible/roles/model-my-watershed.tiler/tasks/dependencies.yml @@ -1,8 +1,8 @@ --- - name: Install canvas rendering dependencies apt: - pkg: ["libcairo2-dev=1.14.*", - "libpango1.0-dev=1.38.*", + pkg: ["libcairo2-dev=1.16.*", + "libpango1.0-dev=1.44.*", "libjpeg8-dev=8c-*", "libgif-dev=5.1.*"] state: present @@ -11,12 +11,11 @@ apt: pkg: ["libmapnik3.0=3.0.*", "libmapnik-dev=3.0.*", - "mapnik-utils=3.0.*", - "python-mapnik=1:0.0~20151125-92e79d2-1build1"] + "mapnik-utils=3.0.*"] state: present - name: Install Windshaft JavaScript dependencies - command: npm install --unsafe-perm + command: sudo npm install --unsafe-perm args: chdir: "{{ tiler_home }}" become: False diff --git a/deployment/ansible/services.yml b/deployment/ansible/services.yml index 5f169fdbd..b032a39df 100644 --- a/deployment/ansible/services.yml +++ b/deployment/ansible/services.yml @@ -7,5 +7,6 @@ apt: update_cache=yes cache_valid_time=3600 roles: + - { role: "model-my-watershed.python", when: "['development', 'test'] | some_are_in(group_names)" } - { role: "model-my-watershed.postgresql", when: "['development', 'test'] | some_are_in(group_names)" } - { role: "azavea.redis", when: "['development', 'test'] | some_are_in(group_names)" } diff --git a/deployment/cfn/application.py b/deployment/cfn/application.py index c6a36db27..d69007a70 100644 --- a/deployment/cfn/application.py +++ b/deployment/cfn/application.py @@ -12,9 +12,9 @@ autoscaling as asg ) -from utils.cfn import get_recent_ami +from cfn.utils.cfn import get_recent_ami -from utils.constants import ( +from cfn.utils.constants import ( ALLOW_ALL_CIDR, EC2_INSTANCE_TYPES, HTTP, diff --git a/deployment/cfn/data_plane.py b/deployment/cfn/data_plane.py index e7aec0711..6f7726598 100644 --- a/deployment/cfn/data_plane.py +++ b/deployment/cfn/data_plane.py @@ -12,9 +12,9 @@ route53 as r53 ) -from utils.cfn import get_recent_ami +from cfn.utils.cfn import get_recent_ami -from utils.constants import ( +from cfn.utils.constants import ( ALLOW_ALL_CIDR, CANONICAL_ACCOUNT_ID, EC2_INSTANCE_TYPES, @@ -64,7 +64,7 @@ class DataPlane(StackNode): 'KeyName': 'mmw-stg', 'IPAccess': ALLOW_ALL_CIDR, 'BastionHostInstanceType': 't2.medium', - 'RDSInstanceType': 'db.t2.micro', + 'RDSInstanceType': 'db.t3.micro', 'RDSDbName': 'modelmywatershed', 'RDSUsername': 'modelmywatershed', 'RDSPassword': 'modelmywatershed', @@ -111,7 +111,7 @@ def set_up_stack(self): ), 'BastionHostAMI') self.rds_instance_type = self.add_parameter(Parameter( - 'RDSInstanceType', Type='String', Default='db.t2.micro', + 'RDSInstanceType', Type='String', Default='db.t3.micro', Description='RDS instance type', AllowedValues=RDS_INSTANCE_TYPES, ConstraintDescription='must be a valid RDS instance type.' ), 'RDSInstanceType') @@ -195,7 +195,7 @@ def get_recent_bastion_ami(self): bastion_ami_id = self.get_input('BastionHostAMI') except MKUnresolvableInputError: filters = {'name': - 'ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*', + 'ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*', 'architecture': 'x86_64', 'block-device-mapping.volume-type': 'gp2', 'root-device-type': 'ebs', @@ -296,7 +296,7 @@ def create_rds_instance(self): DBParameterGroupName=Ref(self.rds_parameter_group_name), DBSubnetGroupName=Ref(rds_subnet_group), Engine='postgres', - EngineVersion='9.6.14', + EngineVersion='13.4', MasterUsername=Ref(self.rds_username), MasterUserPassword=Ref(self.rds_password), MultiAZ=Ref(self.rds_multi_az), diff --git a/deployment/cfn/stacks.py b/deployment/cfn/stacks.py index e5e4dbf88..21927712a 100644 --- a/deployment/cfn/stacks.py +++ b/deployment/cfn/stacks.py @@ -1,18 +1,18 @@ from majorkirby import GlobalConfigNode -from vpc import VPC -from s3_vpc_endpoint import S3VPCEndpoint -from private_hosted_zone import PrivateHostedZone -from data_plane import DataPlane -from application import Application -from tiler import Tiler -from tile_delivery_network import TileDeliveryNetwork -from worker import Worker -from public_hosted_zone import PublicHostedZone +from cfn.vpc import VPC +from cfn.s3_vpc_endpoint import S3VPCEndpoint +from cfn.private_hosted_zone import PrivateHostedZone +from cfn.data_plane import DataPlane +from cfn.application import Application +from cfn.tiler import Tiler +from cfn.tile_delivery_network import TileDeliveryNetwork +from cfn.worker import Worker +from cfn.public_hosted_zone import PublicHostedZone from boto import cloudformation as cfn -import ConfigParser +import configparser import sys @@ -23,13 +23,13 @@ def get_config(mmw_config_path, profile): :param mmw_config_path: Path to the config file :param profile: Config profile to read """ - mmw_config = ConfigParser.ConfigParser() + mmw_config = configparser.ConfigParser() mmw_config.optionxform = str mmw_config.read(mmw_config_path) try: section = mmw_config.items(profile) - except ConfigParser.NoSectionError: + except configparser.NoSectionError: sys.stderr.write('There is no section [{}] in the configuration ' 'file\n'.format(profile)) sys.stderr.write('you specified. Did you specify the correct file?') diff --git a/deployment/cfn/tile_delivery_network.py b/deployment/cfn/tile_delivery_network.py index 4fc04edd2..18df8f6d1 100644 --- a/deployment/cfn/tile_delivery_network.py +++ b/deployment/cfn/tile_delivery_network.py @@ -10,7 +10,7 @@ s3 ) -from utils.constants import ( +from cfn.utils.constants import ( AMAZON_S3_HOSTED_ZONE_ID, AMAZON_S3_WEBSITE_DOMAIN, ) @@ -89,7 +89,7 @@ def create_cloudfront_distributions(self): DomainName=Join('.', ['tile-cache', Ref(self.public_hosted_zone_name)]), - CustomOriginConfig=cf.CustomOrigin( + CustomOriginConfig=cf.CustomOriginConfig( OriginProtocolPolicy='http-only' ) ) @@ -112,7 +112,7 @@ def create_cloudfront_distributions(self): DomainName=Join('.', ['tile-cache', Ref(self.public_hosted_zone_name)]), - CustomOriginConfig=cf.CustomOrigin( + CustomOriginConfig=cf.CustomOriginConfig( OriginProtocolPolicy='http-only' ) ) diff --git a/deployment/cfn/tiler.py b/deployment/cfn/tiler.py index 46929ff4c..2a3767e9d 100644 --- a/deployment/cfn/tiler.py +++ b/deployment/cfn/tiler.py @@ -13,9 +13,9 @@ route53 as r53 ) -from utils.cfn import get_recent_ami +from cfn.utils.cfn import get_recent_ami -from utils.constants import ( +from cfn.utils.constants import ( ALLOW_ALL_CIDR, EC2_INSTANCE_TYPES, HTTP, diff --git a/deployment/cfn/utils/constants.py b/deployment/cfn/utils/constants.py index d707fe720..cfc2982c7 100644 --- a/deployment/cfn/utils/constants.py +++ b/deployment/cfn/utils/constants.py @@ -8,10 +8,10 @@ ] RDS_INSTANCE_TYPES = [ - 'db.t2.micro', - 'db.t2.small', - 'db.t2.medium', - 'db.t2.large' + 'db.t3.micro', + 'db.t3.small', + 'db.t3.medium', + 'db.t3.large' ] ELASTICACHE_INSTANCE_TYPES = [ diff --git a/deployment/cfn/vpc.py b/deployment/cfn/vpc.py index d25db866f..db0922370 100644 --- a/deployment/cfn/vpc.py +++ b/deployment/cfn/vpc.py @@ -7,13 +7,13 @@ ec2 ) -from utils.cfn import ( +from cfn.utils.cfn import ( get_availability_zones, get_recent_ami, get_subnet_cidr_block ) -from utils.constants import ( +from cfn.utils.constants import ( ALLOW_ALL_CIDR, EC2_INSTANCE_TYPES, HTTP, @@ -94,9 +94,9 @@ def set_up_stack(self): self.add_output(Output('AvailabilityZones', Value=','.join(self.default_azs))) self.add_output(Output('PrivateSubnets', - Value=Join(',', map(Ref, self.default_private_subnets)))) # NOQA + Value=Join(',', list(map(Ref, self.default_private_subnets))))) # NOQA self.add_output(Output('PublicSubnets', - Value=Join(',', map(Ref, self.default_public_subnets)))) # NOQA + Value=Join(',', list(map(Ref, self.default_public_subnets))))) # NOQA self.add_output(Output('RouteTableId', Value=Ref(public_route_table))) def get_recent_nat_ami(self): diff --git a/deployment/cfn/worker.py b/deployment/cfn/worker.py index d6481f00f..3e2ab0a9a 100644 --- a/deployment/cfn/worker.py +++ b/deployment/cfn/worker.py @@ -13,9 +13,9 @@ route53 as r53 ) -from utils.cfn import get_recent_ami +from cfn.utils.cfn import get_recent_ami -from utils.constants import ( +from cfn.utils.constants import ( ALLOW_ALL_CIDR, EC2_INSTANCE_TYPES, HTTP, diff --git a/deployment/packer/driver.py b/deployment/packer/driver.py index c477964fd..8505a31e8 100644 --- a/deployment/packer/driver.py +++ b/deployment/packer/driver.py @@ -18,7 +18,7 @@ def get_recent_ubuntu_ami(region, aws_profile): """Gets AMI ID for current release in region""" filters = { - 'name': 'ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*', + 'name': 'ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*', 'architecture': 'x86_64', 'root-device-type': 'ebs', 'virtualization-type': 'hvm', diff --git a/deployment/packer/mmw.json b/deployment/packer/mmw.json index 72c174047..faf82aa0d 100644 --- a/deployment/packer/mmw.json +++ b/deployment/packer/mmw.json @@ -89,9 +89,9 @@ "inline": [ "sleep 5", "sudo apt-get update -qq", - "sudo apt-get install python-pip python-dev -y", - "sudo pip install --upgrade pip==18.1", - "sudo pip install ansible==2.6.18", + "sudo apt-get install python3 python3-dev python3-distutils -y", + "curl -sL https://bootstrap.pypa.io/get-pip.py | sudo python3", + "sudo pip install ansible==2.9.27", "sudo /bin/sh -c 'echo {{user `branch`}} {{user `description`}} > /srv/version.txt'" ] }, @@ -101,7 +101,7 @@ "playbook_dir": "ansible", "inventory_file": "ansible/inventory/packer-app-server", "extra_arguments": [ - "--user 'ubuntu' --extra-vars 'app_deploy_branch={{user `version`}}'" + "--user 'ubuntu' --extra-vars 'app_deploy_branch={{user `version`}} django_test_database=test_mmw'" ], "only": [ "mmw-app" @@ -125,7 +125,7 @@ "playbook_dir": "ansible", "inventory_file": "ansible/inventory/packer-worker-server", "extra_arguments": [ - "--user 'ubuntu' --extra-vars 'app_deploy_branch={{user `version`}}'" + "--user 'ubuntu' --extra-vars 'app_deploy_branch={{user `version`}} django_test_database=test_mmw'" ], "only": [ "mmw-worker" diff --git a/deployment/requirements.txt b/deployment/requirements.txt index 7f0d481b1..b40575552 100644 --- a/deployment/requirements.txt +++ b/deployment/requirements.txt @@ -1,5 +1,6 @@ -ansible==2.6.18 -majorkirby>=0.2.0,<0.2.99 -troposphere==1.1.0 -boto==2.39.0 +cryptography==3.2.1 +ansible==2.9.27 +troposphere==2.4.9 +majorkirby==1.0.0 +boto==2.49.0 awscli>=1.9.15 diff --git a/doc/MMW_API_landproperties_demo.ipynb b/doc/MMW_API_landproperties_demo.ipynb index d4557acae..7a4c159ba 100644 --- a/doc/MMW_API_landproperties_demo.ipynb +++ b/doc/MMW_API_landproperties_demo.ipynb @@ -260,7 +260,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Issue job request: **analyze/land/**" + "#### Issue job request: **analyze/land/2011_2011/**" ] }, { @@ -269,7 +269,7 @@ "metadata": {}, "outputs": [], "source": [ - "result = analyze_api_request('land', s, api_url, json_payload)" + "result = analyze_api_request('land/2011_2011', s, api_url, json_payload)" ] }, { @@ -643,4 +643,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/scripts/aws/setupdb.sh b/scripts/aws/setupdb.sh index 0d5b8443c..80f920481 100755 --- a/scripts/aws/setupdb.sh +++ b/scripts/aws/setupdb.sh @@ -3,14 +3,15 @@ set -e set -x -usage="$(basename "$0") [-h] [-b] [-s] \n +usage="$(basename "$0") [-h] [options] \n --Sets up a postgresql database for MMW \n \n -where: \n +where options are one or more of: \n -h show this help text\n -b load/reload boundary data\n -f load a named boundary sql.gz\n -s load/reload stream data\n + -S load/reload Hi Res stream data (very large)\n -d load/reload DRB stream data\n -m load/reload mapshed data\n -p load/reload DEP data\n @@ -24,11 +25,12 @@ FILE_HOST="https://s3.amazonaws.com/data.mmw.azavea.com" load_boundary=false file_to_load= load_stream=false +load_hires_stream=false load_mapshed=false load_water_quality=false load_catchment=false -while getopts ":hbsdpmqcf:x:" opt; do +while getopts ":hbsSdpmqcf:x:" opt; do case $opt in h) echo -e $usage @@ -37,6 +39,8 @@ while getopts ":hbsdpmqcf:x:" opt; do load_boundary=true ;; s) load_stream=true ;; + S) + load_hires_stream=true ;; d) load_drb_streams=true ;; p) @@ -116,7 +120,7 @@ fi if [ "$load_boundary" = "true" ] ; then # Fetch boundary layer sql files - FILES=("boundary_county.sql.gz" "boundary_school_district.sql.gz" "boundary_district.sql.gz" "boundary_huc12_deduped.sql.gz" "boundary_huc10.sql.gz" "boundary_huc08.sql.gz") + FILES=("boundary_county_20210910.sql.gz" "boundary_school_district.sql.gz" "boundary_district.sql.gz" "boundary_huc12_deduped.sql.gz" "boundary_huc10.sql.gz" "boundary_huc08.sql.gz") PATHS=("county" "district" "huc8" "huc10" "huc12" "school") TRGM_TABLES=("boundary_huc08" "boundary_huc10" "boundary_huc12") @@ -134,6 +138,16 @@ if [ "$load_stream" = "true" ] ; then purge_tile_cache $PATHS fi +if [ "$load_hires_stream" = "true" ] ; then + # Fetch hires stream network layer sql files + FILES=("nhdflowlinehr.sql.gz") + PATHS=("nhd_streams_hr_v1") + + download_and_load $FILES + purge_tile_cache $PATHS +fi + + if [ "$load_drb_streams" = "true" ] ; then # Fetch DRB stream network layer sql file FILES=("drb_streams_50.sql.gz") diff --git a/scripts/aws/staging-deployment.sh b/scripts/aws/staging-deployment.sh index 7702b1393..435e2a958 100755 --- a/scripts/aws/staging-deployment.sh +++ b/scripts/aws/staging-deployment.sh @@ -32,7 +32,7 @@ fi pushd deployment # Attempt to launch a new stack & cutover DNS -python mmw_stack.py launch-stacks \ +python3 mmw_stack.py launch-stacks \ --aws-profile "${MMW_AWS_PROFILE}" \ --mmw-profile "${MMW_PROFILE}" \ --mmw-config-path "${MMW_CONFIG_PATH}" \ @@ -40,7 +40,7 @@ python mmw_stack.py launch-stacks \ --activate-dns # Remove old stack -python mmw_stack.py remove-stacks \ +python3 mmw_stack.py remove-stacks \ --aws-profile "${MMW_AWS_PROFILE}" \ --mmw-profile "${MMW_PROFILE}" \ --mmw-config-path "${MMW_CONFIG_PATH}" \ diff --git a/scripts/data/climate/colorize.py b/scripts/data/climate/colorize.py index 7f0450ba6..50ab94fe8 100644 --- a/scripts/data/climate/colorize.py +++ b/scripts/data/climate/colorize.py @@ -1,6 +1,3 @@ -from __future__ import division -from __future__ import print_function - import os import sys import rasterio diff --git a/src/mmw/apps/bigcz/clients/__init__.py b/src/mmw/apps/bigcz/clients/__init__.py index 3b031e77d..89ae3a64f 100644 --- a/src/mmw/apps/bigcz/clients/__init__.py +++ b/src/mmw/apps/bigcz/clients/__init__.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from apps.bigcz.clients import (cinergi, hydroshare, cuahsi, diff --git a/src/mmw/apps/bigcz/clients/cinergi/__init__.py b/src/mmw/apps/bigcz/clients/cinergi/__init__.py index 762ecc803..5c205fff9 100644 --- a/src/mmw/apps/bigcz/clients/cinergi/__init__.py +++ b/src/mmw/apps/bigcz/clients/cinergi/__init__.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from apps.bigcz.clients.cinergi.models import CinergiResource from apps.bigcz.clients.cinergi.serializers import CinergiResourceSerializer diff --git a/src/mmw/apps/bigcz/clients/cinergi/models.py b/src/mmw/apps/bigcz/clients/cinergi/models.py index 28042363f..c119192b3 100644 --- a/src/mmw/apps/bigcz/clients/cinergi/models.py +++ b/src/mmw/apps/bigcz/clients/cinergi/models.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from apps.bigcz.models import Resource diff --git a/src/mmw/apps/bigcz/clients/cinergi/search.py b/src/mmw/apps/bigcz/clients/cinergi/search.py index 5701241e5..8d3fc9ea8 100644 --- a/src/mmw/apps/bigcz/clients/cinergi/search.py +++ b/src/mmw/apps/bigcz/clients/cinergi/search.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import requests import dateutil.parser -from HTMLParser import HTMLParser +from html.parser import HTMLParser from django.contrib.gis.geos import Polygon from django.conf import settings @@ -18,7 +14,7 @@ CINERGI_HOST = 'http://cinergi.sdsc.edu' CATALOG_NAME = 'cinergi' -CATALOG_URL = '{}/geoportal/opensearch'.format(CINERGI_HOST) +CATALOG_URL = f'{CINERGI_HOST}/geoportal/opensearch' PAGE_SIZE = settings.BIGCZ_CLIENT_PAGE_SIZE @@ -73,7 +69,7 @@ def parse_links(source): result = [] links = source.get('links_s', []) - if isinstance(links, basestring): + if isinstance(links, str): links = [links] for url in links: @@ -87,7 +83,7 @@ def parse_cinergi_url(fileid): Convert fileid to URL in CINERGI Portal """ - return '{}/geoportal/?filter=%22{}%22'.format(CINERGI_HOST, fileid) + return f'{CINERGI_HOST}/geoportal/?filter=%22{fileid}%22' def parse_string_or_list(string_or_list): @@ -95,7 +91,7 @@ def parse_string_or_list(string_or_list): Fields like contact_organizations be either a list of strings, or a string. Make it always a list of strings """ - if isinstance(string_or_list, basestring): + if isinstance(string_or_list, str): return [string_or_list] return string_or_list @@ -116,11 +112,11 @@ def parse_categories(source): categories = source.get('hierarchies_cat', source.get('categories_cat')) if not categories or \ - not all(isinstance(c, basestring) for c in categories): + not all(isinstance(c, str) for c in categories): # We only handle categories that are lists of strings return None - if isinstance(categories, basestring): + if isinstance(categories, str): categories = [categories] split_categories = [category.split(">") for category in categories] @@ -222,7 +218,7 @@ def parse_record(item): def prepare_bbox(box): - return '{},{},{},{}'.format(box.xmin, box.ymin, box.xmax, box.ymax) + return f'{box.xmin},{box.ymin},{box.xmax},{box.ymax}' def prepare_date(value): @@ -234,7 +230,7 @@ def prepare_date(value): def prepare_time(from_date, to_date): value = prepare_date(from_date) if to_date: - value = '{}/{}'.format(value, prepare_date(to_date)) + value = f'{value}/{prepare_date(to_date)}' return value diff --git a/src/mmw/apps/bigcz/clients/cinergi/serializers.py b/src/mmw/apps/bigcz/clients/cinergi/serializers.py index f0ad22f9a..a5feb1088 100644 --- a/src/mmw/apps/bigcz/clients/cinergi/serializers.py +++ b/src/mmw/apps/bigcz/clients/cinergi/serializers.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from rest_framework.serializers import (CharField, ListField, DateTimeField, diff --git a/src/mmw/apps/bigcz/clients/cuahsi/__init__.py b/src/mmw/apps/bigcz/clients/cuahsi/__init__.py index 2dae317d7..4b9ff13a3 100644 --- a/src/mmw/apps/bigcz/clients/cuahsi/__init__.py +++ b/src/mmw/apps/bigcz/clients/cuahsi/__init__.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from apps.bigcz.clients.cuahsi.models import CuahsiResource from apps.bigcz.clients.cuahsi.serializers import CuahsiResourceSerializer diff --git a/src/mmw/apps/bigcz/clients/cuahsi/details.py b/src/mmw/apps/bigcz/clients/cuahsi/details.py index 79719dfbf..cf8f391f7 100644 --- a/src/mmw/apps/bigcz/clients/cuahsi/details.py +++ b/src/mmw/apps/bigcz/clients/cuahsi/details.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from datetime import date, timedelta from socket import timeout diff --git a/src/mmw/apps/bigcz/clients/cuahsi/models.py b/src/mmw/apps/bigcz/clients/cuahsi/models.py index 7e304c00e..99d712350 100644 --- a/src/mmw/apps/bigcz/clients/cuahsi/models.py +++ b/src/mmw/apps/bigcz/clients/cuahsi/models.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from apps.bigcz.models import Resource diff --git a/src/mmw/apps/bigcz/clients/cuahsi/search.py b/src/mmw/apps/bigcz/clients/cuahsi/search.py index b2119e998..295d4dc50 100644 --- a/src/mmw/apps/bigcz/clients/cuahsi/search.py +++ b/src/mmw/apps/bigcz/clients/cuahsi/search.py @@ -1,10 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from datetime import date -from urllib2 import URLError +from urllib.error import URLError from socket import timeout from operator import attrgetter, itemgetter @@ -69,7 +65,7 @@ def recursive_asdict(d): From https://gist.github.com/robcowie/a6a56cf5b17a86fdf461 """ out = {} - for k, v in asdict(d).iteritems(): + for k, v in asdict(d).items(): if hasattr(v, '__keylist__'): out[k] = recursive_asdict(v) elif isinstance(v, list): @@ -121,18 +117,14 @@ def parse_details_url(record): if len(parts) == 2: code, id = parts if code == 'NWISDV': - url = 'https://waterdata.usgs.gov/nwis/dv/?site_no={}' - return url.format(id) + return f'https://waterdata.usgs.gov/nwis/dv/?site_no={id}' elif code == 'NWISUV': - url = 'https://waterdata.usgs.gov/nwis/uv/?site_no={}' - return url.format(id) + return f'https://waterdata.usgs.gov/nwis/uv/?site_no={id}' elif code == 'NWISGW': - url = ('https://nwis.waterdata.usgs.gov/' + - 'usa/nwis/gwlevels/?site_no={}') - return url.format(id) + return ('https://nwis.waterdata.usgs.gov/' + f'usa/nwis/gwlevels/?site_no={id}') elif code == 'EnviroDIY': - url = 'http://data.envirodiy.org/sites/{}/' - return url.format(id) + return f'http://data.envirodiy.org/sites/{id}/' return None @@ -205,7 +197,7 @@ def group_series_by_location(series): group.append(record) records = [] - for location, group in groups.iteritems(): + for location, group in groups.items(): records.append({ 'serv_code': group[0]['ServCode'], 'serv_url': group[0]['ServURL'], @@ -234,8 +226,8 @@ def group_series_by_location(series): def make_request(request, expiry, **kwargs): - key = 'bigcz_cuahsi_{}_{}'.format(request.method.name, - hash(frozenset(kwargs.items()))) + key = \ + f'bigcz_cuahsi_{request.method.name}_{hash(frozenset(kwargs.items()))}' cached = cache.get(key) if cached: return cached @@ -244,7 +236,7 @@ def make_request(request, expiry, **kwargs): response = recursive_asdict(request(**kwargs)) cache.set(key, response, timeout=expiry) return response - except URLError, e: + except URLError as e: if isinstance(e.reason, timeout): raise RequestTimedOutError() else: @@ -316,10 +308,9 @@ def search(**kwargs): if bbox_area > settings.BIGCZ_MAX_AREA: raise ValidationError({ - 'error': 'The selected area of interest with a bounding box of {} ' - 'km² is larger than the currently supported maximum size ' - 'of {} km².'.format(round(bbox_area, 2), - settings.BIGCZ_MAX_AREA)}) + 'error': 'The selected area of interest with a bounding box of ' + f'{round(bbox_area, 2)} km² is larger than the currently ' + f'supported maximum size of {settings.BIGCZ_MAX_AREA} km².'}) # NOQA world = BBox(-180, -90, 180, 90) diff --git a/src/mmw/apps/bigcz/clients/cuahsi/serializers.py b/src/mmw/apps/bigcz/clients/cuahsi/serializers.py index 2d30c1893..989d0fdbe 100644 --- a/src/mmw/apps/bigcz/clients/cuahsi/serializers.py +++ b/src/mmw/apps/bigcz/clients/cuahsi/serializers.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from rest_framework.serializers import (CharField, DateTimeField, ListField, diff --git a/src/mmw/apps/bigcz/clients/hydroshare/__init__.py b/src/mmw/apps/bigcz/clients/hydroshare/__init__.py index 8e6d8123f..a12fe5d26 100644 --- a/src/mmw/apps/bigcz/clients/hydroshare/__init__.py +++ b/src/mmw/apps/bigcz/clients/hydroshare/__init__.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from apps.bigcz.clients.hydroshare import models, serializers # Import catalog name and search function, so it can be exported from here diff --git a/src/mmw/apps/bigcz/clients/hydroshare/models.py b/src/mmw/apps/bigcz/clients/hydroshare/models.py index 189691e55..863855dda 100644 --- a/src/mmw/apps/bigcz/clients/hydroshare/models.py +++ b/src/mmw/apps/bigcz/clients/hydroshare/models.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from apps.bigcz.models import Resource diff --git a/src/mmw/apps/bigcz/clients/hydroshare/search.py b/src/mmw/apps/bigcz/clients/hydroshare/search.py index a0c51b0e1..cf19ea809 100644 --- a/src/mmw/apps/bigcz/clients/hydroshare/search.py +++ b/src/mmw/apps/bigcz/clients/hydroshare/search.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from requests import Request, Session, Timeout import dateutil.parser from datetime import datetime @@ -145,7 +141,7 @@ def search(**kwargs): CATALOG_URL, params=params)) - key = 'bigcz_hydroshare_{}'.format(hash(frozenset(params.items()))) + key = f'bigcz_hydroshare_{hash(frozenset(params.items()))}' cached = cache.get(key) if cached: data = cached diff --git a/src/mmw/apps/bigcz/clients/hydroshare/serializers.py b/src/mmw/apps/bigcz/clients/hydroshare/serializers.py index 0869a68c2..43e7442a7 100644 --- a/src/mmw/apps/bigcz/clients/hydroshare/serializers.py +++ b/src/mmw/apps/bigcz/clients/hydroshare/serializers.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from rest_framework.serializers import DateTimeField from apps.bigcz.serializers import ResourceSerializer diff --git a/src/mmw/apps/bigcz/clients/usgswqp/__init__.py b/src/mmw/apps/bigcz/clients/usgswqp/__init__.py index a914c50a7..3df8945eb 100644 --- a/src/mmw/apps/bigcz/clients/usgswqp/__init__.py +++ b/src/mmw/apps/bigcz/clients/usgswqp/__init__.py @@ -1,9 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, - division, - print_function, - unicode_literals) - from apps.bigcz.clients.usgswqp.models import USGSResource from apps.bigcz.clients.usgswqp.serializers import USGSResourceSerializer diff --git a/src/mmw/apps/bigcz/clients/usgswqp/models.py b/src/mmw/apps/bigcz/clients/usgswqp/models.py index 8b04ffbc5..3b9235b93 100644 --- a/src/mmw/apps/bigcz/clients/usgswqp/models.py +++ b/src/mmw/apps/bigcz/clients/usgswqp/models.py @@ -1,9 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, - division, - print_function, - unicode_literals) - from apps.bigcz.models import Resource diff --git a/src/mmw/apps/bigcz/clients/usgswqp/search.py b/src/mmw/apps/bigcz/clients/usgswqp/search.py index ddad22b8c..af906ff46 100644 --- a/src/mmw/apps/bigcz/clients/usgswqp/search.py +++ b/src/mmw/apps/bigcz/clients/usgswqp/search.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, - division, - print_function, - unicode_literals) import requests from datetime import date @@ -64,15 +60,13 @@ def parse_record(record): created_at=None, updated_at=None, geom=geom, - details_url='https://www.waterqualitydata.us/provider/{prov}/{org}/{id}/'.format(prov=record['ProviderName'], # NOQA - org=record['OrganizationIdentifier'], # NOQA - id=record['MonitoringLocationIdentifier']), # NOQA + details_url=f'https://www.waterqualitydata.us/provider/{record["ProviderName"]}/{record["OrganizationIdentifier"]}/{record["MonitoringLocationIdentifier"]}/', # NOQA sample_mediums=None, variables=None, service_org=record['OrganizationIdentifier'], service_orgname=record['OrganizationFormalName'], service_code=record['MonitoringLocationIdentifier'], - service_url='https://www.waterqualitydata.us/data/Result/search?siteid={}&mimeType=csv&sorted=no&zip=yes'.format(record['MonitoringLocationIdentifier']), # NOQA + service_url=f'https://www.waterqualitydata.us/data/Result/search?siteid={record["MonitoringLocationIdentifier"]}&mimeType=csv&sorted=no&zip=yes', # NOQA service_title=None, service_citation='National Water Quality Monitoring Council, [YEAR]. Water Quality Portal. Accessed [DATE ACCESSED]. https://www.waterqualitydata.us/', # NOQA begin_date=None, @@ -96,9 +90,9 @@ def search(**kwargs): if bbox_area > USGS_MAX_SIZE_SQKM: raise ValidationError({ - 'error': 'The selected area of interest with a bounding box of {} ' - 'km² is larger than the currently supported maximum size ' - 'of {} km².'.format(round(bbox_area, 2), USGS_MAX_SIZE_SQKM)}) # NOQA + 'error': 'The selected area of interest with a bounding box of ' + f'{round(bbox_area, 2)} km² is larger than the currently ' + f'supported maximum size of {USGS_MAX_SIZE_SQKM} km².'}) params = { # bBox might be used in the future # 'bBox': '{0:.3f},{1:.3f},{2:.3f},{3:.3f}'.format(bbox.xmin, bbox.ymin, bbox.xmax, bbox.ymax), # NOQA diff --git a/src/mmw/apps/bigcz/clients/usgswqp/serializers.py b/src/mmw/apps/bigcz/clients/usgswqp/serializers.py index e59ccfdc7..b80d9e253 100644 --- a/src/mmw/apps/bigcz/clients/usgswqp/serializers.py +++ b/src/mmw/apps/bigcz/clients/usgswqp/serializers.py @@ -1,9 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, - division, - print_function, - unicode_literals) - from rest_framework.serializers import (CharField, DateTimeField, ListField, diff --git a/src/mmw/apps/bigcz/models.py b/src/mmw/apps/bigcz/models.py index 26de27188..c21fce227 100644 --- a/src/mmw/apps/bigcz/models.py +++ b/src/mmw/apps/bigcz/models.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from django.contrib.gis.geos import Polygon @@ -47,6 +43,6 @@ def area(self): polygon = Polygon.from_bbox(( self.xmin, self.ymin, self.xmax, self.ymax)) - polygon.set_srid(4326) + polygon.srid = 4326 return polygon.transform(5070, clone=True).area diff --git a/src/mmw/apps/bigcz/serializers.py b/src/mmw/apps/bigcz/serializers.py index f6dcc880c..921219903 100644 --- a/src/mmw/apps/bigcz/serializers.py +++ b/src/mmw/apps/bigcz/serializers.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from math import ceil from rest_framework.serializers import \ diff --git a/src/mmw/apps/bigcz/urls.py b/src/mmw/apps/bigcz/urls.py index 5b2f8de23..f7f758b39 100644 --- a/src/mmw/apps/bigcz/urls.py +++ b/src/mmw/apps/bigcz/urls.py @@ -1,15 +1,11 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - -from django.conf.urls import url +from django.urls import re_path from apps.bigcz import views app_name = 'bigcz' urlpatterns = [ - url(r'^search$', views.search, name='search'), - url(r'^details$', views.details, name='details'), - url(r'^values$', views.values, name='values'), + re_path(r'^search$', views.search, name='search'), + re_path(r'^details$', views.details, name='details'), + re_path(r'^values$', views.values, name='values'), ] diff --git a/src/mmw/apps/bigcz/utils.py b/src/mmw/apps/bigcz/utils.py index 080ba30bf..33f6d4487 100644 --- a/src/mmw/apps/bigcz/utils.py +++ b/src/mmw/apps/bigcz/utils.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import csv +from io import TextIOWrapper from apps.bigcz.models import BBox @@ -65,9 +62,8 @@ class RequestTimedOutError(APIException): class ValuesTimedOutError(APIException): status_code = status.HTTP_504_GATEWAY_TIMEOUT - default_detail = \ - 'Request for values did not finish in {} seconds'.format( - settings.BIGCZ_CLIENT_TIMEOUT) + default_detail = ('Request for values did not finish in ' + f'{settings.BIGCZ_CLIENT_TIMEOUT} seconds') class ServiceNotAvailableError(ValidationError): @@ -75,10 +71,11 @@ class ServiceNotAvailableError(ValidationError): default_detail = 'Underlying service is not available.' -def read_unicode_csv(utf8_data, **kwargs): +def read_unicode_csv(file_in_zip, **kwargs): + utf8_data = TextIOWrapper(file_in_zip, encoding='utf-8') csv_reader = csv.DictReader(utf8_data, **kwargs) for row in csv_reader: yield { - key.decode('utf-8'): value.decode('utf-8') - for key, value in row.iteritems() + key: value + for key, value in row.items() } diff --git a/src/mmw/apps/bigcz/views.py b/src/mmw/apps/bigcz/views.py index c04636ffb..9d348979a 100644 --- a/src/mmw/apps/bigcz/views.py +++ b/src/mmw/apps/bigcz/views.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import json from django.contrib.gis.geos import GEOSGeometry @@ -48,8 +44,7 @@ def _do_search(request): if catalog not in CATALOGS: raise ValidationError({ - 'error': 'Catalog must be one of: {}' - .format(', '.join(CATALOGS.keys()))}) + 'error': f'Catalog must be one of: {", ".join(CATALOGS.keys())}'}) # Store geojson to pass in search kwargs geojson = json.dumps(params.get('geom')) @@ -100,15 +95,13 @@ def _get_details(request): if catalog not in CATALOGS: raise ValidationError({ - 'error': 'Catalog must be one of: {}' - .format(', '.join(CATALOGS.keys()))}) + 'error': f'Catalog must be one of: {", ".join(CATALOGS.keys())}'}) details = CATALOGS[catalog]['details'] if not details: raise NotFound({ - 'error': 'No details endpoint for {}' - .format(catalog)}) + 'error': f'No details endpoint for {catalog}'}) details_kwargs = { 'wsdl': params.get('wsdl'), @@ -131,15 +124,13 @@ def _get_values(request): if catalog not in CATALOGS: raise ValidationError({ - 'error': 'Catalog must be one of: {}' - .format(', '.join(CATALOGS.keys()))}) + 'error': f'Catalog must be one of: {", ".join(CATALOGS.keys())}'}) values = CATALOGS[catalog]['values'] if not values: raise NotFound({ - 'error': 'No values endpoint for {}' - .format(catalog)}) + 'error': f'No values endpoint for {catalog}'}) values_kwargs = { 'wsdl': params.get('wsdl'), diff --git a/src/mmw/apps/core/admin.py b/src/mmw/apps/core/admin.py index 1405eb536..d5652ba94 100644 --- a/src/mmw/apps/core/admin.py +++ b/src/mmw/apps/core/admin.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from django.contrib import admin from apps.core.models import Job diff --git a/src/mmw/apps/core/decorators.py b/src/mmw/apps/core/decorators.py index ee0bdfd47..e1867ee95 100644 --- a/src/mmw/apps/core/decorators.py +++ b/src/mmw/apps/core/decorators.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - import sys import rollbar @@ -65,7 +62,7 @@ def _get_remote_addr(request): ipaddr = request.META.get("HTTP_X_FORWARDED_FOR", None) if ipaddr: # X_FORWARDED_FOR returns client1, proxy1, proxy2,... - return [x.strip() for x in ipaddr.split(",")][0] + return [x.strip() for x in ipaddr.split(",")][0] else: return request.META.get("REMOTE_ADDR", "") diff --git a/src/mmw/apps/core/mail/backends/boto_ses_mailer.py b/src/mmw/apps/core/mail/backends/boto_ses_mailer.py index 7d781f3cb..eb628d107 100644 --- a/src/mmw/apps/core/mail/backends/boto_ses_mailer.py +++ b/src/mmw/apps/core/mail/backends/boto_ses_mailer.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import json import logging import sys @@ -35,7 +31,7 @@ def send_messages(self, email_messages): if self.check_quota: remaining_quota = self.mailer.get_remaining_message_quota() else: - remaining_quota = sys.maxint + remaining_quota = sys.maxsize if len(email_messages) <= remaining_quota: for email_message in email_messages: diff --git a/src/mmw/apps/core/migrations/0001_initial.py b/src/mmw/apps/core/migrations/0001_initial.py index 2180dd087..dac56e73f 100644 --- a/src/mmw/apps/core/migrations/0001_initial.py +++ b/src/mmw/apps/core/migrations/0001_initial.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations from django.conf import settings import django.db.models.deletion diff --git a/src/mmw/apps/core/migrations/0002_requestlog.py b/src/mmw/apps/core/migrations/0002_requestlog.py index 8ff64f41b..61df2a60c 100644 --- a/src/mmw/apps/core/migrations/0002_requestlog.py +++ b/src/mmw/apps/core/migrations/0002_requestlog.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models import django.db.models.deletion from django.conf import settings diff --git a/src/mmw/apps/core/migrations/0003_requestlog_api_referrer.py b/src/mmw/apps/core/migrations/0003_requestlog_api_referrer.py index b10fb4bb5..384b49dad 100644 --- a/src/mmw/apps/core/migrations/0003_requestlog_api_referrer.py +++ b/src/mmw/apps/core/migrations/0003_requestlog_api_referrer.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/core/migrations/0004_job_uuid_unique_constraint.py b/src/mmw/apps/core/migrations/0004_job_uuid_unique_constraint.py index bd7f487b0..d63017fcf 100644 --- a/src/mmw/apps/core/migrations/0004_job_uuid_unique_constraint.py +++ b/src/mmw/apps/core/migrations/0004_job_uuid_unique_constraint.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/core/migrations/0005_job_on_delete.py b/src/mmw/apps/core/migrations/0005_job_on_delete.py index 893d2087d..464d96c58 100644 --- a/src/mmw/apps/core/migrations/0005_job_on_delete.py +++ b/src/mmw/apps/core/migrations/0005_job_on_delete.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.22 on 2019-07-16 16:54 -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models import django.db.models.deletion diff --git a/src/mmw/apps/core/models.py b/src/mmw/apps/core/models.py index 7474440b5..6da97b5e5 100644 --- a/src/mmw/apps/core/models.py +++ b/src/mmw/apps/core/models.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - from django.db import models from django.conf import settings @@ -25,7 +22,7 @@ class Job(models.Model): status = models.CharField(max_length=255) def __unicode__(self): - return unicode(self.uuid) + return str(self.uuid) class RequestLog(models.Model): diff --git a/src/mmw/apps/core/tasks.py b/src/mmw/apps/core/tasks.py index fe72d4971..7532a1492 100644 --- a/src/mmw/apps/core/tasks.py +++ b/src/mmw/apps/core/tasks.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import - from django.utils.timezone import now from celery import shared_task from apps.core.models import Job @@ -36,9 +32,9 @@ def save_job_error(request, exc, traceback, job_id): job.status = 'failed' job.save() except Exception as e: - logger.error('Failed to save job error status. Job will appear hung. \ - Job Id: {0}'.format(job.id)) - logger.error('Error: {}'.format(e)) + logger.error('Failed to save job error status. Job will appear hung.' + f'Job Id: {job.id}') + logger.error(f'Error: {e}') @shared_task(bind=True) diff --git a/src/mmw/apps/core/templates/base.html b/src/mmw/apps/core/templates/base.html index 29a0e8dd2..dc41eb903 100644 --- a/src/mmw/apps/core/templates/base.html +++ b/src/mmw/apps/core/templates/base.html @@ -1,5 +1,5 @@ {% include 'head.html' %} -{% load staticfiles %} +{% load static %} {% block header %} diff --git a/src/mmw/apps/core/templates/head.html b/src/mmw/apps/core/templates/head.html index 60590bd5a..4f0f309ac 100644 --- a/src/mmw/apps/core/templates/head.html +++ b/src/mmw/apps/core/templates/head.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} diff --git a/src/mmw/apps/core/tests.py b/src/mmw/apps/core/tests.py index 7734e6a24..23e1480d1 100644 --- a/src/mmw/apps/core/tests.py +++ b/src/mmw/apps/core/tests.py @@ -1,6 +1,3 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division # Create your tests here. diff --git a/src/mmw/apps/export/hydroshare.py b/src/mmw/apps/export/hydroshare.py index 26b1704ea..b6fa9708f 100644 --- a/src/mmw/apps/export/hydroshare.py +++ b/src/mmw/apps/export/hydroshare.py @@ -1,14 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import json -from cStringIO import StringIO +from io import StringIO from zipfile import ZipFile from rauth import OAuth2Service -from urlparse import urljoin, urlparse +from urllib.parse import urljoin, urlparse from hs_restclient import HydroShare, HydroShareAuthOAuth2, HydroShareNotFound from django.conf import settings @@ -59,7 +55,7 @@ def renew_access_token(self, user_id): if 'error' in res: raise RuntimeError(res['error']) - for key, value in res.iteritems(): + for key, value in res.items(): setattr(token, key, value) token.save() diff --git a/src/mmw/apps/export/migrations/0001_hydroshareresource.py b/src/mmw/apps/export/migrations/0001_hydroshareresource.py index c1290911a..0c9474495 100644 --- a/src/mmw/apps/export/migrations/0001_hydroshareresource.py +++ b/src/mmw/apps/export/migrations/0001_hydroshareresource.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models import django.db.models.deletion diff --git a/src/mmw/apps/export/migrations/0002_hydroshare_disable_autosync.py b/src/mmw/apps/export/migrations/0002_hydroshare_disable_autosync.py index aeefd1a89..a8618ddba 100644 --- a/src/mmw/apps/export/migrations/0002_hydroshare_disable_autosync.py +++ b/src/mmw/apps/export/migrations/0002_hydroshare_disable_autosync.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/export/models.py b/src/mmw/apps/export/models.py index 32405fe52..a1857e8e3 100644 --- a/src/mmw/apps/export/models.py +++ b/src/mmw/apps/export/models.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from django.conf import settings from django.contrib.gis.db import models @@ -33,9 +29,9 @@ class HydroShareResource(models.Model): auto_now=True) def _url(self): - return '{}resource/{}'.format(HYDROSHARE_BASE_URL, self.resource) + return f'{HYDROSHARE_BASE_URL}resource/{self.resource}' url = property(_url) def __unicode__(self): - return '{} <{}>'.format(self.title, self.url) + return f'{self.title} <{self.url}>' diff --git a/src/mmw/apps/export/serializers.py b/src/mmw/apps/export/serializers.py index 07d932a4a..e9baf9cdd 100644 --- a/src/mmw/apps/export/serializers.py +++ b/src/mmw/apps/export/serializers.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from rest_framework import serializers -from models import HydroShareResource +from apps.export.models import HydroShareResource class HydroShareResourceSerializer(serializers.ModelSerializer): diff --git a/src/mmw/apps/export/tasks.py b/src/mmw/apps/export/tasks.py index 4079d9ffd..edf13224e 100644 --- a/src/mmw/apps/export/tasks.py +++ b/src/mmw/apps/export/tasks.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import fiona import io import json @@ -17,9 +13,9 @@ from apps.modeling.models import Project, Scenario from apps.modeling.tasks import to_gms_file -from hydroshare import HydroShareService -from models import HydroShareResource -from serializers import HydroShareResourceSerializer +from apps.export.hydroshare import HydroShareService +from apps.export.models import HydroShareResource +from apps.export.serializers import HydroShareResourceSerializer hss = HydroShareService() @@ -86,16 +82,15 @@ def create_resource(user_id, project_id, params): crs = {'no_defs': True, 'proj': 'longlat', 'ellps': 'WGS84', 'datum': 'WGS84'} schema = {'geometry': aoi_json['type'], 'properties': {}} - with fiona.open('/tmp/{}.shp'.format(resource), 'w', + with fiona.open(f'/tmp/{resource}.shp', 'w', driver='ESRI Shapefile', crs=crs, schema=schema) as shapefile: shapefile.write({'geometry': aoi_json, 'properties': {}}) for ext in SHAPEFILE_EXTENSIONS: - filename = '/tmp/{}.{}'.format(resource, ext) + filename = f'/tmp/{resource}.{ext}' with open(filename) as shapefile: - hs.addResourceFile(resource, shapefile, - 'area-of-interest.{}'.format(ext)) + hs.addResourceFile(resource, shapefile, f'area-of-interest.{ext}') os.remove(filename) # MapShed BMP Spreadsheet Tool @@ -167,7 +162,7 @@ def padep_worksheet(results): """ payload = [] - for k, v in results.iteritems(): + for k, v in results.items(): huc12_stream_length_km = sum( [c['lengthkm'] for c in v['huc12']['streams']['categories']]) huc12_stream_ag_pct = \ diff --git a/src/mmw/apps/export/urls.py b/src/mmw/apps/export/urls.py index 6ada634d4..af826683b 100644 --- a/src/mmw/apps/export/urls.py +++ b/src/mmw/apps/export/urls.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - -from django.conf.urls import url +from django.urls import re_path from apps.modeling.views import get_job from apps.modeling.urls import uuid_regex @@ -12,8 +8,8 @@ app_name = 'export' urlpatterns = [ - url(r'^hydroshare/?$', hydroshare, name='hydroshare'), - url(r'^shapefile/?$', shapefile, name='shapefile'), - url(r'^worksheet/?$', worksheet, name='worksheet'), - url(r'jobs/' + uuid_regex, get_job, name='get_job'), + re_path(r'^hydroshare/?$', hydroshare, name='hydroshare'), + re_path(r'^shapefile/?$', shapefile, name='shapefile'), + re_path(r'^worksheet/?$', worksheet, name='worksheet'), + re_path(r'jobs/' + uuid_regex, get_job, name='get_job'), ] diff --git a/src/mmw/apps/export/views.py b/src/mmw/apps/export/views.py index 48d3fc7a8..449bc23a0 100644 --- a/src/mmw/apps/export/views.py +++ b/src/mmw/apps/export/views.py @@ -1,18 +1,15 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import BMPxlsx import fiona import glob import json import os import shutil -import StringIO import tempfile import zipfile +from io import BytesIO + from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponse @@ -26,10 +23,10 @@ from apps.modeling.serializers import AoiSerializer from apps.geoprocessing_api.views import start_celery_job -from hydroshare import HydroShareService -from models import HydroShareResource -from serializers import HydroShareResourceSerializer -from tasks import create_resource, update_resource, padep_worksheet +from apps.export.hydroshare import HydroShareService +from apps.export.models import HydroShareResource +from apps.export.serializers import HydroShareResourceSerializer +from apps.export.tasks import create_resource, update_resource, padep_worksheet hss = HydroShareService() HYDROSHARE_BASE_URL = settings.HYDROSHARE['base_url'] @@ -141,16 +138,16 @@ def shapefile(request): try: # Write shapefiles - with fiona.open('{}/area-of-interest.shp'.format(tempdir), 'w', + with fiona.open(f'{tempdir}/area-of-interest.shp', 'w', driver='ESRI Shapefile', crs=crs, schema=schema) as sf: sf.write({'geometry': aoi_json, 'properties': {}}) - shapefiles = ['{}/area-of-interest.{}'.format(tempdir, ext) + shapefiles = [f'{tempdir}/area-of-interest.{ext}' for ext in SHAPEFILE_EXTENSIONS] # Create a zip file in memory from all the shapefiles - stream = StringIO.StringIO() + stream = BytesIO() with zipfile.ZipFile(stream, 'w') as zf: for fpath in shapefiles: _, fname = os.path.split(fpath) @@ -163,8 +160,7 @@ def shapefile(request): # Return the zip file from memory with appropriate headers resp = HttpResponse(stream.getvalue(), content_type='application/zip') - resp['Content-Disposition'] = 'attachment; '\ - 'filename="{}.zip"'.format(filename) + resp['Content-Disposition'] = f'attachment; filename="{filename}.zip"' return resp @@ -182,7 +178,7 @@ def worksheet(request): try: for item in items: - worksheet_path = '{}/{}.xlsx'.format(tempdir, item['name']) + worksheet_path = f'{tempdir}/{item["name"]}.xlsx' # Copy the Excel template shutil.copyfile(EXCEL_TEMPLATE, worksheet_path) @@ -194,16 +190,15 @@ def worksheet(request): # If geojson specified, write it to file if 'geojson' in item: - geojson_path = '{}/{}__Urban_Area.geojson'.format(tempdir, - item['name']) + geojson_path = f'{tempdir}/{item["name"]}__Urban_Area.geojson' with open(geojson_path, 'w') as geojson_file: json.dump(item['geojson'], geojson_file) - files = glob.glob('{}/*.*'.format(tempdir)) + files = glob.glob(f'{tempdir}/*.*') # Create a zip file in memory for all the files - stream = StringIO.StringIO() + stream = BytesIO() with zipfile.ZipFile(stream, 'w') as zf: for fpath in files: _, fname = os.path.split(fpath) diff --git a/src/mmw/apps/geocode/tests.py b/src/mmw/apps/geocode/tests.py index 98e133d26..c0a6928bc 100644 --- a/src/mmw/apps/geocode/tests.py +++ b/src/mmw/apps/geocode/tests.py @@ -1,10 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - -# NOTE Change to from urllib.parse import urlencode for Python 3 -from urllib import urlencode +from urllib.parse import urlencode from django.test import TestCase, Client from django.urls import reverse @@ -15,7 +10,7 @@ class GeocodeTestCase(TestCase): def assert_candidate_exists_for(self, address): c = Client() - url = '{}?{}'.format(self.SEARCH_URL, urlencode({'search': address})) + url = f'{self.SEARCH_URL}?{urlencode({"search": address})}' response = c.get(url).json() self.assertTrue(len(response) > 0, 'Expected ' diff --git a/src/mmw/apps/geocode/urls.py b/src/mmw/apps/geocode/urls.py index 9c4a126ac..553ea08f8 100644 --- a/src/mmw/apps/geocode/urls.py +++ b/src/mmw/apps/geocode/urls.py @@ -1,13 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - -from django.conf.urls import url +from django.urls import re_path from apps.geocode import views app_name = 'geocode' urlpatterns = [ - url(r'^$', views.geocode, name='geocode'), + re_path(r'^$', views.geocode, name='geocode'), ] diff --git a/src/mmw/apps/geocode/views.py b/src/mmw/apps/geocode/views.py index 4b7d705d8..5f4030473 100644 --- a/src/mmw/apps/geocode/views.py +++ b/src/mmw/apps/geocode/views.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - from django.conf import settings from rest_framework.response import Response diff --git a/src/mmw/apps/geoprocessing_api/calcs.py b/src/mmw/apps/geoprocessing_api/calcs.py index 79d97e2a6..ca4003718 100644 --- a/src/mmw/apps/geoprocessing_api/calcs.py +++ b/src/mmw/apps/geoprocessing_api/calcs.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import - import json import requests from operator import itemgetter @@ -39,7 +35,7 @@ def animal_population(geojson): aeu_for_geom = animal_energy_units(geom)[2] aeu_return_values = [] - for animal, aeu_value in aeu_for_geom.iteritems(): + for animal, aeu_value in aeu_for_geom.items(): aeu_return_values.append({ 'type': ANIMAL_DISPLAY_NAMES[animal], 'aeu': int(aeu_value), @@ -52,9 +48,9 @@ def animal_population(geojson): } -def stream_data(results, geojson): +def stream_data(results, geojson, datasource='nhdhr'): """ - Given a GeoJSON shape, retreive stream data from the `nhdflowline` table + Given a GeoJSON shape, retreive stream data from the specified table to display in the Analyze tab Returns a dictionary to append to outgoing JSON for analysis results. @@ -62,17 +58,31 @@ def stream_data(results, geojson): NULL_SLOPE = -9998.0 - sql = ''' - SELECT sum(lengthkm) as lengthkm, + if datasource not in settings.STREAM_TABLES: + raise Exception(f'Invalid stream datasource {datasource}') + + sql = f''' + WITH stream_intersection AS ( + SELECT ST_Length(ST_Transform( + ST_Intersection(geom, + ST_SetSRID(ST_GeomFromGeoJSON(%s), + 4326)), + 5070)) AS lengthm, + stream_order, + slope + FROM {settings.STREAM_TABLES[datasource]} + WHERE ST_Intersects(geom, + ST_SetSRID(ST_GeomFromGeoJSON(%s), 4326))) + + SELECT SUM(lengthm) / 1000 AS lengthkm, stream_order, - sum(lengthkm * NULLIF(slope, {NULL_SLOPE})) as slopesum - FROM nhdflowline - WHERE ST_Intersects(geom, ST_SetSRID(ST_GeomFromGeoJSON(%s), 4326)) + SUM(lengthm * NULLIF(slope, {NULL_SLOPE})) / 1000 AS slopesum + FROM stream_intersection GROUP BY stream_order; - '''.format(NULL_SLOPE=NULL_SLOPE) + ''' with connection.cursor() as cursor: - cursor.execute(sql, [geojson]) + cursor.execute(sql, [geojson, geojson]) if cursor.rowcount: columns = [col[0] for col in cursor.description] @@ -115,7 +125,7 @@ def calculate_avg_slope(slope, length): return { 'displayName': 'Streams', - 'name': 'streams', + 'name': f'streams_{datasource}', 'categories': sorted(list(stream_data.values()), key=itemgetter('order')), } @@ -133,13 +143,12 @@ def point_source_pollution(geojson): geom = GEOSGeometry(geojson, srid=4326) drb = geom.within(DRB) table_name = get_point_source_table(drb) - sql = ''' + sql = f''' SELECT city, state, npdes_id, mgd, kgn_yr, kgp_yr, latitude, - longitude, {facilityname} + longitude, {'facilityname' if drb else 'null'} FROM {table_name} WHERE ST_Intersects(geom, ST_SetSRID(ST_GeomFromText(%s), 4326)) - '''.format(facilityname='facilityname' if drb else 'null', - table_name=table_name) + ''' with connection.cursor() as cursor: cursor.execute(sql, [geom.wkt]) @@ -178,7 +187,7 @@ def catchment_water_quality(geojson): """ geom = GEOSGeometry(geojson, srid=4326) table_name = 'drb_catchment_water_quality' - sql = ''' + sql = f''' SELECT nord, areaha, tn_tot_kgy, tp_tot_kgy, tss_tot_kg, tn_urban_k, tn_riparia, tn_ag_kgyr, tn_natural, tn_pt_kgyr, tp_urban_k, tp_riparia, tp_ag_kgyr, tp_natural, tp_pt_kgyr, @@ -187,7 +196,7 @@ def catchment_water_quality(geojson): ST_AsGeoJSON(ST_Simplify(geom, 0.0003)) as geom FROM {table_name} WHERE ST_Intersects(geom, ST_SetSRID(ST_GeomFromText(%s), 4326)) - '''.format(table_name=table_name) + ''' with connection.cursor() as cursor: cursor.execute(sql, [geom.wkt]) @@ -318,16 +327,20 @@ def huc12s_with_aois(geojson): return matches -def streams_for_huc12s(huc12s, drb=False): +def streams_for_huc12s(huc12s, datasource='nhdhr'): """ Get MultiLineString of all streams in the given HUC-12s """ - sql = ''' - SELECT ST_AsGeoJSON(ST_Collect(ST_Force2D(s.geom))) - FROM {datasource} s INNER JOIN boundary_huc12 b + if datasource not in settings.STREAM_TABLES: + raise Exception(f'Invalid stream datasource {datasource}') + + sql = f''' + SELECT ST_AsGeoJSON(ST_Multi(s.geom)) + FROM {settings.STREAM_TABLES[datasource]} s + INNER JOIN boundary_huc12 b ON ST_Intersects(s.geom, b.geom_detailed) WHERE b.huc12 IN %s - '''.format(datasource='drb_streams_50' if drb else 'nhdflowline') + ''' with connection.cursor() as cursor: cursor.execute(sql, [tuple(huc12s)]) @@ -344,6 +357,6 @@ def drexel_fast_zonal(geojson, key): res.raise_for_status() # Select results for the given key - result = {int(k): v for k, v in res.json()[key].iteritems()} + result = {int(k): v for k, v in res.json()[key].items()} return result diff --git a/src/mmw/apps/geoprocessing_api/permissions.py b/src/mmw/apps/geoprocessing_api/permissions.py index aafbd768a..111b014ee 100644 --- a/src/mmw/apps/geoprocessing_api/permissions.py +++ b/src/mmw/apps/geoprocessing_api/permissions.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - from rest_framework import authentication from rest_framework.authtoken.serializers import AuthTokenSerializer diff --git a/src/mmw/apps/geoprocessing_api/schemas.py b/src/mmw/apps/geoprocessing_api/schemas.py index c04bb597d..993e9eef8 100644 --- a/src/mmw/apps/geoprocessing_api/schemas.py +++ b/src/mmw/apps/geoprocessing_api/schemas.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - from drf_yasg.openapi import ( Parameter, Schema, IN_PATH, IN_QUERY, @@ -11,6 +8,35 @@ from django.conf import settings +STREAM_DATASOURCE = Parameter( + 'datasource', + IN_PATH, + description='The stream datasource to query.' + ' Must be one of: "{}"'.format( + '", "'.join(settings.STREAM_TABLES.keys())), + type=TYPE_STRING, + required=True, +) + +nlcd_year_allowed_values = [ + '2019_2019', + '2019_2016', + '2019_2011', + '2019_2006', + '2019_2001', + '2011_2011', +] +NLCD_YEAR = Parameter( + 'nlcd_year', + IN_PATH, + description='The NLCD product version and target year to query.' + ' Must be one of: "{}"'.format( + '", "'.join(nlcd_year_allowed_values) + ), + type=TYPE_STRING, + required=True, +) + DRB_2100_LAND_KEY = Parameter( 'key', IN_PATH, diff --git a/src/mmw/apps/geoprocessing_api/tasks.py b/src/mmw/apps/geoprocessing_api/tasks.py index 3c94ea557..eda50be03 100644 --- a/src/mmw/apps/geoprocessing_api/tasks.py +++ b/src/mmw/apps/geoprocessing_api/tasks.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import - import os import logging -import urllib +from urllib.parse import urlencode from ast import literal_eval as make_tuple from calendar import month_name @@ -74,7 +70,7 @@ def start_rwd_job(location, snapping, simplify, data_source): if simplify is not False: params['simplify'] = simplify - query_string = urllib.urlencode(params) + query_string = urlencode(params) if query_string: rwd_url += ('?%s' % query_string) @@ -89,12 +85,12 @@ def start_rwd_job(location, snapping, simplify, data_source): @shared_task -def analyze_streams(results, area_of_interest): +def analyze_streams(results, area_of_interest, datasource='nhdhr'): """ Given geoprocessing results with stream data and an area of interest, returns the streams and stream order within it. """ - return {'survey': stream_data(results, area_of_interest)} + return {'survey': stream_data(results, area_of_interest, datasource)} @shared_task @@ -123,9 +119,9 @@ def analyze_catchment_water_quality(area_of_interest): @shared_task(throws=Exception) -def analyze_nlcd(result, area_of_interest=None): +def analyze_nlcd(result, area_of_interest=None, nlcd_year='2011_2011'): if 'error' in result: - raise Exception('[analyze_nlcd] {}'.format(result['error'])) + raise Exception(f'[analyze_nlcd_{nlcd_year}] {result["error"]}') pixel_width = aoi_resolution(area_of_interest) if area_of_interest else 1 @@ -139,7 +135,7 @@ def area(dictionary, key, default=0): return dictionary.get(key, default) * pixel_width * pixel_width # Convert results to histogram, calculate total - for key, count in result.iteritems(): + for key, count in result.items(): nlcd, ara = key total_count += count total_ara += count if ara == 1 else 0 @@ -147,7 +143,7 @@ def area(dictionary, key, default=0): has_ara = total_ara > 0 - for nlcd, (code, name) in layer_classmaps.NLCD.iteritems(): + for nlcd, (code, name) in layer_classmaps.NLCD.items(): categories.append({ 'area': area(histogram, nlcd), 'active_river_area': area(result, (nlcd, 1)) if has_ara else None, @@ -159,8 +155,9 @@ def area(dictionary, key, default=0): return { 'survey': { - 'name': 'land', - 'displayName': 'Land', + 'name': f'land_{nlcd_year}', + 'displayName': + f'Land Use/Cover {nlcd_year[5:]} (NLCD{nlcd_year[2:4]})', 'categories': categories, } } @@ -169,7 +166,7 @@ def area(dictionary, key, default=0): @shared_task(throws=Exception) def analyze_soil(result, area_of_interest=None): if 'error' in result: - raise Exception('[analyze_soil] {}'.format(result['error'])) + raise Exception(f'[analyze_soil] {result["error"]}') pixel_width = aoi_resolution(area_of_interest) if area_of_interest else 1 @@ -178,13 +175,13 @@ def analyze_soil(result, area_of_interest=None): categories = [] # Convert results to histogram, calculate total - for key, count in result.iteritems(): + for key, count in result.items(): total_count += count s = make_tuple(key[4:]) # Change {"List(1)":5} to {1:5} s = s if s != settings.NODATA else 3 # Map NODATA to 3 histogram[s] = count + histogram.get(s, 0) - for soil, (code, name) in layer_classmaps.SOIL.iteritems(): + for soil, (code, name) in layer_classmaps.SOIL.items(): categories.append({ 'area': histogram.get(soil, 0) * pixel_width * pixel_width, 'code': code, @@ -217,7 +214,7 @@ def analyze_climate(result, wkaoi): used for sorting purposes on the client side. """ if 'error' in result: - raise Exception('[analyze_climate] {}'.format(result['error'])) + raise Exception(f'[analyze_climate] {result["error"]}') ppt = {k[5:]: v['List(0)'] for k, v in result[wkaoi].items() if 'ppt' in k} @@ -229,7 +226,7 @@ def analyze_climate(result, wkaoi): 'month': month_name[i], 'ppt': ppt[str(i)] * CM_PER_MM, 'tmean': tmean[str(i)], - } for i in xrange(1, 13)] + } for i in range(1, 13)] return { 'survey': { @@ -282,7 +279,7 @@ def analyze_terrain(result): which has Elevation in m and keeps Slope in %. """ if 'error' in result: - raise Exception('[analyze_terrain] {}'.format(result['error'])) + raise Exception(f'[analyze_terrain] {result["error"]}') [elevation, slope] = result @@ -313,7 +310,7 @@ def cm_to_m(x): @shared_task def analyze_protected_lands(result, area_of_interest=None): if 'error' in result: - raise Exception('[analyze_protected_lands] {}'.format(result['error'])) + raise Exception(f'[analyze_protected_lands] {result["error"]}') pixel_width = aoi_resolution(area_of_interest) if area_of_interest else 1 @@ -322,11 +319,11 @@ def analyze_protected_lands(result, area_of_interest=None): total_count = 0 categories = [] - for key, count in result.iteritems(): + for key, count in result.items(): total_count += count histogram[key] = count + histogram.get(key, 0) - for class_id, (code, name) in layer_classmaps.PROTECTED_LANDS.iteritems(): + for class_id, (code, name) in layer_classmaps.PROTECTED_LANDS.items(): categories.append({ 'area': histogram.get(class_id, 0) * pixel_width * pixel_width, 'class_id': class_id, @@ -351,11 +348,11 @@ def analyze_drb_2100_land(area_of_interest, key): total_count = 0 categories = [] - for nlcd, count in result.iteritems(): + for nlcd, count in result.items(): total_count += count histogram[nlcd] = count + histogram.get(nlcd, 0) - for nlcd, (code, name) in layer_classmaps.NLCD.iteritems(): + for nlcd, (code, name) in layer_classmaps.NLCD.items(): categories.append({ 'area': histogram.get(nlcd, 0), 'code': code, @@ -366,8 +363,8 @@ def analyze_drb_2100_land(area_of_interest, key): return { 'survey': { - 'name': 'drb_2100_land_{}'.format(key), - 'displayName': 'DRB 2100 land forecast ({})'.format(key), + 'name': f'drb_2100_land_{key}', + 'displayName': f'DRB 2100 land forecast ({key})', 'categories': categories, } } @@ -384,7 +381,7 @@ def collect_nlcd(histogram, geojson=None): 'code': code, 'nlcd': nlcd, 'type': name, - } for nlcd, (code, name) in layer_classmaps.NLCD.iteritems()] + } for nlcd, (code, name) in layer_classmaps.NLCD.items()] return {'categories': categories} @@ -398,8 +395,7 @@ def collect_worksheet_aois(result, shapes): their processed results. """ if 'error' in result: - raise Exception('[collect_worksheet_aois] {}' - .format(result['error'])) + raise Exception(f'[collect_worksheet_aois] {result["error"]}') NULL_RESULT = {'nlcd_streams': {}, 'nlcd': {}} collection = {} @@ -423,8 +419,7 @@ def collect_worksheet_wkaois(result, shapes): modeled results, and also the processed NLCD and NLCD+Streams. """ if 'error' in result: - raise Exception('[collect_worksheet_wkaois] {}' - .format(result['error'])) + raise Exception(f'[collect_worksheet_wkaois] {result["error"]}') collection = {} @@ -461,12 +456,12 @@ def collect_worksheet(area_of_interest): worksheet containing these values, which can be used for further modeling. """ def to_aoi_id(m): - return '{}-{}'.format(NOCACHE, m['wkaoi']) + return f'{NOCACHE}-{m["wkaoi"]}' matches = huc12s_with_aois(area_of_interest) huc12_ids = [m['huc12'] for m in matches] - streams = streams_for_huc12s(huc12_ids)[0] + streams = streams_for_huc12s(huc12_ids) aoi_shapes = [{ 'id': to_aoi_id(m), @@ -487,7 +482,7 @@ def to_aoi_id(m): collection = {} for m in matches: - filename = '{}__{}'.format(m['huc12'], m['name'].replace(' ', '_')) + filename = f'{m["huc12"]}__{m["name"].replace(" ", "_")}' collection[filename] = { 'name': m['name'], 'aoi': aoi_results.get(to_aoi_id(m), {}), diff --git a/src/mmw/apps/geoprocessing_api/tests.py b/src/mmw/apps/geoprocessing_api/tests.py index c311bc4c4..4f46d677c 100644 --- a/src/mmw/apps/geoprocessing_api/tests.py +++ b/src/mmw/apps/geoprocessing_api/tests.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import json +from unittest import skip + from django.test import (Client, TestCase, LiveServerTestCase) @@ -157,38 +155,10 @@ def test_survey_land_only(self): expected = { "survey": { - "displayName": "Land", - "name": "land", + "displayName": "Land Use/Cover 2011 (NLCD11)", + "name": "land_2011_2011", "categories": [ { - "code": "mixed_forest", - "active_river_area": None, - "area": 35, - "nlcd": 43, - "coverage": 4.2627666817284424e-05, - "type": "Mixed Forest" - }, { - "code": "grassland", - "active_river_area": None, - "area": 3228, - "nlcd": 71, - "coverage": 0.00393148881389126, - "type": "Grassland/Herbaceous" - }, { - "code": "deciduous_forest", - "active_river_area": None, - "area": 0, - "nlcd": 41, - "coverage": 0.0, - "type": "Deciduous Forest" - }, { - "code": "evergreen_forest", - "active_river_area": None, - "area": 5758, - "nlcd": 42, - "coverage": 0.007012860158112106, - "type": "Evergreen Forest" - }, { "code": "open_water", "active_river_area": None, "area": 279, @@ -202,27 +172,6 @@ def test_survey_land_only(self): "nlcd": 12, "coverage": 0.0, "type": "Perennial Ice/Snow" - }, { - "code": "pasture", - "active_river_area": None, - "area": 57, - "nlcd": 81, - "coverage": 6.942220024529177e-05, - "type": "Pasture/Hay" - }, { - "code": "cultivated_crops", - "active_river_area": None, - "area": 682, - "nlcd": 82, - "coverage": 0.0008306305362682279, - "type": "Cultivated Crops" - }, { - "code": "shrub", - "active_river_area": None, - "area": 499636, - "nlcd": 52, - "coverage": 0.6085233410834492, - "type": "Shrub/Scrub" }, { "code": "developed_open", "active_river_area": None, @@ -251,6 +200,62 @@ def test_survey_land_only(self): "nlcd": 24, "coverage": 0.025234360822494743, "type": "Developed, High Intensity" + }, { + "code": "barren_land", + "active_river_area": None, + "area": 25, + "nlcd": 31, + "coverage": 3.0448333440917446e-05, + "type": "Barren Land (Rock/Sand/Clay)" + }, { + "code": "deciduous_forest", + "active_river_area": None, + "area": 0, + "nlcd": 41, + "coverage": 0.0, + "type": "Deciduous Forest" + }, { + "code": "evergreen_forest", + "active_river_area": None, + "area": 5758, + "nlcd": 42, + "coverage": 0.007012860158112106, + "type": "Evergreen Forest" + }, { + "code": "mixed_forest", + "active_river_area": None, + "area": 35, + "nlcd": 43, + "coverage": 4.2627666817284424e-05, + "type": "Mixed Forest" + }, { + "code": "shrub", + "active_river_area": None, + "area": 499636, + "nlcd": 52, + "coverage": 0.6085233410834492, + "type": "Shrub/Scrub" + }, { + "code": "grassland", + "active_river_area": None, + "area": 3228, + "nlcd": 71, + "coverage": 0.00393148881389126, + "type": "Grassland/Herbaceous" + }, { + "code": "pasture", + "active_river_area": None, + "area": 57, + "nlcd": 81, + "coverage": 6.942220024529177e-05, + "type": "Pasture/Hay" + }, { + "code": "cultivated_crops", + "active_river_area": None, + "area": 682, + "nlcd": 82, + "coverage": 0.0008306305362682279, + "type": "Cultivated Crops" }, { "code": "woody_wetlands", "active_river_area": None, @@ -265,19 +270,12 @@ def test_survey_land_only(self): "nlcd": 95, "coverage": 0.00019365140068423496, "type": "Emergent Herbaceous Wetlands" - }, { - "code": "barren_land", - "active_river_area": None, - "area": 25, - "nlcd": 31, - "coverage": 3.0448333440917446e-05, - "type": "Barren Land (Rock/Sand/Clay)" } ] } } - actual = tasks.analyze_nlcd(histogram) + actual = tasks.analyze_nlcd(histogram, nlcd_year='2011_2011') self.assertEqual(actual, expected) def test_survey_land_with_ara(self): @@ -318,38 +316,6 @@ def test_survey_land_with_ara(self): expected = { "survey": { "categories": [ - { - "active_river_area": 117, - "area": 329, - "code": "mixed_forest", - "coverage": 0.002653825057270997, - "nlcd": 43, - "type": "Mixed Forest" - }, - { - "active_river_area": 260, - "area": 684, - "code": "grassland", - "coverage": 0.005517374891104443, - "nlcd": 71, - "type": "Grassland/Herbaceous" - }, - { - "active_river_area": 7254, - "area": 19218, - "code": "deciduous_forest", - "coverage": 0.1550188752298906, - "nlcd": 41, - "type": "Deciduous Forest" - }, - { - "active_river_area": 15, - "area": 153, - "code": "evergreen_forest", - "coverage": 0.001234149646694415, - "nlcd": 42, - "type": "Evergreen Forest" - }, { "active_river_area": 34, "area": 39, @@ -366,30 +332,6 @@ def test_survey_land_with_ara(self): "nlcd": 12, "type": "Perennial Ice/Snow" }, - { - "active_river_area": 2108, - "area": 8922, - "code": "pasture", - "coverage": 0.07196786371116058, - "nlcd": 81, - "type": "Pasture/Hay" - }, - { - "active_river_area": 1632, - "area": 6345, - "code": "cultivated_crops", - "coverage": 0.051180911818797796, - "nlcd": 82, - "type": "Cultivated Crops" - }, - { - "active_river_area": 963, - "area": 3309, - "code": "shrub", - "coverage": 0.026691510986351755, - "nlcd": 52, - "type": "Shrub/Scrub" - }, { "active_river_area": 9330, "area": 40558, @@ -422,6 +364,70 @@ def test_survey_land_with_ara(self): "nlcd": 24, "type": "Developed, High Intensity" }, + { + "active_river_area": 132, + "area": 364, + "code": "barren_land", + "coverage": 0.0029361468718742943, + "nlcd": 31, + "type": "Barren Land (Rock/Sand/Clay)" + }, + { + "active_river_area": 7254, + "area": 19218, + "code": "deciduous_forest", + "coverage": 0.1550188752298906, + "nlcd": 41, + "type": "Deciduous Forest" + }, + { + "active_river_area": 15, + "area": 153, + "code": "evergreen_forest", + "coverage": 0.001234149646694415, + "nlcd": 42, + "type": "Evergreen Forest" + }, + { + "active_river_area": 117, + "area": 329, + "code": "mixed_forest", + "coverage": 0.002653825057270997, + "nlcd": 43, + "type": "Mixed Forest" + }, + { + "active_river_area": 963, + "area": 3309, + "code": "shrub", + "coverage": 0.026691510986351755, + "nlcd": 52, + "type": "Shrub/Scrub" + }, + { + "active_river_area": 260, + "area": 684, + "code": "grassland", + "coverage": 0.005517374891104443, + "nlcd": 71, + "type": "Grassland/Herbaceous" + }, + { + "active_river_area": 2108, + "area": 8922, + "code": "pasture", + "coverage": 0.07196786371116058, + "nlcd": 81, + "type": "Pasture/Hay" + }, + { + "active_river_area": 1632, + "area": 6345, + "code": "cultivated_crops", + "coverage": 0.051180911818797796, + "nlcd": 82, + "type": "Cultivated Crops" + }, { "active_river_area": 3756, "area": 3940, @@ -438,21 +444,13 @@ def test_survey_land_with_ara(self): "nlcd": 95, "type": "Emergent Herbaceous Wetlands" }, - { - "active_river_area": 132, - "area": 364, - "code": "barren_land", - "coverage": 0.0029361468718742943, - "nlcd": 31, - "type": "Barren Land (Rock/Sand/Clay)" - } ], - "displayName": "Land", - "name": "land" + "displayName": "Land Use/Cover 2011 (NLCD11)", + "name": "land_2011_2011" } } - actual = tasks.analyze_nlcd(histogram) + actual = tasks.analyze_nlcd(histogram, nlcd_year='2011_2011') self.assertEqual(actual, expected) def test_survey_soil(self): @@ -757,6 +755,7 @@ def test_sq_km_aoi(self): self.assertTrue(calcs.catchment_intersects_aoi(reprojected_aoi, contained_catchment)) + @skip('Disabling until Django Upgrade #3419') def test_hundred_sq_km_aoi(self): aoi = GEOSGeometry(json.dumps({ "type": "Polygon", @@ -878,6 +877,7 @@ def test_hundred_sq_km_aoi(self): self.assertTrue(calcs.catchment_intersects_aoi(reprojected_aoi, contained_catchment)) + @skip('Disabling until Django Upgrade #3419') def test_thousand_sq_km_aoi(self): aoi = GEOSGeometry(json.dumps({ "type": "Polygon", @@ -999,6 +999,7 @@ def test_thousand_sq_km_aoi(self): self.assertTrue(calcs.catchment_intersects_aoi(reprojected_aoi, contained_catchment)) + @skip('Disabling until Django Upgrade #3419') def test_ten_thousand_sq_km_aoi(self): aoi = GEOSGeometry(json.dumps({ "type": "Polygon", diff --git a/src/mmw/apps/geoprocessing_api/throttling.py b/src/mmw/apps/geoprocessing_api/throttling.py index e8015cb5b..6b3c30752 100644 --- a/src/mmw/apps/geoprocessing_api/throttling.py +++ b/src/mmw/apps/geoprocessing_api/throttling.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - from django.conf import settings from rest_framework.throttling import UserRateThrottle diff --git a/src/mmw/apps/geoprocessing_api/urls.py b/src/mmw/apps/geoprocessing_api/urls.py index 4ede72462..1fc7598f2 100644 --- a/src/mmw/apps/geoprocessing_api/urls.py +++ b/src/mmw/apps/geoprocessing_api/urls.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - -from django.conf.urls import url +from django.urls import re_path from apps.modeling.views import get_job from apps.modeling.urls import uuid_regex @@ -12,32 +8,33 @@ app_name = 'geoprocessing_api' urlpatterns = [ - url(r'^token/', views.get_auth_token, - name="authtoken"), - url(r'analyze/land/$', views.start_analyze_land, - name='start_analyze_land'), - url(r'analyze/soil/$', views.start_analyze_soil, - name='start_analyze_soil'), - url(r'analyze/animals/$', views.start_analyze_animals, - name='start_analyze_animals'), - url(r'analyze/pointsource/$', views.start_analyze_pointsource, - name='start_analyze_pointsource'), - url(r'analyze/catchment-water-quality/$', - views.start_analyze_catchment_water_quality, - name='start_analyze_catchment_water_quality'), - url(r'analyze/climate/$', views.start_analyze_climate, - name='start_analyze_climate'), - url(r'analyze/streams/$', views.start_analyze_streams, - name='start_analyze_streams'), - url(r'analyze/terrain/$', views.start_analyze_terrain, - name='start_analyze_terrain'), - url(r'analyze/protected-lands/$', views.start_analyze_protected_lands, - name='start_analyze_protected_lands'), - url(r'analyze/drb-2100-land/(?P\w+)/$', - views.start_analyze_drb_2100_land, - name='start_analyze_drb_2100_land'), - url(r'jobs/' + uuid_regex, get_job, name='get_job'), - url(r'modeling/worksheet/$', views.start_modeling_worksheet, - name='start_modeling_worksheet'), - url(r'watershed/$', views.start_rwd, name='start_rwd'), + re_path(r'^token/', views.get_auth_token, + name="authtoken"), + re_path(r'analyze/land/(?P\w+)/?$', views.start_analyze_land, + name='start_analyze_land'), + re_path(r'analyze/soil/$', views.start_analyze_soil, + name='start_analyze_soil'), + re_path(r'analyze/animals/$', views.start_analyze_animals, + name='start_analyze_animals'), + re_path(r'analyze/pointsource/$', views.start_analyze_pointsource, + name='start_analyze_pointsource'), + re_path(r'analyze/catchment-water-quality/$', + views.start_analyze_catchment_water_quality, + name='start_analyze_catchment_water_quality'), + re_path(r'analyze/climate/$', views.start_analyze_climate, + name='start_analyze_climate'), + re_path(r'analyze/streams/(?P\w+)/?$', + views.start_analyze_streams, + name='start_analyze_streams'), + re_path(r'analyze/terrain/$', views.start_analyze_terrain, + name='start_analyze_terrain'), + re_path(r'analyze/protected-lands/$', views.start_analyze_protected_lands, + name='start_analyze_protected_lands'), + re_path(r'analyze/drb-2100-land/(?P\w+)/$', + views.start_analyze_drb_2100_land, + name='start_analyze_drb_2100_land'), + re_path(r'jobs/' + uuid_regex, get_job, name='get_job'), + re_path(r'modeling/worksheet/$', views.start_modeling_worksheet, + name='start_modeling_worksheet'), + re_path(r'watershed/$', views.start_rwd, name='start_rwd'), ] diff --git a/src/mmw/apps/geoprocessing_api/validation.py b/src/mmw/apps/geoprocessing_api/validation.py index a60c3748a..314731bb5 100644 --- a/src/mmw/apps/geoprocessing_api/validation.py +++ b/src/mmw/apps/geoprocessing_api/validation.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from numbers import Number from rest_framework.exceptions import ValidationError @@ -29,12 +25,12 @@ def validate_rwd(location, data_source, snapping, simplify): def create_invalid_rwd_location_format_error_msg(loc): - return ('Invalid required `location` parameter value `{}`. Must be a ' - '`[lat, lng] where `lat` and `lng` are numeric.'.format(loc)) + return (f'Invalid required `location` parameter value `{loc}`. Must be a ' + '`[lat, lng] where `lat` and `lng` are numeric.') def check_location_format(loc): - if loc is None or type(loc) is not list or len(loc) is not 2: + if loc is None or type(loc) is not list or len(loc) != 2: return False else: [lat, lng] = loc @@ -43,8 +39,8 @@ def check_location_format(loc): def create_invalid_rwd_data_source_error_msg(source): - return ('Invalid optional `dataSource` parameter value `{}`. Must be ' - '`drb` or `nhd`.'.format(source)) + return (f'Invalid optional `dataSource` parameter value `{source}`.' + ' Must be `drb` or `nhd`.') def check_rwd_data_source(source): @@ -52,13 +48,13 @@ def check_rwd_data_source(source): def create_invalid_rwd_snapping_error_msg(snapping): - return ('Invalid optional `snappingOn` parameter value `{}`. Must be ' - '`true` or `false`.'.format(snapping)) + return (f'Invalid optional `snappingOn` parameter value `{snapping}`.' + ' Must be `true` or `false`.') def create_invalid_rwd_simplify_param_type_error_msg(simplify): - return ('Invalid optional `simplify` parameter value: `{}`. Must be a ' - 'number.'.format(simplify)) + return (f'Invalid optional `simplify` parameter value: `{simplify}`.' + ' Must be a number.') def check_rwd_simplify_param_type(simplify): diff --git a/src/mmw/apps/geoprocessing_api/views.py b/src/mmw/apps/geoprocessing_api/views.py index 708682cc6..c22c5922f 100644 --- a/src/mmw/apps/geoprocessing_api/views.py +++ b/src/mmw/apps/geoprocessing_api/views.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - from celery import chain from rest_framework.response import Response from rest_framework import decorators, status from rest_framework.authentication import (TokenAuthentication, SessionAuthentication) +from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated from rest_framework.authtoken.models import Token from drf_yasg.utils import swagger_auto_schema @@ -103,9 +101,7 @@ def start_rwd(request, format=None): this point is automatically delineated using the 10m resolution national elevation model or the 30m resolution flow direction grid. - For more information, see the - [technical documentation](https://wikiwatershed.org/ - documentation/mmw-tech/#delineate-watershed). + For more information, see the [technical documentation](https://wikiwatershed.org/documentation/mmw-tech/#delineate-watershed). # NOQA ## Request Body @@ -240,7 +236,8 @@ def start_rwd(request, format=None): @swagger_auto_schema(method='post', - manual_parameters=[schemas.WKAOI], + manual_parameters=[schemas.NLCD_YEAR, + schemas.WKAOI], request_body=schemas.MULTIPOLYGON, responses={200: schemas.JOB_STARTED_RESPONSE}) @decorators.api_view(['POST']) @@ -249,15 +246,15 @@ def start_rwd(request, format=None): @decorators.permission_classes((IsAuthenticated, )) @decorators.throttle_classes([BurstRateThrottle, SustainedRateThrottle]) @log_request -def start_analyze_land(request, format=None): +def start_analyze_land(request, nlcd_year, format=None): """ - Starts a job to produce a land-use histogram for a given area. + Starts a job to produce a land-use histogram for a given area and year. - Uses the National Land Cover Database (NLCD 2011) + Supports the years 2019, 2016, 2011, 2006, and 2001 from the NLCD 2019 + product. Also supports the year 2011 from the NLCD 2011 product, which + used to be the default previously. - For more information, see the - [technical documentation](https://wikiwatershed.org/ - documentation/mmw-tech/#overlays-tab-coverage). + For more information, see the [technical documentation](https://wikiwatershed.org/documentation/mmw-tech/#overlays-tab-coverage). # NOQA ## Response @@ -272,8 +269,8 @@ def start_analyze_land(request, format=None): { "survey": { - "displayName": "Land", - "name": "land", + "displayName": "Land Use/Cover 2019 (NLCD19)", + "name": "land_2019_2019", "categories": [ { "nlcd": 43, @@ -414,9 +411,18 @@ def start_analyze_land(request, format=None): geop_input = {'polygon': [area_of_interest]} + layer_overrides = {} + if nlcd_year == '2011_2011': + layer_overrides['__LAND__'] = 'nlcd-2011-30m-epsg5070-512-int8' + + nlcd, year = nlcd_year.split('_') + if nlcd == '2019' and year in ['2019', '2016', '2011', '2006', '2001']: + layer_overrides['__LAND__'] = f'nlcd-{year}-30m-epsg5070-512-byte' + return start_celery_job([ - geoprocessing.run.s('nlcd_ara', geop_input, wkaoi), - tasks.analyze_nlcd.s(area_of_interest) + geoprocessing.run.s('nlcd_ara', geop_input, wkaoi, + layer_overrides=layer_overrides), + tasks.analyze_nlcd.s(area_of_interest, nlcd_year) ], area_of_interest, user) @@ -436,9 +442,7 @@ def start_analyze_soil(request, format=None): Uses the Hydrologic Soil Groups From USDA gSSURGO 2016 - For more information, see the - [technical documentation](https://wikiwatershed.org/ - documentation/mmw-tech/#overlays-tab-coverage). + For more information, see the [technical documentation](https://wikiwatershed.org/documentation/mmw-tech/#overlays-tab-coverage). # NOQA ## Response @@ -516,7 +520,8 @@ def start_analyze_soil(request, format=None): @swagger_auto_schema(method='post', - manual_parameters=[schemas.WKAOI], + manual_parameters=[schemas.STREAM_DATASOURCE, + schemas.WKAOI], request_body=schemas.MULTIPOLYGON, responses={200: schemas.JOB_STARTED_RESPONSE}) @decorators.api_view(['POST']) @@ -525,14 +530,12 @@ def start_analyze_soil(request, format=None): @decorators.permission_classes((IsAuthenticated, )) @decorators.throttle_classes([BurstRateThrottle, SustainedRateThrottle]) @log_request -def start_analyze_streams(request, format=None): +def start_analyze_streams(request, datasource, format=None): """ Starts a job to display streams & stream order within a given area of interest. - For more information, see - the [technical documentation](https://wikiwatershedorg/documentation/ - mmw-tech/#additional-data-layers) + For more information, see the [technical documentation](https://wikiwatershedorg/documentation/mmw-tech/#additional-data-layers) # NOQA ## Response @@ -549,7 +552,7 @@ def start_analyze_streams(request, format=None): { "survey": { "displayName": "Streams", - "name": "streams", + "name": "streams_nhd", "categories": [ { "lengthkm": 2.598, @@ -637,12 +640,19 @@ def start_analyze_streams(request, format=None): user = request.user if request.user.is_authenticated else None area_of_interest, wkaoi = _parse_input(request) + if datasource not in settings.STREAM_TABLES: + raise ValidationError(f'Invalid stream datasource: {datasource}.' + ' Must be one of: "{}".'.format( + '", "'.join(settings.STREAM_TABLES.keys()))) + return start_celery_job([ geoprocessing.run.s('nlcd_streams', {'polygon': [area_of_interest], - 'vector': streams(area_of_interest)}, wkaoi), + 'vector': streams(area_of_interest, datasource)}, + wkaoi, + cache_key=datasource), nlcd_streams.s(), - tasks.analyze_streams.s(area_of_interest) + tasks.analyze_streams.s(area_of_interest, datasource) ], area_of_interest, user) @@ -662,9 +672,7 @@ def start_analyze_animals(request, format=None): Source USDA - For more information, see - the [technical documentation](https://wikiwatershed.org/documentation/ - mmw-tech/#additional-data-layers) + For more information, see the [technical documentation](https://wikiwatershed.org/documentation/mmw-tech/#additional-data-layers) # NOQA ## Response @@ -744,9 +752,7 @@ def start_analyze_pointsource(request, format=None): Source EPA NPDES - For more information, see the - [technical documentation](https://wikiwatershed.org/ - documentation/mmw-tech/#additional-data-layers) + For more information, see the [technical documentation](https://wikiwatershed.org/documentation/mmw-tech/#additional-data-layers) # NOQA ## Response @@ -807,9 +813,7 @@ def start_analyze_catchment_water_quality(request, format=None): Source Stream Reach Tool Assessment (SRAT) - For more information, see - the [technical documentation](https://wikiwatershed.org/ - documentation/mmw-tech/#overlays-tab-coverage) + For more information, see the [technical documentation](https://wikiwatershed.org/documentation/mmw-tech/#overlays-tab-coverage) # NOQA ## Response @@ -895,9 +899,7 @@ def start_analyze_climate(request, format=None): Source PRISM Climate Group - For more information, see - the [technical documentation](https://wikiwatershed.org/ - documentation/mmw-tech/#overlays-tab-coverage) + For more information, see the [technical documentation](https://wikiwatershed.org/documentation/mmw-tech/#overlays-tab-coverage) # NOQA ## Response @@ -961,9 +963,7 @@ def start_analyze_terrain(request, format=None): Source NHDPlus V2 NEDSnapshot DEM - For more information, see the - [technical documentation](https://wikiwatershed.org/ - documentation/mmw-tech/#overlays-tab-coverage). + For more information, see the [technical documentation](https://wikiwatershed.org/documentation/mmw-tech/#overlays-tab-coverage). # NOQA ## Response @@ -1030,9 +1030,7 @@ def start_analyze_protected_lands(request, format=None): Uses the Protected Areas Database of the United States (PADUS), published by the U.S. Geological Survey Gap Analysis Program in 2016. - For more information, see the - [technical documentation](https://wikiwatershed.org/ - documentation/mmw-tech/#overlays-tab-coverage). + For more information, see the [technical documentation](https://wikiwatershed.org/documentation/mmw-tech/#overlays-tab-coverage). # NOQA ## Response @@ -1172,9 +1170,7 @@ def start_analyze_drb_2100_land(request, key=None, format=None): Shippensburg University, serviced via APIs by Drexel University and the Academy of Natural Sciences. - For more information, see the - [technical documentation](https://wikiwatershed.org/ - documentation/mmw-tech/#overlays-tab-coverage). + For more information, see the [technical documentation](https://wikiwatershed.org/documentation/mmw-tech/#overlays-tab-coverage). # NOQA ## Response diff --git a/src/mmw/apps/home/admin.py b/src/mmw/apps/home/admin.py index 3bd751f43..895772c19 100644 --- a/src/mmw/apps/home/admin.py +++ b/src/mmw/apps/home/admin.py @@ -1,6 +1,3 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division # Register your models here. diff --git a/src/mmw/apps/home/models.py b/src/mmw/apps/home/models.py index c6304cd62..40a96afc6 100644 --- a/src/mmw/apps/home/models.py +++ b/src/mmw/apps/home/models.py @@ -1,4 +1 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division diff --git a/src/mmw/apps/home/routes.py b/src/mmw/apps/home/routes.py index c6304cd62..40a96afc6 100644 --- a/src/mmw/apps/home/routes.py +++ b/src/mmw/apps/home/routes.py @@ -1,4 +1 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division diff --git a/src/mmw/apps/home/tests.py b/src/mmw/apps/home/tests.py index 364854ed4..7b30eb902 100644 --- a/src/mmw/apps/home/tests.py +++ b/src/mmw/apps/home/tests.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from django.contrib.auth.models import User from django.test import TestCase diff --git a/src/mmw/apps/home/urls.py b/src/mmw/apps/home/urls.py index b360be94d..0c52ce329 100644 --- a/src/mmw/apps/home/urls.py +++ b/src/mmw/apps/home/urls.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - -from django.conf.urls import url +from django.urls import re_path from apps.home.views import ( home_page, project, @@ -16,27 +12,28 @@ app_name = 'home' urlpatterns = [ - url(r'^$', home_page, name='home_page'), - url(r'^draw/?$', home_page, name='home_page'), - url(r'^account/?$', home_page, name='account'), - url(r'^projects/$', projects, name='projects'), - url(r'^project/$', project, name='project'), - url(r'^project/new/', project, name='project'), - url(r'^project/(?P[0-9]+)/$', project, name='project'), - url(r'^project/(?P[0-9]+)/clone/?$', - project_clone, name='project_clone'), - url(r'^project/(?P[0-9]+)/scenario/(?P[0-9]+)/$', - project, name='project'), - url(r'^project/compare/$', project, name='project'), - url(r'^project/(?P[0-9]+)/compare/$', project, name='project'), - url(r'^project/via/hydroshare/(?P\w+)/?$', - project_via_hydroshare_open, name='project_via_hydroshare_open'), - url(r'^project/via/hydroshare/(?P\w+)/open/?$', - project_via_hydroshare_open, name='project_via_hydroshare_open'), - url(r'^project/via/hydroshare/(?P\w+)/edit/?$', - project_via_hydroshare_edit, name='project_via_hydroshare_edit'), - url(r'^analyze$', home_page, name='analyze'), - url(r'^search$', home_page, name='search'), - url(r'^error', home_page, name='error'), - url(r'^sign-up', home_page, name='sign_up'), + re_path(r'^$', home_page, name='home_page'), + re_path(r'^draw/?$', home_page, name='home_page'), + re_path(r'^account/?$', home_page, name='account'), + re_path(r'^projects/$', projects, name='projects'), + re_path(r'^project/$', project, name='project'), + re_path(r'^project/new/', project, name='project'), + re_path(r'^project/(?P[0-9]+)/$', project, name='project'), + re_path(r'^project/(?P[0-9]+)/clone/?$', + project_clone, name='project_clone'), + re_path(r'^project/(?P[0-9]+)/scenario/(?P[0-9]+)/$', + project, name='project'), + re_path(r'^project/compare/$', project, name='project'), + re_path(r'^project/(?P[0-9]+)/compare/$', + project, name='project'), + re_path(r'^project/via/hydroshare/(?P\w+)/?$', + project_via_hydroshare_open, name='project_via_hydroshare_open'), + re_path(r'^project/via/hydroshare/(?P\w+)/open/?$', + project_via_hydroshare_open, name='project_via_hydroshare_open'), + re_path(r'^project/via/hydroshare/(?P\w+)/edit/?$', + project_via_hydroshare_edit, name='project_via_hydroshare_edit'), + re_path(r'^analyze$', home_page, name='analyze'), + re_path(r'^search$', home_page, name='search'), + re_path(r'^error', home_page, name='error'), + re_path(r'^sign-up', home_page, name='sign_up'), ] diff --git a/src/mmw/apps/home/views.py b/src/mmw/apps/home/views.py index 35b3faa9d..ac4106ae3 100644 --- a/src/mmw/apps/home/views.py +++ b/src/mmw/apps/home/views.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import json import requests -from urlparse import urljoin +from urllib.parse import urljoin from copy import deepcopy from hs_restclient import HydroShareNotAuthorized @@ -91,7 +87,7 @@ def project_clone(request, proj_id=None): scenario.project = project scenario.save() - return redirect('/project/{0}'.format(project.id)) + return redirect(f'/project/{project.id}') def _via_hydroshare(request, resource, callback, errback): @@ -158,7 +154,7 @@ def project_via_hydroshare_open(request, resource): """Redirect to project given a HydroShare resource, if found.""" def callback(project_id): - return redirect('/project/{}/'.format(project_id)) + return redirect(f'/project/{project_id}/') def errback(): return redirect('/error/hydroshare-not-found') @@ -204,11 +200,11 @@ def callback(project_id): if hsresource and hsresource.resource == resource: # Use case (1). The user owns this exact project, so we show it. if request.user == project.user: - return redirect('/project/{}/'.format(project_id)) + return redirect(f'/project/{project_id}/') # Use case (2). This is a different user trying to edit a project # they don't own, so we clone it to their account. - return redirect('/project/{}/clone'.format(project_id)) + return redirect(f'/project/{project_id}/clone') # Use cases (3) and (4). This is a copy in HydroShare that needs a # corresponding new copy in MMW. Fetch that resource's details. @@ -265,7 +261,7 @@ def callback(project_id): # from which this project was created, don't associate it pass - return redirect('/project/{0}'.format(project.id)) + return redirect(f'/project/{project.id}') except IntegrityError: return redirect('/error/hydroshare-not-found') @@ -314,7 +310,7 @@ def get_api_token(): username=settings.CLIENT_APP_USERNAME) token = Token.objects.get(user=client_app_user) return token.key - except User.DoesNotExist, Token.DoesNotExist: + except (User.DoesNotExist, Token.DoesNotExist): return None @@ -366,6 +362,7 @@ def get_client_settings(request): }, 'enabled_features': settings.ENABLED_FEATURES, 'unit_scheme': unit_scheme, + 'celery_task_time_limit': settings.CELERY_TASK_TIME_LIMIT, }), 'google_maps_api_key': settings.GOOGLE_MAPS_API_KEY, 'title': title, diff --git a/src/mmw/apps/modeling/admin.py b/src/mmw/apps/modeling/admin.py index dd55b6450..a97fcb2eb 100644 --- a/src/mmw/apps/modeling/admin.py +++ b/src/mmw/apps/modeling/admin.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from django.contrib import admin from apps.modeling.models import Project, Scenario diff --git a/src/mmw/apps/modeling/calcs.py b/src/mmw/apps/modeling/calcs.py index a9705de36..4960bce1b 100644 --- a/src/mmw/apps/modeling/calcs.py +++ b/src/mmw/apps/modeling/calcs.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import - import csv import json import requests @@ -14,8 +10,6 @@ from django.conf import settings from django.db import connection -from django.contrib.gis.geos import WKBReader - from apps.modeling.mapshed.calcs import (area_calculations, nearest_weather_stations, average_weather_data @@ -44,16 +38,17 @@ def get_weather_modifications(csv_file): Returns a tuple where the first item is the output and the second is a list of errors. """ - rows = list(csv.reader(csv_file)) + file = csv_file.read().decode('utf-8') + rows = list(csv.reader(file.splitlines())) errs = [] def err(msg, line=None): - text = 'Line {}: {}'.format(line, msg) if line else msg + text = f'Line {line}: {msg}' if line else msg errs.append(text) if rows[0] != ['DATE', 'PRCP', 'TAVG']: err('Missing or incorrect header. Expected "DATE,PRCP,TAVG", got {}' - .format(','.join(rows[0]), 1)) + .format(','.join(rows[0])), 1) if len(rows) < 1097: err('Need at least 3 years of contiguous data.') @@ -79,9 +74,8 @@ def err(msg, line=None): year_range = endyear - begyear + 1 if year_range < 3 or year_range > 30: - err('Invalid year range {} between beginning year {}' - ' and end year {}. Year range must be between 3 and 30.' - .format(year_range, begyear, endyear)) + err(f'Invalid year range {year_range} between beginning year {begyear}' + f' and end year {endyear}. Year range must be between 3 and 30.') if errs: return None, errs @@ -109,7 +103,7 @@ def err(msg, line=None): # is the next one in the sequence. if idx > 0: if d == previous_d: - raise ValueError('Duplicate date: {}'.format(date)) + raise ValueError(f'Duplicate date: {date}') expected_d = previous_d + timedelta(days=1) if d != expected_d: @@ -159,13 +153,13 @@ def get_weather_simulation_for_project(project, category): # Ensure the station exists, if not exit quickly res = requests.head(url) if not res.ok: - errs.append('Error {} while getting data for {}/{}' - .format(res.status_code, category, ws.station)) + errs.append(f'Error {res.status_code} while getting data for' + f' {category}/{ws.station}') return {}, errs # Fetch and parse station weather data, noting any errors with closing(requests.get(url, stream=True)) as r: - ws_data, ws_errs = get_weather_modifications(r.iter_lines()) + ws_data, ws_errs = get_weather_modifications(r.raw) data.append(ws_data) errs += ws_errs @@ -177,7 +171,7 @@ def get_weather_simulation_for_project(project, category): for c in ['WxYrBeg', 'WxYrEnd', 'WxYrs']: s = set([d[c] for d in data]) if len(s) > 1: - errs.append('{} does not match in dataset: {}'.format(c, s)) + errs.append(f'{c} does not match in dataset: {s}') # Respond with errors, if any if errs: @@ -203,14 +197,14 @@ def split_into_huc12s(code, id): table_name = layer.get('table_name') huc_code = table_name.split('_')[1] - sql = ''' + sql = f''' SELECT 'huc12__' || boundary_huc12.id, boundary_huc12.huc12, ST_AsGeoJSON(boundary_huc12.geom_detailed) FROM boundary_huc12, {table_name} WHERE huc12 LIKE ({huc_code} || '%%') AND {table_name}.id = %s - '''.format(table_name=table_name, huc_code=huc_code) + ''' with connection.cursor() as cursor: cursor.execute(sql, [int(id)]) @@ -268,14 +262,14 @@ def apply_gwlfe_modifications(gms, modifications): modified_gms = deepcopy(gms) for mod in modifications: - for key, value in mod.iteritems(): + for key, value in mod.items(): if '__' in key: array_mods.append({key: value}) else: key_mods.append({key: value}) for mod in array_mods: - for key, value in mod.iteritems(): + for key, value in mod.items(): gmskey, i = key.split('__') modified_gms[gmskey][int(i)] = value @@ -310,7 +304,7 @@ def apply_subbasin_gwlfe_modifications(gms, modifications, urban_pct_total_stream_length = 1 for mod in weighted_modifications: - for key, val in mod.iteritems(): + for key, val in mod.items(): if key in ag_stream_length_weighted_keys: val *= ag_pct_total_stream_length elif key in urban_stream_length_weighted_keys: @@ -321,9 +315,9 @@ def apply_subbasin_gwlfe_modifications(gms, modifications, def sum_subbasin_stream_lengths(gmss): - ag = sum([gms['AgLength'] for gms in gmss.itervalues()]) + ag = sum([gms['AgLength'] for gms in gmss.values()]) urban = sum([gms['StreamLength'] - gms['AgLength'] - for gms in gmss.itervalues()]) + for gms in gmss.values()]) return { 'ag': ag, @@ -348,9 +342,9 @@ def get_layer_shape(table_code, id): properties = '' if table.startswith('boundary_huc'): - properties = "'huc', {}".format(table[-5:]) + properties = f"'huc', {table[-5:]}" - sql = ''' + sql = f''' SELECT json_build_object( 'type', 'Feature', 'id', id, @@ -358,7 +352,7 @@ def get_layer_shape(table_code, id): 'properties', json_build_object({properties})) FROM {table} WHERE id = %s - '''.format(field=field, properties=properties, table=table) + ''' with connection.cursor() as cursor: cursor.execute(sql, [int(id)]) @@ -370,6 +364,31 @@ def get_layer_shape(table_code, id): return None +def get_layer_value(layer, layer_overrides=dict): + """ + Given a layer, gets its value from default or overridden config. + + If the default config is something like: + + { + '__LAND__': 'nlcd-2019-30m-epsg5070-512-byte', + '__SOIL__': 'ssurgo-hydro-groups-30m-epsg5070-512-int8', + '__STREAMS__': 'nhd', + } + + and the layer_overrides are: + + { + '__STREAMS__': 'drb', + } + + Then get_layer_value('__STREAMS__', layer_overrides) => 'drb'. + + Throws an exception if the layer name is not found. + """ + return layer_overrides.get(layer, settings.GEOP['layers'][layer]) + + def boundary_search_context(search_term): suggestions = [] if len(search_term) < 3 else \ _do_boundary_search(search_term) @@ -409,11 +428,11 @@ def _get_boundary_search_query(search_term): subquery = ' UNION ALL '.join(selects) - return """ - SELECT id, code, name, rank, center - FROM ({}) AS subquery + return f""" + SELECT id, code, name, rank, ST_X(center) AS x, ST_Y(center) AS y + FROM ({subquery}) AS subquery ORDER BY rank DESC, name - """.format(subquery) + """ def _do_boundary_search(search_term): @@ -424,17 +443,16 @@ def _do_boundary_search(search_term): query = _get_boundary_search_query(search_term) with connection.cursor() as cursor: - wildcard_term = '%{}%'.format(search_term) + wildcard_term = f'%{search_term}%' cursor.execute(query, {'term': wildcard_term}) - wkb_r = WKBReader() - for row in cursor.fetchall(): id = row[0] code = row[1] name = row[2] rank = row[3] - point = wkb_r.read(row[4]) + x = row[4] + y = row[5] layer = _get_boundary_layer_by_code(code) @@ -444,8 +462,8 @@ def _do_boundary_search(search_term): 'text': name, 'label': layer['short_display'], 'rank': rank, - 'y': point.y, - 'x': point.x, + 'x': x, + 'y': y, }) return result diff --git a/src/mmw/apps/modeling/geoprocessing.py b/src/mmw/apps/modeling/geoprocessing.py index c1085c74a..e5ba8a834 100644 --- a/src/mmw/apps/modeling/geoprocessing.py +++ b/src/mmw/apps/modeling/geoprocessing.py @@ -1,12 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import - import requests import json from ast import literal_eval as make_tuple +from copy import deepcopy from celery import shared_task from celery.exceptions import Retry @@ -20,7 +17,8 @@ @shared_task(bind=True, default_retry_delay=1, max_retries=6) -def run(self, opname, input_data, wkaoi=None, cache_key=''): +def run(self, opname, input_data, wkaoi=None, cache_key='', + layer_overrides={}): """ Run a geoprocessing operation. @@ -41,6 +39,9 @@ def run(self, opname, input_data, wkaoi=None, cache_key=''): cache_key can be provided which will be used for caching instead of the opname, which in this case is not unique to the operation. + Uses the layers configured in settings.GEOP['layers'] by default. If any + should be overridden, they may be specified in layer_overrides. + To be used for single operation requests. Uses the /run endpoint of the geoprocessing service. @@ -48,11 +49,12 @@ def run(self, opname, input_data, wkaoi=None, cache_key=''): :param input_data: Dictionary of values to extend base operation JSON with :param wkaoi: String id of well-known area of interest. "{table}__{id}" :param cache_key: String to use for caching instead of opname. Optional. + :param layer_overrides: Dictionary of layers to override defaults with :return: Dictionary containing either results if successful, error if not """ if opname not in settings.GEOP['json']: return { - 'error': 'Unsupported operation {}'.format(opname) + 'error': f'Unsupported operation {opname}' } if not input_data: @@ -63,14 +65,31 @@ def run(self, opname, input_data, wkaoi=None, cache_key=''): key = '' if wkaoi and settings.GEOP['cache']: - key = 'geop_{}__{}{}'.format(wkaoi, opname, cache_key) + layers_cache_key = '__'.join(layer_overrides.values()) + key = f'geop_{wkaoi}__{opname}{layers_cache_key}{cache_key}' cached = cache.get(key) if cached: return cached - data = settings.GEOP['json'][opname].copy() + data = deepcopy(settings.GEOP['json'][opname]) data['input'].update(input_data) + # Populate layers + layer_config = dict(settings.GEOP['layers'], **layer_overrides) + + try: + if 'targetRaster' in data['input']: + data['input']['targetRaster'] = use_layer( + data['input']['targetRaster'], layer_config) + + for idx, raster in enumerate(data['input']['rasters']): + data['input']['rasters'][idx] = use_layer( + raster, layer_config) + except Exception as x: + return { + 'error': str(x) + } + # If no vector data is supplied for vector operation, shortcut to empty if 'vector' in data['input'] and data['input']['vector'] == [None]: result = {} @@ -96,7 +115,7 @@ def run(self, opname, input_data, wkaoi=None, cache_key=''): @shared_task(bind=True, default_retry_delay=1, max_retries=6) -def multi(self, opname, shapes, stream_lines): +def multi(self, opname, shapes, stream_lines, layer_overrides={}): """ Perform a multi-operation geoprocessing request. @@ -106,13 +125,13 @@ def multi(self, opname, shapes, stream_lines): { 'id': '', shape: '' } where 'shape' is a stringified GeoJSON of a Polygon or MultiPolygon and - 'stream_lines' is a stringified GeoJSON of a MultiLine, combines it with - the JSON saved in settings.GEOP.json corresponding to the opname, to make - a payload like this: + 'stream_lines' is a list of stringified GeoJSON of a MultiLine, combines + it with the JSON saved in settings.GEOP.json corresponding to the opname, + to make a payload like this: { 'shapes': [], - 'streamLines': '', + 'streamLines': [], 'operations': [] } @@ -136,6 +155,9 @@ def multi(self, opname, shapes, stream_lines): If there is an operation with 'name' == 'RasterLinesJoin', 'streamLines' should not be empty. If it is empty, that operation will be skipped. + If layer_overrides are provided, they will be used. Else we'll default to + the layers defined in settings.GEOP['layers']. + The results will be in the format: { @@ -153,7 +175,9 @@ def multi(self, opname, shapes, stream_lines): Each `operation_results` is cached with the key: - {{ shape_id }}__{{ operation_label }} + {{ shape_id }}__{{ operation_label }}{{ layers_cache_key }} + + where `layers_cache_key` is a string of optional layers provided. Before running the geoprocessing service, we inspect the cache to see if all the requested operations are already cached for this shape. If so, @@ -164,7 +188,7 @@ def multi(self, opname, shapes, stream_lines): we are using the same cache naming scheme as run, any operation cached via `multi` can be reused by `run`. """ - data = settings.GEOP['json'][opname].copy() + data = deepcopy(settings.GEOP['json'][opname]) data['shapes'] = [] # Don't include the RasterLinesJoin operation if the AoI does @@ -178,13 +202,31 @@ def multi(self, opname, shapes, stream_lines): operation_count = len(data['operations']) output = {} + # Populate layers + layer_config = dict(settings.GEOP['layers'], **layer_overrides) + layers_cache_key = '__'.join(layer_overrides.values()) + + try: + for oidx, operation in enumerate(data['operations']): + if 'targetRaster' in operation: + data['operations'][oidx]['targetRaster'] = use_layer( + operation['targetRaster'], layer_config) + + for ridx, raster in enumerate(operation['rasters']): + data['operations'][oidx]['rasters'][ridx] = use_layer( + raster, layer_config) + except Exception as x: + return { + 'error': str(x) + } + # Get cached results for shape in shapes: cached_operations = 0 if not shape['id'].startswith(NOCACHE): output[shape['id']] = {} for op in data['operations']: - key = 'geop_{}__{}'.format(shape['id'], op['label']) + key = f'geop_{shape["id"]}__{op["label"]}{layers_cache_key}' cached = cache.get(key) if cached: output[shape['id']][op['label']] = cached @@ -201,10 +243,10 @@ def multi(self, opname, shapes, stream_lines): result = geoprocess('multi', data, self.retry) # Set cached results - for shape_id, operation_results in result.iteritems(): + for shape_id, operation_results in result.items(): if not shape_id.startswith(NOCACHE): - for op_label, value in operation_results.iteritems(): - key = 'geop_{}__{}'.format(shape_id, op_label) + for op_label, value in operation_results.items(): + key = f'geop_{shape_id}__{op_label}{layers_cache_key}' cache.set(key, value, None) output.update(result) @@ -230,7 +272,7 @@ def geoprocess(endpoint, data, retry=None): host = settings.GEOP['host'] port = settings.GEOP['port'] - geop_url = 'http://{}:{}/{}'.format(host, port, endpoint) + geop_url = f'http://{host}:{port}/{endpoint}' try: response = requests.post(geop_url, @@ -240,7 +282,7 @@ def geoprocess(endpoint, data, retry=None): except ConnectionError as exc: if retry is not None: retry(exc=exc) - except Timeout as exc: + except Timeout: raise Exception('Geoprocessing service timed out.') if response.ok: @@ -250,8 +292,7 @@ def geoprocess(endpoint, data, retry=None): else: return result else: - raise Exception('Geoprocessing Error.\n' - 'Details: {}'.format(response.text)) + raise Exception(f'Geoprocessing Error.\nDetails: {response.text}') def parse(result): @@ -276,3 +317,17 @@ def parse(result): :return: Dictionary mapping tuples of ints to ints """ return {make_tuple(key[4:]): val for key, val in result.items()} + + +def use_layer(token, config): + """ + Replace layer tokens with real values from the config. + + Given a token string and a config in the shape of settings.GEOP['layers'], + if the token string is in the format __XYZ__, finds the tokenized layer + from the config and return that. Else, the input is a layer string, not a + token string, and is returned as is. + + Throws an exception if the token is not found in the configuration. + """ + return config[token] if token.startswith('__') else token diff --git a/src/mmw/apps/modeling/mapshed/calcs.py b/src/mmw/apps/modeling/mapshed/calcs.py index 9ada8630c..30a0e8347 100644 --- a/src/mmw/apps/modeling/mapshed/calcs.py +++ b/src/mmw/apps/modeling/mapshed/calcs.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import - import math import numpy @@ -48,7 +44,7 @@ def day_lengths(geom): math.cos(0.0172 * ((m + 1) * 30.4375 - 5))) - return [round(l, 1) for l in lengths] + return [round(length, 1) for length in lengths] def nearest_weather_stations(shapes, n=NUM_WEATHER_STATIONS): @@ -283,7 +279,7 @@ def ls_factors(lu_strms, total_strm_len, areas, avg_slope, ag_lscp): results[0] = ag_lscp.hp_ls results[1] = ag_lscp.crop_ls - for i in xrange(2, 16): + for i in range(2, 16): results[i] = (ls_factor(lu_strms[i] * total_strm_len * KM_PER_M, areas[i], avg_slope, m)) @@ -313,22 +309,25 @@ def ls_factor(stream_length, area, avg_slope, m): return ls -def stream_length(geom, drb=False): +def stream_length(geom, datasource='nhdhr'): """ Given a geometry, finds the total length of streams in meters within it. If the drb flag is set, we use the Delaware River Basin dataset instead of NHD Flowline. """ - sql = ''' + if datasource not in settings.STREAM_TABLES: + raise Exception(f'Invalid stream datasource {datasource}') + + sql = f''' SELECT ROUND(SUM(ST_Length( ST_Transform( ST_Intersection(geom, ST_SetSRID(ST_GeomFromText(%s), 4326)), 5070)))) - FROM {datasource} + FROM {settings.STREAM_TABLES[datasource]} WHERE ST_Intersects(geom, ST_SetSRID(ST_GeomFromText(%s), 4326)); - '''.format(datasource='drb_streams_50' if drb else 'nhdflowline') + ''' with connection.cursor() as cursor: cursor.execute(sql, [geom.wkt, geom.wkt]) @@ -336,19 +335,22 @@ def stream_length(geom, drb=False): return cursor.fetchone()[0] or 0 # Aggregate query returns singleton -def streams(geojson, drb=False): +def streams(geojson, datasource='nhdhr'): """ Given a GeoJSON, returns a list containing a single MultiLineString, that represents the set of streams that intersect with the geometry, in LatLng. If the drb flag is set, we use the Delaware River Basin dataset instead of NHD Flowline. """ - sql = ''' - SELECT ST_AsGeoJSON(ST_Collect(ST_Force2D(geom))) - FROM {datasource} + if datasource not in settings.STREAM_TABLES: + raise Exception(f'Invalid stream datasource {datasource}') + + sql = f''' + SELECT ST_AsGeoJSON(ST_Multi(geom)) + FROM {settings.STREAM_TABLES[datasource]} WHERE ST_Intersects(geom, ST_SetSRID(ST_GeomFromGeoJSON(%s), 4326)) - '''.format(datasource='drb_streams_50' if drb else 'nhdflowline') + ''' with connection.cursor() as cursor: cursor.execute(sql, [geojson]) @@ -369,14 +371,14 @@ def point_source_discharge(geom, area, drb=False): this uses the ms_pointsource_drb table. """ table_name = get_point_source_table(drb) - sql = ''' + sql = f''' SELECT SUM(mgd) AS mg_d, SUM(kgn_yr) / 12 AS kgn_month, SUM(kgp_yr) / 12 AS kgp_month FROM {table_name} WHERE ST_Intersects(geom, ST_SetSRID(ST_GeomFromText(%s), 4326)); - '''.format(table_name=table_name) + ''' with connection.cursor() as cursor: cursor.execute(sql, [geom.wkt]) @@ -485,7 +487,7 @@ def curve_number(n_count, ng_count): # Calculate average hydrological soil group for each NLCD type by # reducing [(n, g): c] to [n: avg(g * c)] n_gavg = {} - for (n, g), count in ng_count.iteritems(): + for (n, g), count in ng_count.items(): n_gavg[n] = float(g) * count / n_count[n] + n_gavg.get(n, 0) def cni(nlcd): @@ -532,7 +534,7 @@ def groundwater_nitrogen_conc(gwn_dict): weighted_conc = 0 if valid_total_cells > 0: weighted_conc = sum([float(gwn * count)/valid_total_cells - for gwn, count in valid_res.iteritems()]) + for gwn, count in valid_res.items()]) groundwater_nitrogen_conc = (0.7973 * weighted_conc) - 0.692 groundwater_phosphorus_conc = (0.0049 * weighted_conc) + 0.0089 @@ -569,7 +571,7 @@ def landuse_pcts(n_count): total = sum(n_count.values()) if total > 0: n_pct = {nlcd: float(count) / total - for nlcd, count in n_count.iteritems()} + for nlcd, count in n_count.items()} else: n_pct = {nlcd: 0 for nlcd in n_count.keys()} @@ -614,7 +616,7 @@ def num_normal_sys(lu_area): normal_sys_estimate = SSLDR * lu_area[14] + SSLDM * lu_area[11] normal_sys_int = int(round(normal_sys_estimate)) - return [normal_sys_int for n in xrange(12)] + return [normal_sys_int for n in range(12)] def sed_a_factor(landuse_pct_vals, cn, AEU, AvKF, AvSlope): diff --git a/src/mmw/apps/modeling/mapshed/tasks.py b/src/mmw/apps/modeling/mapshed/tasks.py index 8a85c01be..cd8c6b428 100644 --- a/src/mmw/apps/modeling/mapshed/tasks.py +++ b/src/mmw/apps/modeling/mapshed/tasks.py @@ -1,13 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import - from celery import shared_task from django.conf import settings from django.contrib.gis.geos import GEOSGeometry +from apps.modeling.calcs import get_layer_value from apps.modeling.geoprocessing import NOCACHE, multi, run, parse from apps.modeling.mapshed.calcs import (day_lengths, nearest_weather_stations, @@ -45,7 +42,8 @@ @shared_task -def collect_data(geop_results, geojson, watershed_id=None, weather=None): +def collect_data(geop_results, geojson, watershed_id=None, weather=None, + layer_overrides={}): geop_result = {k: v for r in geop_results for k, v in r.items()} geom = GEOSGeometry(geojson, srid=4326) @@ -91,8 +89,9 @@ def collect_data(geop_results, geojson, watershed_id=None, weather=None): z['ManNitr'], z['ManPhos'] = manure_spread(z['AEU']) # Data from Streams dataset - z['StreamLength'] = stream_length(geom) or 10 # Meters - z['n42b'] = round(z['StreamLength'] / 1000, 1) # Kilometers + datasource = get_layer_value('__STREAMS__', layer_overrides) + z['StreamLength'] = stream_length(geom, datasource) or 10 # Meters + z['n42b'] = round(z['StreamLength'] / 1000, 1) # Kilometers # Data from Point Source Discharge dataset n_load, p_load, discharge = point_source_discharge(geom, area, @@ -105,8 +104,8 @@ def collect_data(geop_results, geojson, watershed_id=None, weather=None): if weather is None: wd = weather_data(ws, z['WxYrBeg'], z['WxYrEnd']) temps_dict, prcps_dict = wd - temps = average_weather_data(temps_dict.values()) - prcps = average_weather_data(prcps_dict.values()) + temps = average_weather_data(list(temps_dict.values())) + prcps = average_weather_data(list(prcps_dict.values())) else: temps, prcps = wd z['Temp'] = temps @@ -214,7 +213,7 @@ def nlcd_streams(result): post-processing tasks, to be used in geop_tasks. """ if 'error' in result: - raise Exception('[nlcd_streams] {}'.format(result['error'])) + raise Exception(f'[nlcd_streams] {result["error"]}') # This can't be done in geoprocessing.run because the keys may be tuples, # which are not JSON serializable and thus can't be shared between tasks @@ -231,7 +230,7 @@ def nlcd_streams(result): low_urban_count, med_high_urban_count)) lu_stream_pct = [0.0] * NLU - for nlcd, stream_count in result.iteritems(): + for nlcd, stream_count in result.items(): lu = get_lu_index(nlcd) if lu is not None: lu_stream_pct[lu] += float(stream_count) / total @@ -251,13 +250,13 @@ def nlcd_streams_drb(result): the percentage of DRB streams in each land use type. """ if 'error' in result: - raise Exception('[nlcd_streams_drb] {}'.format(result['error'])) + raise Exception(f'[nlcd_streams_drb] {result["error"]}') result = parse(result) total = sum(result.values()) lu_stream_pct_drb = [0.0] * NLU - for nlcd, stream_count in result.iteritems(): + for nlcd, stream_count in result.items(): lu = get_lu_index(nlcd) if lu is not None: lu_stream_pct_drb[lu] += float(stream_count) / total @@ -279,7 +278,7 @@ def nlcd_soil(result): of these raster datasets. """ if 'error' in result: - raise Exception('[nlcd_soil] {}'.format(result['error'])) + raise Exception(f'[nlcd_soil] {result["error"]}') ng_count = parse(result) @@ -291,7 +290,7 @@ def nlcd_soil(result): # Reduce [(n, g, t): c] to n_count = {} # [n: sum(c)] ng2_count = {} # [(n, g): sum(c)] - for (n, g), count in ng_count.iteritems(): + for (n, g), count in ng_count.items(): n_count[n] = count + n_count.get(n, 0) # Map soil group values to usable subset @@ -311,8 +310,7 @@ def gwn(result): Derive Groundwater Nitrogen and Phosphorus """ if 'error' in result: - raise Exception('[gwn] {}' - .format(result['error'])) + raise Exception(f'[gwn] {result["error"]}') result = parse(result) gr_nitr_conc, gr_phos_conc = groundwater_nitrogen_conc(result) @@ -331,13 +329,12 @@ def avg_awc(result): Original at Class1.vb@1.3.0:4150 """ if 'error' in result: - raise Exception('[awc] {}' - .format(result['error'])) + raise Exception(f'[awc] {result["error"]}') result = parse(result) return { - 'avg_awc': result.values()[0] + 'avg_awc': list(result.values())[0] } @@ -349,12 +346,11 @@ def soilp(result): Originally calculated via lookup table at Class1.vb@1.3.0:8975-8988 """ if 'error' in result: - raise Exception('[soilp] {}' - .format(result['error'])) + raise Exception(f'[soilp] {result["error"]}') result = parse(result) - soilp = result.values()[0] * 1.6 + soilp = list(result.values())[0] * 1.6 return { 'soilp': soilp @@ -369,12 +365,11 @@ def recess_coef(result): Originally a static value 0.06 Class1.vb@1.3.0:10333 """ if 'error' in result: - raise Exception('[recess_coef] {}' - .format(result['error'])) + raise Exception(f'[recess_coef] {result["error"]}') result = parse(result) - recess_coef = result.values()[0] * -0.0015 + 0.1103 + recess_coef = list(result.values())[0] * -0.0015 + 0.1103 recess_coef = recess_coef if recess_coef >= 0 else 0.01 return { @@ -390,12 +385,11 @@ def soiln(result): Originally a static value of 2000 at Class1.vb@1.3.0:9587 """ if 'error' in result: - raise Exception('[soiln] {}' - .format(result['error'])) + raise Exception(f'[soiln] {result["error"]}') result = parse(result) - soiln = result.values()[0] * 9.0 + soiln = list(result.values())[0] * 9.0 return { 'soiln': soiln @@ -405,7 +399,7 @@ def soiln(result): @shared_task(throws=Exception) def nlcd_slope(result): if 'error' in result: - raise Exception('[nlcd_slope] {}'.format(result['error'])) + raise Exception(f'[nlcd_slope] {result["error"]}') result = parse(result) @@ -414,7 +408,7 @@ def nlcd_slope(result): ag_count = 0 total_count = 0 - for (nlcd_code, slope), count in result.iteritems(): + for (nlcd_code, slope), count in result.items(): if nlcd_code in AG_NLCD_CODES: if slope > 3: ag_slope_3_count += count @@ -449,7 +443,7 @@ def nlcd_slope(result): @shared_task(throws=Exception) def slope(result): if 'error' in result: - raise Exception('[slope] {}'.format(result['error'])) + raise Exception(f'[slope] {result["error"]}') result = parse(result) @@ -467,14 +461,14 @@ def slope(result): @shared_task(throws=Exception) def nlcd_kfactor(result): if 'error' in result: - raise Exception('[nlcd_kfactor] {}'.format(result['error'])) + raise Exception(f'[nlcd_kfactor] {result["error"]}') result = parse(result) # average kfactor for each land use # see Class1.vb#6431 kf = [0.0] * NLU - for nlcd_code, kfactor in result.iteritems(): + for nlcd_code, kfactor in result.items(): lu_ind = get_lu_index(nlcd_code) if lu_ind is not None: kf[lu_ind] = kfactor @@ -494,27 +488,30 @@ def nlcd_kfactor(result): return output -def multi_mapshed(aoi, wkaoi): +def multi_mapshed(aoi, wkaoi, layer_overrides={}): shape = [{'id': wkaoi or NOCACHE, 'shape': aoi}] - stream_lines = streams(aoi)[0] + datasource = get_layer_value('__STREAMS__', layer_overrides) + stream_lines = streams(aoi, datasource) - return multi.s('mapshed', shape, stream_lines) + return multi.s('mapshed', shape, stream_lines, + layer_overrides=layer_overrides) -def multi_subbasin(parent_aoi, child_shapes): +def multi_subbasin(parent_aoi, child_shapes, layer_overrides={}): shapes = [{'id': wkaoi, 'shape': aoi} for (wkaoi, _, aoi) in child_shapes] - stream_lines = streams(parent_aoi)[0] + datasource = get_layer_value('__STREAMS__', layer_overrides) + stream_lines = streams(parent_aoi, datasource) - return multi.s('mapshed', shapes, stream_lines) + return multi.s('mapshed', shapes, stream_lines, + layer_overrides=layer_overrides) @shared_task(throws=Exception) def convert_data(payload, wkaoi): if 'error' in payload: raise Exception( - '[convert_data] {} {}'.format( - wkaoi or NOCACHE, payload['error'])) + f'[convert_data] {wkaoi or NOCACHE} {payload["error"]}') results = payload[wkaoi or NOCACHE] @@ -537,7 +534,7 @@ def convert_data(payload, wkaoi): @shared_task -def collect_subbasin(payload, shapes): +def collect_subbasin(payload, shapes, layer_overrides={}): # Gather weather stations and their data # collectively to avoid re-reading stations # that are shared across huc-12s @@ -564,7 +561,8 @@ def get_weather(watershed_id): # Build the GMS data for each huc-12 return [ collect_data(convert_data(payload, wkaoi), aoi, watershed_id, - get_weather(watershed_id)) + get_weather(watershed_id), + layer_overrides=layer_overrides) for (wkaoi, watershed_id, aoi) in shapes ] diff --git a/src/mmw/apps/modeling/migrations/0001_initial.py b/src/mmw/apps/modeling/migrations/0001_initial.py index de121aa55..845f7f14b 100644 --- a/src/mmw/apps/modeling/migrations/0001_initial.py +++ b/src/mmw/apps/modeling/migrations/0001_initial.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import django.contrib.gis.db.models.fields diff --git a/src/mmw/apps/modeling/migrations/0002_add_projects_scenarios.py b/src/mmw/apps/modeling/migrations/0002_add_projects_scenarios.py index b2a1523d3..68d939c7a 100644 --- a/src/mmw/apps/modeling/migrations/0002_add_projects_scenarios.py +++ b/src/mmw/apps/modeling/migrations/0002_add_projects_scenarios.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations from django.conf import settings import django.contrib.gis.db.models.fields diff --git a/src/mmw/apps/modeling/migrations/0003_auto_20150526_1700.py b/src/mmw/apps/modeling/migrations/0003_auto_20150526_1700.py index 09d8fb1b5..55cca2b41 100644 --- a/src/mmw/apps/modeling/migrations/0003_auto_20150526_1700.py +++ b/src/mmw/apps/modeling/migrations/0003_auto_20150526_1700.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0004_auto_20150529_1700.py b/src/mmw/apps/modeling/migrations/0004_auto_20150529_1700.py index 8022df49c..b4dc59ce5 100644 --- a/src/mmw/apps/modeling/migrations/0004_auto_20150529_1700.py +++ b/src/mmw/apps/modeling/migrations/0004_auto_20150529_1700.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0005_auto_20150608_1502.py b/src/mmw/apps/modeling/migrations/0005_auto_20150608_1502.py index bc3763f79..5fe9fcedc 100644 --- a/src/mmw/apps/modeling/migrations/0005_auto_20150608_1502.py +++ b/src/mmw/apps/modeling/migrations/0005_auto_20150608_1502.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0006_auto_20150630_1320.py b/src/mmw/apps/modeling/migrations/0006_auto_20150630_1320.py index 272c5524a..1884243c5 100644 --- a/src/mmw/apps/modeling/migrations/0006_auto_20150630_1320.py +++ b/src/mmw/apps/modeling/migrations/0006_auto_20150630_1320.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0007_scenario_census.py b/src/mmw/apps/modeling/migrations/0007_scenario_census.py index 83b8a211c..b345aef89 100644 --- a/src/mmw/apps/modeling/migrations/0007_scenario_census.py +++ b/src/mmw/apps/modeling/migrations/0007_scenario_census.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0008_delete_district.py b/src/mmw/apps/modeling/migrations/0008_delete_district.py index cdeada646..6ffc28228 100644 --- a/src/mmw/apps/modeling/migrations/0008_delete_district.py +++ b/src/mmw/apps/modeling/migrations/0008_delete_district.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0009_scenario_results_to_json.py b/src/mmw/apps/modeling/migrations/0009_scenario_results_to_json.py index 06f2aba42..44cd30aca 100644 --- a/src/mmw/apps/modeling/migrations/0009_scenario_results_to_json.py +++ b/src/mmw/apps/modeling/migrations/0009_scenario_results_to_json.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations diff --git a/src/mmw/apps/modeling/migrations/0010_scenario_inputmod_hash.py b/src/mmw/apps/modeling/migrations/0010_scenario_inputmod_hash.py index 1037925be..d85fbca72 100644 --- a/src/mmw/apps/modeling/migrations/0010_scenario_inputmod_hash.py +++ b/src/mmw/apps/modeling/migrations/0010_scenario_inputmod_hash.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0011_project_null_aoi.py b/src/mmw/apps/modeling/migrations/0011_project_null_aoi.py index 9a35d02ef..eedc53e6c 100644 --- a/src/mmw/apps/modeling/migrations/0011_project_null_aoi.py +++ b/src/mmw/apps/modeling/migrations/0011_project_null_aoi.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import django.contrib.gis.db.models.fields diff --git a/src/mmw/apps/modeling/migrations/0012_project_activity_flag.py b/src/mmw/apps/modeling/migrations/0012_project_activity_flag.py index 3763eb554..fe258e2c6 100644 --- a/src/mmw/apps/modeling/migrations/0012_project_activity_flag.py +++ b/src/mmw/apps/modeling/migrations/0012_project_activity_flag.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0013_project_area_of_interest_name.py b/src/mmw/apps/modeling/migrations/0013_project_area_of_interest_name.py index c7b155da0..01842ad17 100644 --- a/src/mmw/apps/modeling/migrations/0013_project_area_of_interest_name.py +++ b/src/mmw/apps/modeling/migrations/0013_project_area_of_interest_name.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0014_auto_20151005_0945.py b/src/mmw/apps/modeling/migrations/0014_auto_20151005_0945.py index 2a6ea314a..df16f8c75 100644 --- a/src/mmw/apps/modeling/migrations/0014_auto_20151005_0945.py +++ b/src/mmw/apps/modeling/migrations/0014_auto_20151005_0945.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0015_remove_scenario_census.py b/src/mmw/apps/modeling/migrations/0015_remove_scenario_census.py index 9c3df74c3..92db60eb2 100644 --- a/src/mmw/apps/modeling/migrations/0015_remove_scenario_census.py +++ b/src/mmw/apps/modeling/migrations/0015_remove_scenario_census.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0016_old_scenarios.py b/src/mmw/apps/modeling/migrations/0016_old_scenarios.py index ffee853fd..75bffeabd 100644 --- a/src/mmw/apps/modeling/migrations/0016_old_scenarios.py +++ b/src/mmw/apps/modeling/migrations/0016_old_scenarios.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0017_add_gwlfe.py b/src/mmw/apps/modeling/migrations/0017_add_gwlfe.py index 14f604c1d..d0b8d14ac 100644 --- a/src/mmw/apps/modeling/migrations/0017_add_gwlfe.py +++ b/src/mmw/apps/modeling/migrations/0017_add_gwlfe.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/modeling/migrations/0018_postgis_add_EPSG5070.py b/src/mmw/apps/modeling/migrations/0018_postgis_add_EPSG5070.py index 0aad1ff1b..8715c2edb 100644 --- a/src/mmw/apps/modeling/migrations/0018_postgis_add_EPSG5070.py +++ b/src/mmw/apps/modeling/migrations/0018_postgis_add_EPSG5070.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/modeling/migrations/0019_project_gis_data.py b/src/mmw/apps/modeling/migrations/0019_project_gis_data.py index a1a534ae0..f44457aa8 100644 --- a/src/mmw/apps/modeling/migrations/0019_project_gis_data.py +++ b/src/mmw/apps/modeling/migrations/0019_project_gis_data.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/modeling/migrations/0020_old_scenarios.py b/src/mmw/apps/modeling/migrations/0020_old_scenarios.py index cfff265c3..b6214ce7d 100644 --- a/src/mmw/apps/modeling/migrations/0020_old_scenarios.py +++ b/src/mmw/apps/modeling/migrations/0020_old_scenarios.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0021_old_scenarios.py b/src/mmw/apps/modeling/migrations/0021_old_scenarios.py index 3bfe72c40..ef7cedb84 100644 --- a/src/mmw/apps/modeling/migrations/0021_old_scenarios.py +++ b/src/mmw/apps/modeling/migrations/0021_old_scenarios.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/src/mmw/apps/modeling/migrations/0022_project_wkaoi.py b/src/mmw/apps/modeling/migrations/0022_project_wkaoi.py index 3b9af5496..048aaf1a4 100644 --- a/src/mmw/apps/modeling/migrations/0022_project_wkaoi.py +++ b/src/mmw/apps/modeling/migrations/0022_project_wkaoi.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/modeling/migrations/0023_fix_gis_data_serialization.py b/src/mmw/apps/modeling/migrations/0023_fix_gis_data_serialization.py index d55bf180e..619ed1e85 100644 --- a/src/mmw/apps/modeling/migrations/0023_fix_gis_data_serialization.py +++ b/src/mmw/apps/modeling/migrations/0023_fix_gis_data_serialization.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - import ast import json diff --git a/src/mmw/apps/modeling/migrations/0024_fix_gwlfe_gis_data.py b/src/mmw/apps/modeling/migrations/0024_fix_gwlfe_gis_data.py index 3cb054f1e..44ef100ce 100644 --- a/src/mmw/apps/modeling/migrations/0024_fix_gwlfe_gis_data.py +++ b/src/mmw/apps/modeling/migrations/0024_fix_gwlfe_gis_data.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from datetime import datetime from django.db import migrations +from django.utils.timezone import make_aware def fix_gis_data_serialization(apps, schema_editor): @@ -16,7 +17,7 @@ def fix_gis_data_serialization(apps, schema_editor): """ Project = apps.get_model('modeling', 'Project') - bug_released_date = '2017-10-17' + bug_released_date = make_aware(datetime.fromisoformat('2017-10-17')) # Apply fix to Multi-Year projects created after the release for project in Project.objects.filter(created_at__gte=bug_released_date, diff --git a/src/mmw/apps/modeling/migrations/0025_hydroshareresource.py b/src/mmw/apps/modeling/migrations/0025_hydroshareresource.py index d592621b2..fde3d8bb0 100644 --- a/src/mmw/apps/modeling/migrations/0025_hydroshareresource.py +++ b/src/mmw/apps/modeling/migrations/0025_hydroshareresource.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models import django.db.models.deletion diff --git a/src/mmw/apps/modeling/migrations/0026_delete_hydroshareresource.py b/src/mmw/apps/modeling/migrations/0026_delete_hydroshareresource.py index e9ec6a082..fba4556eb 100644 --- a/src/mmw/apps/modeling/migrations/0026_delete_hydroshareresource.py +++ b/src/mmw/apps/modeling/migrations/0026_delete_hydroshareresource.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/modeling/migrations/0027_project_add_mapshed_job_uuids.py b/src/mmw/apps/modeling/migrations/0027_project_add_mapshed_job_uuids.py index 5231bbf42..124791a31 100644 --- a/src/mmw/apps/modeling/migrations/0027_project_add_mapshed_job_uuids.py +++ b/src/mmw/apps/modeling/migrations/0027_project_add_mapshed_job_uuids.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models import django.db.models.deletion diff --git a/src/mmw/apps/modeling/migrations/0028_clear_old_mapshed_results.py b/src/mmw/apps/modeling/migrations/0028_clear_old_mapshed_results.py index 07e545d81..b33854a9f 100644 --- a/src/mmw/apps/modeling/migrations/0028_clear_old_mapshed_results.py +++ b/src/mmw/apps/modeling/migrations/0028_clear_old_mapshed_results.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/modeling/migrations/0029_project_on_delete.py b/src/mmw/apps/modeling/migrations/0029_project_on_delete.py index 6104cd5a3..77de79d77 100644 --- a/src/mmw/apps/modeling/migrations/0029_project_on_delete.py +++ b/src/mmw/apps/modeling/migrations/0029_project_on_delete.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.22 on 2019-07-16 16:54 -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models import django.db.models.deletion diff --git a/src/mmw/apps/modeling/migrations/0030_custom_weather_dataset.py b/src/mmw/apps/modeling/migrations/0030_custom_weather_dataset.py index 1e51d3d05..10e40430e 100644 --- a/src/mmw/apps/modeling/migrations/0030_custom_weather_dataset.py +++ b/src/mmw/apps/modeling/migrations/0030_custom_weather_dataset.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.29 on 2020-03-24 19:52 -from __future__ import unicode_literals - import apps.modeling.models from django.db import migrations, models diff --git a/src/mmw/apps/modeling/migrations/0031_scenario_add_weather_fields.py b/src/mmw/apps/modeling/migrations/0031_scenario_add_weather_fields.py index 01c379c69..faf109149 100644 --- a/src/mmw/apps/modeling/migrations/0031_scenario_add_weather_fields.py +++ b/src/mmw/apps/modeling/migrations/0031_scenario_add_weather_fields.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.29 on 2020-04-21 02:19 -from __future__ import unicode_literals - import apps.modeling.models from django.db import migrations, models diff --git a/src/mmw/apps/modeling/migrations/0032_project_remove_weather_fields.py b/src/mmw/apps/modeling/migrations/0032_project_remove_weather_fields.py index 3b75703cf..585e8c936 100644 --- a/src/mmw/apps/modeling/migrations/0032_project_remove_weather_fields.py +++ b/src/mmw/apps/modeling/migrations/0032_project_remove_weather_fields.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.29 on 2020-04-21 04:42 -from __future__ import unicode_literals - from django.db import migrations diff --git a/src/mmw/apps/modeling/migrations/0033_delete_empty_aoi_projects.py b/src/mmw/apps/modeling/migrations/0033_delete_empty_aoi_projects.py index d105d8c43..9648ee8b5 100644 --- a/src/mmw/apps/modeling/migrations/0033_delete_empty_aoi_projects.py +++ b/src/mmw/apps/modeling/migrations/0033_delete_empty_aoi_projects.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.29 on 2021-01-06 20:55 -from __future__ import unicode_literals - from django.db import migrations diff --git a/src/mmw/apps/modeling/migrations/0034_forbid_null_aoi_on_non_activity_projects.py b/src/mmw/apps/modeling/migrations/0034_forbid_null_aoi_on_non_activity_projects.py index 6bac8b187..6d8c554b1 100644 --- a/src/mmw/apps/modeling/migrations/0034_forbid_null_aoi_on_non_activity_projects.py +++ b/src/mmw/apps/modeling/migrations/0034_forbid_null_aoi_on_non_activity_projects.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.29 on 2021-01-06 21:03 -from __future__ import unicode_literals - from django.db import migrations diff --git a/src/mmw/apps/modeling/migrations/0035_project_layer_overrides.py b/src/mmw/apps/modeling/migrations/0035_project_layer_overrides.py new file mode 100644 index 000000000..223e28e65 --- /dev/null +++ b/src/mmw/apps/modeling/migrations/0035_project_layer_overrides.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2021-07-28 17:29 +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('modeling', '0034_forbid_null_aoi_on_non_activity_projects'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='layer_overrides', + field=django.contrib.postgres.fields.jsonb.JSONField(default={'__LAND__': 'nlcd-2011-30m-epsg5070-512-int8'}, help_text='JSON object of layers to override defaults with'), + preserve_default=False, + ), + ] diff --git a/src/mmw/apps/modeling/migrations/0036_no_overrides_by_default.py b/src/mmw/apps/modeling/migrations/0036_no_overrides_by_default.py new file mode 100644 index 000000000..569cdd2cc --- /dev/null +++ b/src/mmw/apps/modeling/migrations/0036_no_overrides_by_default.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2021-07-28 17:30 +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('modeling', '0035_project_layer_overrides'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='layer_overrides', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, help_text='JSON object of layers to override defaults with'), + ), + ] diff --git a/src/mmw/apps/modeling/migrations/0037_layer_overrides_streams.py b/src/mmw/apps/modeling/migrations/0037_layer_overrides_streams.py new file mode 100644 index 000000000..d0cda56d3 --- /dev/null +++ b/src/mmw/apps/modeling/migrations/0037_layer_overrides_streams.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2021-11-02 04:15 +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('modeling', '0036_no_overrides_by_default'), + ] + + # Add {'__STREAMS__': 'nhd'} to all layer_overrides + operations = [ + migrations.RunSQL(''' + UPDATE modeling_project + SET layer_overrides = + layer_overrides::jsonb || + JSON_BUILD_OBJECT('__STREAMS__', 'nhd')::jsonb; + ''') + ] diff --git a/src/mmw/apps/modeling/migrations/0038_alter_project_layer_overrides.py b/src/mmw/apps/modeling/migrations/0038_alter_project_layer_overrides.py new file mode 100644 index 000000000..eb9d26500 --- /dev/null +++ b/src/mmw/apps/modeling/migrations/0038_alter_project_layer_overrides.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.10 on 2021-12-16 19:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('modeling', '0037_layer_overrides_streams'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='layer_overrides', + field=models.JSONField(default=dict, help_text='JSON object of layers to override defaults with'), + ), + ] diff --git a/src/mmw/apps/modeling/migrations/0039_override_sedaadjust_for_old_scenarios.py b/src/mmw/apps/modeling/migrations/0039_override_sedaadjust_for_old_scenarios.py new file mode 100644 index 000000000..77cc95f5f --- /dev/null +++ b/src/mmw/apps/modeling/migrations/0039_override_sedaadjust_for_old_scenarios.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.10 on 2021-12-27 19:26 +import json +from django.db import migrations + + +def override_sedaadjust_for_old_projects(apps, schema_editor): + """ + The default value of SedAAdjust is being changed from 1.5 to 1.25 for all + new projects, which will use the high resolution "nhdhr" stream data. For + older projects using the medium resolution "nhd" data, we override the + value to be 1.5, so they remain consistent with old data, unless they were + overridden by a user. + """ + db_alias = schema_editor.connection.alias + Project = apps.get_model('modeling', 'Project') + + ps = Project.objects.filter(layer_overrides__contains={'__STREAMS__':'nhd'}) + + for p in ps: + for s in p.scenarios.all(): + mods = json.loads(s.modifications) + m_other = next((m for m in mods if m.get('modKey') == 'entry_other'), None) + + if m_other: + if 'SedAAdjust' not in m_other['output']: + m_other['output']['SedAAdjust'] = 1.5 + m_other['userInput']['SedAAdjust'] = 1.5 + + s.modifications = json.dumps(mods) + s.save() + else: + mods.append({ + 'modKey': 'entry_other', + 'output': {'SedAAdjust': 1.5}, + 'userInput': {'SedAAdjust': 1.5}}) + + s.modifications = json.dumps(mods) + s.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('modeling', '0038_alter_project_layer_overrides'), + ] + + operations = [ + migrations.RunPython(override_sedaadjust_for_old_projects), + ] diff --git a/src/mmw/apps/modeling/models.py b/src/mmw/apps/modeling/models.py index 823167c4d..8ad5299a3 100644 --- a/src/mmw/apps/modeling/models.py +++ b/src/mmw/apps/modeling/models.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - -from django.db.models import FileField +from django.db.models import FileField, JSONField from django.conf import settings from django.contrib.gis.db import models from django.contrib.auth.models import User @@ -12,11 +8,11 @@ def project_filename(project, filename): - return 'project_{0}/{1}'.format(project.id, filename) + return f'project_{project.id}/{filename}' def scenario_filename(scenario, filename): - return 'p{0}/s{1}/{2}'.format(scenario.project.id, scenario.id, filename) + return f'p{scenario.project.id}/s{scenario.id}/{filename}' class Project(models.Model): @@ -75,6 +71,9 @@ class Project(models.Model): null=True, max_length=255, help_text='Well-Known Area of Interest ID for faster geoprocessing') + layer_overrides = JSONField( + default=dict, + help_text='JSON object of layers to override defaults with') def __unicode__(self): return self.name diff --git a/src/mmw/apps/modeling/serializers.py b/src/mmw/apps/modeling/serializers.py index fce64e527..da44ce8f6 100644 --- a/src/mmw/apps/modeling/serializers.py +++ b/src/mmw/apps/modeling/serializers.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - import rollbar - from django.contrib.gis.geos import (GEOSGeometry, MultiPolygon) @@ -41,16 +37,17 @@ def to_internal_value(self, data): """ if data == '' or data is None: return data - if isinstance(data, basestring): + if isinstance(data, str): data = json.loads(data) - geometry = data['geometry'] if 'geometry' in data else data + geometry = data try: if not isinstance(geometry, GEOSGeometry): + geometry = data['geometry'] if 'geometry' in data else data geometry = GEOSGeometry(json.dumps(geometry)) geometry.srid = 4326 - except: + except Exception: raise ValidationError('Area of interest must ' + 'be valid GeoJSON, of type ' + 'Feature, Polygon or MultiPolygon') @@ -115,7 +112,7 @@ class Meta: 'scenarios', 'model_package', 'created_at', 'modified_at', 'is_private', 'is_activity', 'gis_data', 'mapshed_job_uuid', 'subbasin_mapshed_job_uuid', 'wkaoi', 'user', 'hydroshare', - 'in_drb') + 'in_drb', 'layer_overrides') user = UserSerializer(default=serializers.CurrentUserDefault()) gis_data = JsonField(required=False, allow_null=True) @@ -139,7 +136,7 @@ class Meta: model = Project fields = ('id', 'name', 'area_of_interest_name', 'is_private', 'model_package', 'created_at', 'modified_at', 'user', - 'hydroshare') + 'hydroshare', 'layer_overrides') hydroshare = HydroShareResourceSerializer(read_only=True) @@ -153,7 +150,8 @@ class Meta: fields = ('id', 'name', 'area_of_interest', 'area_of_interest_name', 'model_package', 'created_at', 'modified_at', 'is_private', 'is_activity', 'gis_data', 'mapshed_job_uuid', - 'subbasin_mapshed_job_uuid', 'wkaoi', 'user') + 'subbasin_mapshed_job_uuid', 'wkaoi', 'user', + 'layer_overrides') user = UserSerializer(default=serializers.CurrentUserDefault(), read_only=True) @@ -195,7 +193,7 @@ def validate(self, data): # Validate that either AoI or WKAoI is specified correctly serializer = AoiSerializer(data=data) serializer.is_valid(raise_exception=True) - except: + except Exception: rollbar.report_exc_info() raise @@ -229,12 +227,12 @@ def to_internal_value(self, data): if (wkaoi and not aoi): try: table, id = wkaoi.split('__') - except: + except Exception: raise ValidationError('wkaoi must be of the form table__id') aoi = get_layer_shape(table, id) if (not aoi): - raise ValidationError(detail='Invalid wkaoi: {}'.format(wkaoi)) + raise ValidationError(detail=f'Invalid wkaoi: {wkaoi}') aoi_field = MultiPolygonGeoJsonField().to_internal_value(aoi) diff --git a/src/mmw/apps/modeling/tasks.py b/src/mmw/apps/modeling/tasks.py index abf9fc665..8fa654e4c 100644 --- a/src/mmw/apps/modeling/tasks.py +++ b/src/mmw/apps/modeling/tasks.py @@ -1,15 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import - import logging import requests import json from ast import literal_eval as make_tuple from requests.exceptions import ConnectionError, Timeout -from StringIO import StringIO +from io import StringIO +from functools import reduce from celery import shared_task @@ -49,7 +46,7 @@ def map_and_convert_units(input): 'runoff': model_output[key]['runoff'] # Already CM } - quality[key] = map(map_and_convert_units, zip(measures, codes)) + quality[key] = list(map(map_and_convert_units, zip(measures, codes))) return quality @@ -183,7 +180,7 @@ def add_huc12(srat_huc12, aggregate): 'SummaryLoads': summary_loads, 'Catchments': {comid: format_catchment(result) for comid, result - in srat_huc12['catchments'].iteritems()}, + in srat_huc12['catchments'].items()}, } aggregate = { @@ -192,13 +189,13 @@ def add_huc12(srat_huc12, aggregate): 'SummaryLoads': empty_source('Entire area'), # All gwlf-e results should have the same inputmod hash, # so grab any of them - 'inputmod_hash': huc12_gwlfe_results.itervalues() - .next()['inputmod_hash'], + 'inputmod_hash': next(iter( + huc12_gwlfe_results.values()))['inputmod_hash'], } aggregate['HUC12s'] = {huc12_id: add_huc12(result, aggregate) for huc12_id, result - in srat_catchment_results['huc12s'].iteritems()} + in srat_catchment_results['huc12s'].items()} return aggregate @@ -206,12 +203,12 @@ def add_huc12(srat_huc12, aggregate): @shared_task(throws=Exception) def nlcd_soil(result): if 'error' in result: - raise Exception('[nlcd_soil] {}'.format(result['error'])) + raise Exception(f'[nlcd_soil] {result["error"]}') dist = {} total_count = 0 - for key, count in result.iteritems(): + for key, count in result.items(): # Extract (3, 4) from "List(3,4)" (n, s) = make_tuple(key[4:]) # Map [NODATA, ad, bd] to c, [cd] to d @@ -373,7 +370,7 @@ def run_subbasin_gwlfe_chunks(mapshed_job_uuid, modifications, @shared_task def run_srat(watersheds, mapshed_job_uuid): try: - data = [format_for_srat(id, w) for id, w in watersheds.iteritems()] + data = [format_for_srat(id, w) for id, w in watersheds.items()] except Exception as e: raise Exception('Formatting sub-basin GWLF-E results failed: %s' % e) @@ -384,7 +381,7 @@ def run_srat(watersheds, mapshed_job_uuid): headers=headers, data=json.dumps(data), timeout=settings.TASK_REQUEST_TIMEOUT) - except Timeout as e: + except Timeout: raise Exception('Request to SRAT Catchment API timed out') except ConnectionError: raise Exception('Failed to connect to SRAT Catchment API') diff --git a/src/mmw/apps/modeling/tests.py b/src/mmw/apps/modeling/tests.py index 345528c9b..3c4e99c3b 100644 --- a/src/mmw/apps/modeling/tests.py +++ b/src/mmw/apps/modeling/tests.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import os from celery import chain, shared_task @@ -136,6 +132,13 @@ def test_census(self): self.assertEqual(actual, expected) +CELERY_TEST_OVERRIDES = { + 'task_always_eager': True, + 'task_store_eager_result': True, + 'task_eager_propagates': True, +} + + class TaskRunnerTestCase(TestCase): def setUp(self): self.model_input = { @@ -206,7 +209,7 @@ def setUp(self): status='started') self.job.save() - @override_settings(CELERY_TASK_ALWAYS_EAGER=True) + @override_settings(**CELERY_TEST_OVERRIDES) def test_tr55_job_runs_in_chain(self): # For the purposes of this test, there are no modifications self.model_input['modification_pieces'] = [] @@ -232,7 +235,7 @@ def test_tr55_job_runs_in_chain(self): 'complete', 'Job found but incomplete.') - @override_settings(CELERY_TASK_ALWAYS_EAGER=True) + @override_settings(**CELERY_TEST_OVERRIDES) def test_tr55_job_error_in_chain(self): model_input = { 'inputs': [], @@ -348,14 +351,14 @@ def test_tr55_chain_doesnt_generate_aoi_census_if_it_exists_and_mods(self): job_chain = views._construct_tr55_job_chain(self.model_input, self.job.id) - cached_argument = ("cached_aoi_census={u'distribution': " - "{u'b:developed_med'" - ": {u'cell_count': 155}, u'a:developed_high': " - "{u'cell_count': 1044}, u'b:developed_high': " - "{u'cell_count': 543}, u'd:developed_high': " - "{u'cell_count': 503}, u'd:developed_med': " - "{u'cell_count': 164}, u'a:developed_med': " - "{u'cell_count': 295}}, u'cell_count': 2704})") + cached_argument = ("cached_aoi_census={'distribution': " + "{'b:developed_med'" + ": {'cell_count': 155}, 'a:developed_high': " + "{'cell_count': 1044}, 'b:developed_high': " + "{'cell_count': 543}, 'd:developed_high': " + "{'cell_count': 503}, 'd:developed_med': " + "{'cell_count': 164}, 'a:developed_med': " + "{'cell_count': 295}}, 'cell_count': 2704})") self.assertTrue(all([True if t in str(job_chain) else False for t in skipped_tasks]), @@ -405,7 +408,7 @@ def test_tr55_chain_doesnt_generate_aoi_census_if_it_exists_and_no_mods(self): else False for t in needed_tasks]), 'missing necessary job in chain') - @override_settings(CELERY_TASK_ALWAYS_EAGER=True) + @override_settings(**CELERY_TEST_OVERRIDES) def test_tr55_chain_generates_modification_censuses_if_they_are_old(self): """If they modification censuses exist in the model input, but the hash stored with the censuses does not match the hash passed in @@ -459,7 +462,7 @@ def test_tr55_chain_generates_modification_censuses_if_they_are_old(self): else False for t in needed_tasks]), 'missing necessary job in chain') - @override_settings(CELERY_TASK_ALWAYS_EAGER=True) + @override_settings(**CELERY_TEST_OVERRIDES) def test_tr55_chain_generates_both_censuses_if_they_are_missing(self): """If neither the AoI censuses or the modification censuses exist, they are both generated. @@ -953,12 +956,10 @@ def tearDown(self): self.weather_data_file.close() def endpoint(self, scenario_id): - return '/mmw/modeling/scenarios/{}/custom-weather-data/'\ - .format(scenario_id) + return f'/mmw/modeling/scenarios/{scenario_id}/custom-weather-data/' def download_endpoint(self, scenario_id): - return '/mmw/modeling/scenarios/{}/custom-weather-data/download/'\ - .format(scenario_id) + return f'/mmw/modeling/scenarios/{scenario_id}/custom-weather-data/download/' # NOQA def delete_weather_dataset(self, path): """ @@ -967,7 +968,7 @@ def delete_weather_dataset(self, path): Only runs if MEDIA_ROOT is defined to prevent accidents. """ if settings.MEDIA_ROOT and path: - os.remove('{}/{}'.format(settings.MEDIA_ROOT, path)) + os.remove(f'{settings.MEDIA_ROOT}/{path}') def create_private_scenario(self): response = self.c.post('/mmw/modeling/projects/', self.project, @@ -993,12 +994,12 @@ def create_public_scenario_with_weather_data(self): project_id = scenario['project'] - response = self.c.get('/mmw/modeling/projects/{}'.format(project_id)) + response = self.c.get(f'/mmw/modeling/projects/{project_id}') project = response.data project['user'] = project['user']['id'] project['is_private'] = False - self.c.patch('/mmw/modeling/projects/{}'.format(project_id), + self.c.patch(f'/mmw/modeling/projects/{project_id}', project, format='json') @@ -1010,7 +1011,7 @@ def create_current_conditions_scenario(self): scenario['name'] = 'Current Conditions' scenario['is_current_conditions'] = True - self.c.put('/mmw/modeling/scenarios/{}'.format(scenario['id']), + self.c.put(f'/mmw/modeling/scenarios/{scenario["id"]}', scenario, format='json') @@ -1195,8 +1196,7 @@ def test_weather_put_project_owner_can(self): scenario = self.create_private_scenario_with_weather_data() scenario['weather_type'] = WeatherType.DEFAULT - response = self.c.put('/mmw/modeling/scenarios/{}' - .format(scenario['id']), + response = self.c.put(f'/mmw/modeling/scenarios/{scenario["id"]}', scenario, format='json') @@ -1209,8 +1209,7 @@ def test_weather_put_logged_out_user_cant(self): scenario['weather_type'] = WeatherType.DEFAULT self.c.logout() - response = self.c.put('/mmw/modeling/scenarios/{}' - .format(scenario['id']), + response = self.c.put(f'/mmw/modeling/scenarios/{scenario["id"]}', scenario, format='json') @@ -1220,8 +1219,7 @@ def test_weather_put_invalid_param_400s(self): scenario = self.create_private_scenario_with_weather_data() scenario['weather_type'] = 'A_WRONG_VALUE' - response = self.c.put('/mmw/modeling/scenarios/{}' - .format(scenario['id']), + response = self.c.put(f'/mmw/modeling/scenarios/{scenario["id"]}', scenario, format='json') @@ -1231,8 +1229,7 @@ def test_weather_put_without_weather_data_400s(self): scenario = self.create_private_scenario() scenario['weather_type'] = WeatherType.CUSTOM - response = self.c.put('/mmw/modeling/scenarios/{}' - .format(scenario['id']), + response = self.c.put(f'/mmw/modeling/scenarios/{scenario["id"]}', scenario, format='json') self.assertEqual(response.status_code, 400) @@ -1241,16 +1238,14 @@ def test_weather_put_cannot_set_on_current_conditions(self): scenario = self.create_current_conditions_scenario() scenario['weather_type'] = WeatherType.CUSTOM - response = self.c.put('/mmw/modeling/scenarios/{}' - .format(scenario['id']), + response = self.c.put(f'/mmw/modeling/scenarios/{scenario["id"]}', scenario, format='json') self.assertEqual(response.status_code, 400) scenario['weather_type'] = WeatherType.SIMULATION - response = self.c.put('/mmw/modeling/scenarios/{}' - .format(scenario['id']), + response = self.c.put(f'/mmw/modeling/scenarios/{scenario["id"]}', scenario, format='json') self.assertEqual(response.status_code, 400) diff --git a/src/mmw/apps/modeling/tr55/utils.py b/src/mmw/apps/modeling/tr55/utils.py index 0bd1c970d..97c72215c 100644 --- a/src/mmw/apps/modeling/tr55/utils.py +++ b/src/mmw/apps/modeling/tr55/utils.py @@ -1,20 +1,16 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import - import json from math import sqrt def aoi_resolution(area_of_interest): - if isinstance(area_of_interest, basestring): + if isinstance(area_of_interest, str): area_of_interest = json.loads(area_of_interest) pairs = area_of_interest['coordinates'][0][0] - average_lat = reduce(lambda total, p: total+p[1], pairs, 0) / len(pairs) + average_lat = sum([p[1] for p in pairs]) / len(pairs) max_lat = 48.7 max_lat_count = 1116 # Number of pixels found in sq km at max lat diff --git a/src/mmw/apps/modeling/urls.py b/src/mmw/apps/modeling/urls.py index 87f64f879..37da535df 100644 --- a/src/mmw/apps/modeling/urls.py +++ b/src/mmw/apps/modeling/urls.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - -from django.conf.urls import url +from django.urls import re_path from apps.modeling import views @@ -17,35 +13,43 @@ app_name = 'modeling' urlpatterns = [ - url(r'projects/$', views.projects, name='projects'), - url(r'projects/(?P[0-9]+)$', views.project, name='project'), - url(r'projects/(?P[0-9]+)/weather/(?P\w+)?$', - views.project_weather, - name='project_weather'), - url(r'scenarios/$', views.scenarios, name='scenarios'), - url(r'scenarios/(?P[0-9]+)$', views.scenario, name='scenario'), - url(r'scenarios/(?P[0-9]+)/duplicate/?$', - views.scenario_duplicate, - name='scenario_duplicate'), - url(r'scenarios/(?P[0-9]+)/custom-weather-data/?$', - views.scenario_custom_weather_data, - name='scenario_custom_weather_data'), - url(r'scenarios/(?P[0-9]+)/custom-weather-data/download/?$', - views.scenario_custom_weather_data_download, - name='scenario_custom_weather_data_download'), - url(r'mapshed/$', views.start_mapshed, name='start_mapshed'), - url(r'jobs/' + uuid_regex, views.get_job, name='get_job'), - url(r'tr55/$', views.start_tr55, name='start_tr55'), - url(r'gwlfe/$', views.start_gwlfe, name='start_gwlfe'), - url(r'subbasins/$', views.subbasins_detail, name='subbasins_detail'), - url(r'subbasins/catchments/$', views.subbasin_catchments_detail, - name='subbasin_catchments_detail'), - url(r'boundary-layers/(?P\w+)/(?P[0-9]+)/$', - views.boundary_layer_detail, name='boundary_layer_detail'), - url(r'boundary-layers-search/$', - views.boundary_layer_search, name='boundary_layer_search'), - url(r'export/gms/?$', views.export_gms, name='export_gms'), - url(r'point-source/$', views.drb_point_sources, name='drb_point_sources'), - url(r'weather-stations/$', views.weather_stations, - name='weather_stations'), + re_path(r'projects/$', views.projects, name='projects'), + re_path(r'projects/(?P[0-9]+)$', views.project, name='project'), + re_path(r'projects/(?P[0-9]+)/weather/(?P\w+)?$', + views.project_weather, + name='project_weather'), + re_path(r'scenarios/$', views.scenarios, name='scenarios'), + re_path(r'scenarios/(?P[0-9]+)$', + views.scenario, + name='scenario'), + re_path(r'scenarios/(?P[0-9]+)/duplicate/?$', + views.scenario_duplicate, + name='scenario_duplicate'), + re_path(r'scenarios/(?P[0-9]+)/custom-weather-data/?$', + views.scenario_custom_weather_data, + name='scenario_custom_weather_data'), + re_path(r'scenarios/(?P[0-9]+)/custom-weather-data/download/?$', + views.scenario_custom_weather_data_download, + name='scenario_custom_weather_data_download'), + re_path(r'mapshed/$', views.start_mapshed, name='start_mapshed'), + re_path(r'jobs/' + uuid_regex, views.get_job, name='get_job'), + re_path(r'tr55/$', views.start_tr55, name='start_tr55'), + re_path(r'gwlfe/$', views.start_gwlfe, name='start_gwlfe'), + re_path(r'subbasins/$', views.subbasins_detail, name='subbasins_detail'), + re_path(r'subbasins/catchments/$', + views.subbasin_catchments_detail, + name='subbasin_catchments_detail'), + re_path(r'boundary-layers/(?P\w+)/(?P[0-9]+)/$', + views.boundary_layer_detail, + name='boundary_layer_detail'), + re_path(r'boundary-layers-search/$', + views.boundary_layer_search, + name='boundary_layer_search'), + re_path(r'export/gms/?$', views.export_gms, name='export_gms'), + re_path(r'point-source/$', + views.drb_point_sources, + name='drb_point_sources'), + re_path(r'weather-stations/$', + views.weather_stations, + name='weather_stations'), ] diff --git a/src/mmw/apps/modeling/validation.py b/src/mmw/apps/modeling/validation.py index ec5ba9aca..840dcf0a6 100644 --- a/src/mmw/apps/modeling/validation.py +++ b/src/mmw/apps/modeling/validation.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - import json from django.conf import settings @@ -32,9 +28,9 @@ def get_aoi_sq_km(aoi): def create_excessive_aoi_size_error_msg(aoi): - return ('Area of interest is too exceeds maximum size: submitted {} sq km ' - 'but the maximum size is {}'.format(get_aoi_sq_km(aoi), - settings.MMW_MAX_AREA)) + return ('Area of interest is too exceeds maximum size:' + f' submitted {get_aoi_sq_km(aoi)} sq km' + f' but the maximum size is {settings.MMW_MAX_AREA}') def check_analyze_aoi_size_below_max_area(aoi): @@ -42,8 +38,7 @@ def check_analyze_aoi_size_below_max_area(aoi): def create_invalid_shape_error_msg(aoi): - return ('Area of interest is invalid: {}'.format( - aoi.valid_reason)) + return f'Area of interest is invalid: {aoi.valid_reason}' def check_aoi_does_not_self_intersect(aoi): diff --git a/src/mmw/apps/modeling/views.py b/src/mmw/apps/modeling/views.py index 6890b2c84..ccd94b4cb 100644 --- a/src/mmw/apps/modeling/views.py +++ b/src/mmw/apps/modeling/views.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - import json import rollbar -import urllib +from contextlib import closing +from urllib.parse import unquote from celery import chain, group @@ -21,7 +19,7 @@ from django.shortcuts import get_object_or_404 from django.utils.timezone import now from django.db import connection -from django.db.models.sql import EmptyResultSet +from django.core.exceptions import EmptyResultSet from django.http import (HttpResponse, Http404, ) @@ -157,8 +155,7 @@ def project_weather(request, proj_id, category): # Report errors as server side, since they are fault with our # built-in data if errs: - rollbar.report_message('Weather Data Errors: {}'.format(errs), - 'error') + rollbar.report_message(f'Weather Data Errors: {errs}', 'error') return Response({'errors': errs}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @@ -235,11 +232,11 @@ def scenario_duplicate(request, scen_id): # Give the scenario a new name. Same logic as in # modeling/models.js:makeNewScenarioName. names = scenario.project.scenarios.values_list('name', flat=True) - copy_name = 'Copy of {}'.format(scenario.name) + copy_name = f'Copy of {scenario.name}' copy_counter = 1 while copy_name in names: - copy_name = 'Copy of {} {}'.format(scenario.name, copy_counter) + copy_name = f'Copy of {scenario.name} {copy_counter}' copy_counter += 1 scenario.name = copy_name @@ -274,7 +271,8 @@ def scenario_custom_weather_data(request, scen_id): if not scenario.weather_custom.name: return Response(status=status.HTTP_404_NOT_FOUND) - mods, errs = get_weather_modifications(scenario.weather_custom) + with closing(scenario.weather_custom): + mods, errs = get_weather_modifications(scenario.weather_custom) return Response({'output': mods, 'errors': errs, 'file_name': scenario.weather_custom.name}) @@ -363,8 +361,7 @@ def scenario_custom_weather_data_download(request, scen_id): filename = cwd.name.split('/')[-1] response = HttpResponse(cwd, content_type='text/csv') - response['Content-Disposition'] = 'attachment; ' \ - 'filename={}'.format(filename) + response['Content-Disposition'] = f'attachment; filename={filename}' return response @@ -458,21 +455,22 @@ def _initiate_subbasin_gwlfe_job_chain(model_input, mapshed_job_uuid, # If we don't chunk, a shape that has 60+ subbasins could take >60sec # to generate a response (and thus timeout) because we'll be waiting to # submit one task for each subbasin. - gwlfe_chunked_group = group(iter([ + gwlfe_chunked_group = group([ tasks.run_subbasin_gwlfe_chunks.s(mapshed_job_uuid, modifications, stream_lengths, inputmod_hash, watershed_id_chunk) .set(link_error=errback) - for watershed_id_chunk in watershed_id_chunks])) + for watershed_id_chunk in watershed_id_chunks]) - post_process = \ - tasks.subbasin_results_to_dict.s().set(link_error=errback) | \ - tasks.run_srat.s(mapshed_job_uuid).set(link_error=errback) | \ - save_job_result.s(job_id, mapshed_job_uuid) + job_chain = ( + gwlfe_chunked_group | + tasks.subbasin_results_to_dict.s().set(link_error=errback) | + tasks.run_srat.s(mapshed_job_uuid).set(link_error=errback) | + save_job_result.s(job_id, mapshed_job_uuid)) - return (gwlfe_chunked_group | post_process).apply_async() + return chain(job_chain).apply_async() @decorators.api_view(['POST']) @@ -514,6 +512,8 @@ def _initiate_subbasin_mapshed_job_chain(mapshed_input, job_id): area_of_interest, wkaoi = _parse_input(mapshed_input) + layer_overrides = mapshed_input.get('layer_overrides', {}) + if not wkaoi: raise ValidationError('You must provide the `wkaoi` key: ' + 'a HUC id is currently required for ' + @@ -528,8 +528,8 @@ def _initiate_subbasin_mapshed_job_chain(mapshed_input, job_id): if not huc12s: raise EmptyResultSet('No subbasins found') - job_chain = (multi_subbasin(area_of_interest, huc12s) | - collect_subbasin.s(huc12s) | + job_chain = (multi_subbasin(area_of_interest, huc12s, layer_overrides) | + collect_subbasin.s(huc12s, layer_overrides=layer_overrides) | tasks.subbasin_results_to_dict.s() | save_job_result.s(job_id, mapshed_input)) @@ -541,10 +541,12 @@ def _initiate_mapshed_job_chain(mapshed_input, job_id): area_of_interest, wkaoi = _parse_input(mapshed_input) + layer_overrides = mapshed_input.get('layer_overrides', {}) + job_chain = ( - multi_mapshed(area_of_interest, wkaoi) | + multi_mapshed(area_of_interest, wkaoi, layer_overrides) | convert_data.s(wkaoi) | - collect_data.s(area_of_interest) | + collect_data.s(area_of_interest, layer_overrides=layer_overrides) | save_job_result.s(job_id, mapshed_input)) return chain(job_chain).apply_async(link_error=errback) @@ -563,8 +565,7 @@ def export_gms(request, format=None): gms_file = tasks.to_gms_file(mapshed_data) response = HttpResponse(FileWrapper(gms_file), content_type='text/plain') - response['Content-Disposition'] = 'attachment; '\ - 'filename={}.gms'.format(filename) + response['Content-Disposition'] = f'attachment; filename={filename}.gms' return response @@ -604,6 +605,7 @@ def _construct_tr55_job_chain(model_input, job_id): aoi = json.loads(aoi_json_str) aoi_census = model_input.get('aoi_census') modification_censuses = model_input.get('modification_censuses') + layer_overrides = model_input.get('layer_overrides', {}) # Non-overlapping polygons derived from the modifications pieces = model_input.get('modification_pieces', []) # The hash of the current modifications @@ -629,8 +631,11 @@ def _construct_tr55_job_chain(model_input, job_id): polygons = [m['shape']['geometry'] for m in pieces] geop_input = {'polygon': [json.dumps(p) for p in polygons]} - job_chain.insert(0, geoprocessing.run.s('nlcd_soil', - geop_input)) + job_chain.insert( + 0, + geoprocessing.run.s('nlcd_soil', + geop_input, + layer_overrides=layer_overrides)) job_chain.append(tasks.run_tr55.s(aoi, model_input, cached_aoi_census=aoi_census)) else: @@ -639,9 +644,12 @@ def _construct_tr55_job_chain(model_input, job_id): # Use WKAoI only if there are no pieces to modify the AoI wkaoi = wkaoi if not pieces else None - job_chain.insert(0, geoprocessing.run.s('nlcd_soil', - geop_input, - wkaoi)) + job_chain.insert( + 0, + geoprocessing.run.s('nlcd_soil', + geop_input, + wkaoi, + layer_overrides=layer_overrides)) job_chain.append(tasks.run_tr55.s(aoi, model_input)) job_chain.append(save_job_result.s(job_id, model_input)) @@ -666,7 +674,7 @@ def subbasins_detail(request): @decorators.permission_classes((AllowAny, )) def subbasin_catchments_detail(request): encoded_comids = request.query_params.get('catchment_comids') - catchment_comids = json.loads(urllib.unquote(encoded_comids)) + catchment_comids = json.loads(unquote(encoded_comids)) if catchment_comids and len(catchment_comids) > 0: catchments = get_catchments(catchment_comids) return Response(catchments) @@ -705,7 +713,7 @@ def drb_point_sources(request): FROM ms_pointsource_drb ''' - point_source_results = {u'type': u'FeatureCollection', u'features': []} + point_source_results = {'type': 'FeatureCollection', 'features': []} with connection.cursor() as cursor: cursor.execute(query) @@ -732,7 +740,7 @@ def drb_point_sources(request): point_source_results['features'] = point_source_array - return Response(json.dumps(point_source_results), + return Response(point_source_results, headers={'Cache-Control': 'max-age: 604800'}) @@ -761,8 +769,9 @@ def weather_stations(request): cursor.execute(query) result = cursor.fetchall()[0][0] - return Response(result, - headers={'Cache-Control': 'max-age: 604800'}) + return HttpResponse(result, + content_type='application/json', + headers={'Cache-Control': 'max-age: 604800'}) @swagger_auto_schema(method='get', diff --git a/src/mmw/apps/monitoring/urls.py b/src/mmw/apps/monitoring/urls.py index 0f41ada74..847c35cd2 100644 --- a/src/mmw/apps/monitoring/urls.py +++ b/src/mmw/apps/monitoring/urls.py @@ -1,12 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - -from django.conf.urls import url +from django.urls import re_path from apps.monitoring.views import health_check app_name = 'monitoring' urlpatterns = [ - url(r'^$', health_check, name='health_check'), + re_path(r'^$', health_check, name='health_check'), ] diff --git a/src/mmw/apps/monitoring/views.py b/src/mmw/apps/monitoring/views.py index 402c59efb..ca69a7f51 100644 --- a/src/mmw/apps/monitoring/views.py +++ b/src/mmw/apps/monitoring/views.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from rest_framework import status from django.http import JsonResponse @@ -21,7 +17,7 @@ def health_check(request): for check in [_check_cache, _check_database]: response.update(check()) - if all(map(lambda x: x[0]['default']['ok'], response.values())): + if all([x[0]['default']['ok'] for x in response.values()]): return JsonResponse(response, status=status.HTTP_200_OK) else: return JsonResponse(response, @@ -29,7 +25,7 @@ def health_check(request): def _check_cache(cache='default'): - key = 'health-check-{}'.format(uuid.uuid4()) + key = f'health-check-{uuid.uuid4()}' try: caches[cache].set(key, uuid.uuid4()) diff --git a/src/mmw/apps/user/admin.py b/src/mmw/apps/user/admin.py index 758d135c9..1dde5a092 100644 --- a/src/mmw/apps/user/admin.py +++ b/src/mmw/apps/user/admin.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from django.contrib import admin from apps.user.models import ItsiUser diff --git a/src/mmw/apps/user/countries.py b/src/mmw/apps/user/countries.py index 2107b5c0a..b5455eb17 100644 --- a/src/mmw/apps/user/countries.py +++ b/src/mmw/apps/user/countries.py @@ -2,253 +2,253 @@ US = 'US' # From https://en.wikipedia.org/wiki/ISO_3166-1#Officially_assigned_code_elements # NOQA COUNTRY_CHOICES = ( - ('AF', u'Afghanistan'), - ('AX', u'Åland Islands'), - ('AL', u'Albania'), - ('DZ', u'Algeria'), - ('AS', u'American Samoa'), - ('AD', u'Andorra'), - ('AO', u'Angola'), - ('AI', u'Anguilla'), - ('AQ', u'Antarctica'), - ('AG', u'Antigua and Barbuda'), - ('AR', u'Argentina'), - ('AM', u'Armenia'), - ('AW', u'Aruba'), - ('AU', u'Australia'), - ('AT', u'Austria'), - ('AZ', u'Azerbaijan'), - ('BS', u'Bahamas'), - ('BH', u'Bahrain'), - ('BD', u'Bangladesh'), - ('BB', u'Barbados'), - ('BY', u'Belarus'), - ('BE', u'Belgium'), - ('BZ', u'Belize'), - ('BJ', u'Benin'), - ('BM', u'Bermuda'), - ('BT', u'Bhutan'), - ('BO', u'Bolivia (Plurinational State of)'), - ('BQ', u'Bonaire, Sint Eustatius and Saba'), - ('BA', u'Bosnia and Herzegovina'), - ('BW', u'Botswana'), - ('BV', u'Bouvet Island'), - ('BR', u'Brazil'), - ('IO', u'British Indian Ocean Territory'), - ('BN', u'Brunei Darussalam'), - ('BG', u'Bulgaria'), - ('BF', u'Burkina Faso'), - ('BI', u'Burundi'), - ('CV', u'Cabo Verde'), - ('KH', u'Cambodia'), - ('CM', u'Cameroon'), - ('CA', u'Canada'), - ('KY', u'Cayman Islands'), - ('CF', u'Central African Republic'), - ('TD', u'Chad'), - ('CL', u'Chile'), - ('CN', u'China'), - ('CX', u'Christmas Island'), - ('CC', u'Cocos (Keeling) Islands'), - ('CO', u'Colombia'), - ('KM', u'Comoros'), - ('CG', u'Congo'), - ('CD', u'Congo (Democratic Republic of the)'), - ('CK', u'Cook Islands'), - ('CR', u'Costa Rica'), - ('CI', u'Côte d\'Ivoire'), - ('HR', u'Croatia'), - ('CU', u'Cuba'), - ('CW', u'Curaçao'), - ('CY', u'Cyprus'), - ('CZ', u'Czechia'), - ('DK', u'Denmark'), - ('DJ', u'Djibouti'), - ('DM', u'Dominica'), - ('DO', u'Dominican Republic'), - ('EC', u'Ecuador'), - ('EG', u'Egypt'), - ('SV', u'El Salvador'), - ('GQ', u'Equatorial Guinea'), - ('ER', u'Eritrea'), - ('EE', u'Estonia'), - ('ET', u'Ethiopia'), - ('FK', u'Falkland Islands (Malvinas)'), - ('FO', u'Faroe Islands'), - ('FJ', u'Fiji'), - ('FI', u'Finland'), - ('FR', u'France'), - ('GF', u'French Guiana'), - ('PF', u'French Polynesia'), - ('TF', u'French Southern Territories'), - ('GA', u'Gabon'), - ('GM', u'Gambia'), - ('GE', u'Georgia'), - ('DE', u'Germany'), - ('GH', u'Ghana'), - ('GI', u'Gibraltar'), - ('GR', u'Greece'), - ('GL', u'Greenland'), - ('GD', u'Grenada'), - ('GP', u'Guadeloupe'), - ('GU', u'Guam'), - ('GT', u'Guatemala'), - ('GG', u'Guernsey'), - ('GN', u'Guinea'), - ('GW', u'Guinea-Bissau'), - ('GY', u'Guyana'), - ('HT', u'Haiti'), - ('HM', u'Heard Island and McDonald Islands'), - ('VA', u'Holy See'), - ('HN', u'Honduras'), - ('HK', u'Hong Kong'), - ('HU', u'Hungary'), - ('IS', u'Iceland'), - ('IN', u'India'), - ('ID', u'Indonesia'), - ('IR', u'Iran (Islamic Republic of)'), - ('IQ', u'Iraq'), - ('IE', u'Ireland'), - ('IM', u'Isle of Man'), - ('IL', u'Israel'), - ('IT', u'Italy'), - ('JM', u'Jamaica'), - ('JP', u'Japan'), - ('JE', u'Jersey'), - ('JO', u'Jordan'), - ('KZ', u'Kazakhstan'), - ('KE', u'Kenya'), - ('KI', u'Kiribati'), - ('KP', u'Korea (Democratic People\'s Republic of)'), - ('KR', u'Korea (Republic of)'), - ('KW', u'Kuwait'), - ('KG', u'Kyrgyzstan'), - ('LA', u'Lao People\'s Democratic Republic'), - ('LV', u'Latvia'), - ('LB', u'Lebanon'), - ('LS', u'Lesotho'), - ('LR', u'Liberia'), - ('LY', u'Libya'), - ('LI', u'Liechtenstein'), - ('LT', u'Lithuania'), - ('LU', u'Luxembourg'), - ('MO', u'Macao'), - ('MK', u'Macedonia (the former Yugoslav Republic of)'), - ('MG', u'Madagascar'), - ('MW', u'Malawi'), - ('MY', u'Malaysia'), - ('MV', u'Maldives'), - ('ML', u'Mali'), - ('MT', u'Malta'), - ('MH', u'Marshall Islands'), - ('MQ', u'Martinique'), - ('MR', u'Mauritania'), - ('MU', u'Mauritius'), - ('YT', u'Mayotte'), - ('MX', u'Mexico'), - ('FM', u'Micronesia (Federated States of)'), - ('MD', u'Moldova (Republic of)'), - ('MC', u'Monaco'), - ('MN', u'Mongolia'), - ('ME', u'Montenegro'), - ('MS', u'Montserrat'), - ('MA', u'Morocco'), - ('MZ', u'Mozambique'), - ('MM', u'Myanmar'), - ('NA', u'Namibia'), - ('NR', u'Nauru'), - ('NP', u'Nepal'), - ('NL', u'Netherlands'), - ('NC', u'New Caledonia'), - ('NZ', u'New Zealand'), - ('NI', u'Nicaragua'), - ('NE', u'Niger'), - ('NG', u'Nigeria'), - ('NU', u'Niue'), - ('NF', u'Norfolk Island'), - ('MP', u'Northern Mariana Islands'), - ('NO', u'Norway'), - ('OM', u'Oman'), - ('PK', u'Pakistan'), - ('PW', u'Palau'), - ('PS', u'Palestine, State of'), - ('PA', u'Panama'), - ('PG', u'Papua New Guinea'), - ('PY', u'Paraguay'), - ('PE', u'Peru'), - ('PH', u'Philippines'), - ('PN', u'Pitcairn'), - ('PL', u'Poland'), - ('PT', u'Portugal'), - ('PR', u'Puerto Rico'), - ('QA', u'Qatar'), - ('RE', u'Réunion'), - ('RO', u'Romania'), - ('RU', u'Russian Federation'), - ('RW', u'Rwanda'), - ('BL', u'Saint Barthélemy'), - ('SH', u'Saint Helena, Ascension and Tristan da Cunha'), - ('KN', u'Saint Kitts and Nevis'), - ('LC', u'Saint Lucia'), - ('MF', u'Saint Martin (French part)'), - ('PM', u'Saint Pierre and Miquelon'), - ('VC', u'Saint Vincent and the Grenadines'), - ('WS', u'Samoa'), - ('SM', u'San Marino'), - ('ST', u'Sao Tome and Principe'), - ('SA', u'Saudi Arabia'), - ('SN', u'Senegal'), - ('RS', u'Serbia'), - ('SC', u'Seychelles'), - ('SL', u'Sierra Leone'), - ('SG', u'Singapore'), - ('SX', u'Sint Maarten (Dutch part)'), - ('SK', u'Slovakia'), - ('SI', u'Slovenia'), - ('SB', u'Solomon Islands'), - ('SO', u'Somalia'), - ('ZA', u'South Africa'), - ('GS', u'South Georgia and the South Sandwich Islands'), - ('SS', u'South Sudan'), - ('ES', u'Spain'), - ('LK', u'Sri Lanka'), - ('SD', u'Sudan'), - ('SR', u'Suriname'), - ('SJ', u'Svalbard and Jan Mayen'), - ('SZ', u'Swaziland'), - ('SE', u'Sweden'), - ('CH', u'Switzerland'), - ('SY', u'Syrian Arab Republic'), - ('TW', u'Taiwan, Province of China[a]'), - ('TJ', u'Tajikistan'), - ('TZ', u'Tanzania, United Republic of'), - ('TH', u'Thailand'), - ('TL', u'Timor-Leste'), - ('TG', u'Togo'), - ('TK', u'Tokelau'), - ('TO', u'Tonga'), - ('TT', u'Trinidad and Tobago'), - ('TN', u'Tunisia'), - ('TR', u'Turkey'), - ('TM', u'Turkmenistan'), - ('TC', u'Turks and Caicos Islands'), - ('TV', u'Tuvalu'), - ('UG', u'Uganda'), - ('UA', u'Ukraine'), - ('AE', u'United Arab Emirates'), - ('GB', u'United Kingdom of Great Britain and Northern Ireland'), - ('US', u'United States of America'), - ('UM', u'United States Minor Outlying Islands'), - ('UY', u'Uruguay'), - ('UZ', u'Uzbekistan'), - ('VU', u'Vanuatu'), - ('VE', u'Venezuela (Bolivarian Republic of)'), - ('VN', u'Viet Nam'), - ('VG', u'Virgin Islands (British)'), - ('VI', u'Virgin Islands (U.S.)'), - ('WF', u'Wallis and Futuna'), - ('EH', u'Western Sahara'), - ('YE', u'Yemen'), - ('ZM', u'Zambia'), - ('ZW', u'Zimbabwe'), + ('AF', 'Afghanistan'), + ('AX', 'Åland Islands'), + ('AL', 'Albania'), + ('DZ', 'Algeria'), + ('AS', 'American Samoa'), + ('AD', 'Andorra'), + ('AO', 'Angola'), + ('AI', 'Anguilla'), + ('AQ', 'Antarctica'), + ('AG', 'Antigua and Barbuda'), + ('AR', 'Argentina'), + ('AM', 'Armenia'), + ('AW', 'Aruba'), + ('AU', 'Australia'), + ('AT', 'Austria'), + ('AZ', 'Azerbaijan'), + ('BS', 'Bahamas'), + ('BH', 'Bahrain'), + ('BD', 'Bangladesh'), + ('BB', 'Barbados'), + ('BY', 'Belarus'), + ('BE', 'Belgium'), + ('BZ', 'Belize'), + ('BJ', 'Benin'), + ('BM', 'Bermuda'), + ('BT', 'Bhutan'), + ('BO', 'Bolivia (Plurinational State of)'), + ('BQ', 'Bonaire, Sint Eustatius and Saba'), + ('BA', 'Bosnia and Herzegovina'), + ('BW', 'Botswana'), + ('BV', 'Bouvet Island'), + ('BR', 'Brazil'), + ('IO', 'British Indian Ocean Territory'), + ('BN', 'Brunei Darussalam'), + ('BG', 'Bulgaria'), + ('BF', 'Burkina Faso'), + ('BI', 'Burundi'), + ('CV', 'Cabo Verde'), + ('KH', 'Cambodia'), + ('CM', 'Cameroon'), + ('CA', 'Canada'), + ('KY', 'Cayman Islands'), + ('CF', 'Central African Republic'), + ('TD', 'Chad'), + ('CL', 'Chile'), + ('CN', 'China'), + ('CX', 'Christmas Island'), + ('CC', 'Cocos (Keeling) Islands'), + ('CO', 'Colombia'), + ('KM', 'Comoros'), + ('CG', 'Congo'), + ('CD', 'Congo (Democratic Republic of the)'), + ('CK', 'Cook Islands'), + ('CR', 'Costa Rica'), + ('CI', 'Côte d\'Ivoire'), + ('HR', 'Croatia'), + ('CU', 'Cuba'), + ('CW', 'Curaçao'), + ('CY', 'Cyprus'), + ('CZ', 'Czechia'), + ('DK', 'Denmark'), + ('DJ', 'Djibouti'), + ('DM', 'Dominica'), + ('DO', 'Dominican Republic'), + ('EC', 'Ecuador'), + ('EG', 'Egypt'), + ('SV', 'El Salvador'), + ('GQ', 'Equatorial Guinea'), + ('ER', 'Eritrea'), + ('EE', 'Estonia'), + ('ET', 'Ethiopia'), + ('FK', 'Falkland Islands (Malvinas)'), + ('FO', 'Faroe Islands'), + ('FJ', 'Fiji'), + ('FI', 'Finland'), + ('FR', 'France'), + ('GF', 'French Guiana'), + ('PF', 'French Polynesia'), + ('TF', 'French Southern Territories'), + ('GA', 'Gabon'), + ('GM', 'Gambia'), + ('GE', 'Georgia'), + ('DE', 'Germany'), + ('GH', 'Ghana'), + ('GI', 'Gibraltar'), + ('GR', 'Greece'), + ('GL', 'Greenland'), + ('GD', 'Grenada'), + ('GP', 'Guadeloupe'), + ('GU', 'Guam'), + ('GT', 'Guatemala'), + ('GG', 'Guernsey'), + ('GN', 'Guinea'), + ('GW', 'Guinea-Bissau'), + ('GY', 'Guyana'), + ('HT', 'Haiti'), + ('HM', 'Heard Island and McDonald Islands'), + ('VA', 'Holy See'), + ('HN', 'Honduras'), + ('HK', 'Hong Kong'), + ('HU', 'Hungary'), + ('IS', 'Iceland'), + ('IN', 'India'), + ('ID', 'Indonesia'), + ('IR', 'Iran (Islamic Republic of)'), + ('IQ', 'Iraq'), + ('IE', 'Ireland'), + ('IM', 'Isle of Man'), + ('IL', 'Israel'), + ('IT', 'Italy'), + ('JM', 'Jamaica'), + ('JP', 'Japan'), + ('JE', 'Jersey'), + ('JO', 'Jordan'), + ('KZ', 'Kazakhstan'), + ('KE', 'Kenya'), + ('KI', 'Kiribati'), + ('KP', 'Korea (Democratic People\'s Republic of)'), + ('KR', 'Korea (Republic of)'), + ('KW', 'Kuwait'), + ('KG', 'Kyrgyzstan'), + ('LA', 'Lao People\'s Democratic Republic'), + ('LV', 'Latvia'), + ('LB', 'Lebanon'), + ('LS', 'Lesotho'), + ('LR', 'Liberia'), + ('LY', 'Libya'), + ('LI', 'Liechtenstein'), + ('LT', 'Lithuania'), + ('LU', 'Luxembourg'), + ('MO', 'Macao'), + ('MK', 'Macedonia (the former Yugoslav Republic of)'), + ('MG', 'Madagascar'), + ('MW', 'Malawi'), + ('MY', 'Malaysia'), + ('MV', 'Maldives'), + ('ML', 'Mali'), + ('MT', 'Malta'), + ('MH', 'Marshall Islands'), + ('MQ', 'Martinique'), + ('MR', 'Mauritania'), + ('MU', 'Mauritius'), + ('YT', 'Mayotte'), + ('MX', 'Mexico'), + ('FM', 'Micronesia (Federated States of)'), + ('MD', 'Moldova (Republic of)'), + ('MC', 'Monaco'), + ('MN', 'Mongolia'), + ('ME', 'Montenegro'), + ('MS', 'Montserrat'), + ('MA', 'Morocco'), + ('MZ', 'Mozambique'), + ('MM', 'Myanmar'), + ('NA', 'Namibia'), + ('NR', 'Nauru'), + ('NP', 'Nepal'), + ('NL', 'Netherlands'), + ('NC', 'New Caledonia'), + ('NZ', 'New Zealand'), + ('NI', 'Nicaragua'), + ('NE', 'Niger'), + ('NG', 'Nigeria'), + ('NU', 'Niue'), + ('NF', 'Norfolk Island'), + ('MP', 'Northern Mariana Islands'), + ('NO', 'Norway'), + ('OM', 'Oman'), + ('PK', 'Pakistan'), + ('PW', 'Palau'), + ('PS', 'Palestine, State of'), + ('PA', 'Panama'), + ('PG', 'Papua New Guinea'), + ('PY', 'Paraguay'), + ('PE', 'Peru'), + ('PH', 'Philippines'), + ('PN', 'Pitcairn'), + ('PL', 'Poland'), + ('PT', 'Portugal'), + ('PR', 'Puerto Rico'), + ('QA', 'Qatar'), + ('RE', 'Réunion'), + ('RO', 'Romania'), + ('RU', 'Russian Federation'), + ('RW', 'Rwanda'), + ('BL', 'Saint Barthélemy'), + ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), + ('KN', 'Saint Kitts and Nevis'), + ('LC', 'Saint Lucia'), + ('MF', 'Saint Martin (French part)'), + ('PM', 'Saint Pierre and Miquelon'), + ('VC', 'Saint Vincent and the Grenadines'), + ('WS', 'Samoa'), + ('SM', 'San Marino'), + ('ST', 'Sao Tome and Principe'), + ('SA', 'Saudi Arabia'), + ('SN', 'Senegal'), + ('RS', 'Serbia'), + ('SC', 'Seychelles'), + ('SL', 'Sierra Leone'), + ('SG', 'Singapore'), + ('SX', 'Sint Maarten (Dutch part)'), + ('SK', 'Slovakia'), + ('SI', 'Slovenia'), + ('SB', 'Solomon Islands'), + ('SO', 'Somalia'), + ('ZA', 'South Africa'), + ('GS', 'South Georgia and the South Sandwich Islands'), + ('SS', 'South Sudan'), + ('ES', 'Spain'), + ('LK', 'Sri Lanka'), + ('SD', 'Sudan'), + ('SR', 'Suriname'), + ('SJ', 'Svalbard and Jan Mayen'), + ('SZ', 'Swaziland'), + ('SE', 'Sweden'), + ('CH', 'Switzerland'), + ('SY', 'Syrian Arab Republic'), + ('TW', 'Taiwan, Province of China[a]'), + ('TJ', 'Tajikistan'), + ('TZ', 'Tanzania, United Republic of'), + ('TH', 'Thailand'), + ('TL', 'Timor-Leste'), + ('TG', 'Togo'), + ('TK', 'Tokelau'), + ('TO', 'Tonga'), + ('TT', 'Trinidad and Tobago'), + ('TN', 'Tunisia'), + ('TR', 'Turkey'), + ('TM', 'Turkmenistan'), + ('TC', 'Turks and Caicos Islands'), + ('TV', 'Tuvalu'), + ('UG', 'Uganda'), + ('UA', 'Ukraine'), + ('AE', 'United Arab Emirates'), + ('GB', 'United Kingdom of Great Britain and Northern Ireland'), + ('US', 'United States of America'), + ('UM', 'United States Minor Outlying Islands'), + ('UY', 'Uruguay'), + ('UZ', 'Uzbekistan'), + ('VU', 'Vanuatu'), + ('VE', 'Venezuela (Bolivarian Republic of)'), + ('VN', 'Viet Nam'), + ('VG', 'Virgin Islands (British)'), + ('VI', 'Virgin Islands (U.S.)'), + ('WF', 'Wallis and Futuna'), + ('EH', 'Western Sahara'), + ('YE', 'Yemen'), + ('ZM', 'Zambia'), + ('ZW', 'Zimbabwe'), ) diff --git a/src/mmw/apps/user/management/commands/drbusers.py b/src/mmw/apps/user/management/commands/drbusers.py index ede1c5b9e..8b2dfb7a7 100644 --- a/src/mmw/apps/user/management/commands/drbusers.py +++ b/src/mmw/apps/user/management/commands/drbusers.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division - from django.core.management.base import BaseCommand from apps.user.models import UserProfile @@ -152,4 +148,4 @@ def handle(self, *args, **options): u.postal_code )) - print('\n'.join(rows)) + print(('\n'.join(rows))) diff --git a/src/mmw/apps/user/middleware.py b/src/mmw/apps/user/middleware.py index 206855583..9d3f329ba 100644 --- a/src/mmw/apps/user/middleware.py +++ b/src/mmw/apps/user/middleware.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - from django.conf import settings diff --git a/src/mmw/apps/user/migrations/0001_initial.py b/src/mmw/apps/user/migrations/0001_initial.py index 854f0e9e6..99b1dfe32 100644 --- a/src/mmw/apps/user/migrations/0001_initial.py +++ b/src/mmw/apps/user/migrations/0001_initial.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations from django.conf import settings import django.db.models.deletion diff --git a/src/mmw/apps/user/migrations/0002_auth_tokens.py b/src/mmw/apps/user/migrations/0002_auth_tokens.py index ee42dd318..c59d7a28c 100644 --- a/src/mmw/apps/user/migrations/0002_auth_tokens.py +++ b/src/mmw/apps/user/migrations/0002_auth_tokens.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations from django.conf import settings from django.contrib.auth.models import User diff --git a/src/mmw/apps/user/migrations/0003_client_app_user.py b/src/mmw/apps/user/migrations/0003_client_app_user.py index 37555888d..7308494d4 100644 --- a/src/mmw/apps/user/migrations/0003_client_app_user.py +++ b/src/mmw/apps/user/migrations/0003_client_app_user.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations from django.conf import settings from django.contrib.auth.models import User diff --git a/src/mmw/apps/user/migrations/0004_userprofile.py b/src/mmw/apps/user/migrations/0004_userprofile.py index 8d4337e8a..b0f3ee961 100644 --- a/src/mmw/apps/user/migrations/0004_userprofile.py +++ b/src/mmw/apps/user/migrations/0004_userprofile.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models from django.conf import settings import django.db.models.deletion diff --git a/src/mmw/apps/user/migrations/0005_hydrosharetoken.py b/src/mmw/apps/user/migrations/0005_hydrosharetoken.py index 306126e6d..589d680cc 100644 --- a/src/mmw/apps/user/migrations/0005_hydrosharetoken.py +++ b/src/mmw/apps/user/migrations/0005_hydrosharetoken.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models from django.conf import settings import django.db.models.deletion diff --git a/src/mmw/apps/user/migrations/0006_userprofile_has_seen_hotspot_info.py b/src/mmw/apps/user/migrations/0006_userprofile_has_seen_hotspot_info.py index 4bd709a92..5e9c7e8cf 100644 --- a/src/mmw/apps/user/migrations/0006_userprofile_has_seen_hotspot_info.py +++ b/src/mmw/apps/user/migrations/0006_userprofile_has_seen_hotspot_info.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/user/migrations/0007_userprofile_unit_scheme.py b/src/mmw/apps/user/migrations/0007_userprofile_unit_scheme.py index f250e19d9..cf0284a7c 100644 --- a/src/mmw/apps/user/migrations/0007_userprofile_unit_scheme.py +++ b/src/mmw/apps/user/migrations/0007_userprofile_unit_scheme.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/src/mmw/apps/user/migrations/0008_concorduser.py b/src/mmw/apps/user/migrations/0008_concorduser.py index 4eba1b5da..268411454 100644 --- a/src/mmw/apps/user/migrations/0008_concorduser.py +++ b/src/mmw/apps/user/migrations/0008_concorduser.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.29 on 2020-04-02 23:49 -from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models diff --git a/src/mmw/apps/user/migrations/0009_django_upgrade_string_edits.py b/src/mmw/apps/user/migrations/0009_django_upgrade_string_edits.py new file mode 100644 index 000000000..dbbcb62ca --- /dev/null +++ b/src/mmw/apps/user/migrations/0009_django_upgrade_string_edits.py @@ -0,0 +1,38 @@ +# Generated by Django 2.0.13 on 2021-12-16 01:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0008_concorduser'), + ] + + operations = [ + migrations.AlterField( + model_name='hydrosharetoken', + name='scope', + field=models.CharField(default='read write', max_length=255), + ), + migrations.AlterField( + model_name='hydrosharetoken', + name='token_type', + field=models.CharField(default='Bearer', max_length=255), + ), + migrations.AlterField( + model_name='userprofile', + name='country', + field=models.TextField(choices=[('AF', 'Afghanistan'), ('AX', 'Åland Islands'), ('AL', 'Albania'), ('DZ', 'Algeria'), ('AS', 'American Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarctica'), ('AG', 'Antigua and Barbuda'), ('AR', 'Argentina'), ('AM', 'Armenia'), ('AW', 'Aruba'), ('AU', 'Australia'), ('AT', 'Austria'), ('AZ', 'Azerbaijan'), ('BS', 'Bahamas'), ('BH', 'Bahrain'), ('BD', 'Bangladesh'), ('BB', 'Barbados'), ('BY', 'Belarus'), ('BE', 'Belgium'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivia (Plurinational State of)'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('BA', 'Bosnia and Herzegovina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brazil'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei Darussalam'), ('BG', 'Bulgaria'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('CV', 'Cabo Verde'), ('KH', 'Cambodia'), ('CM', 'Cameroon'), ('CA', 'Canada'), ('KY', 'Cayman Islands'), ('CF', 'Central African Republic'), ('TD', 'Chad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Colombia'), ('KM', 'Comoros'), ('CG', 'Congo'), ('CD', 'Congo (Democratic Republic of the)'), ('CK', 'Cook Islands'), ('CR', 'Costa Rica'), ('CI', "Côte d'Ivoire"), ('HR', 'Croatia'), ('CU', 'Cuba'), ('CW', 'Curaçao'), ('CY', 'Cyprus'), ('CZ', 'Czechia'), ('DK', 'Denmark'), ('DJ', 'Djibouti'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('EC', 'Ecuador'), ('EG', 'Egypt'), ('SV', 'El Salvador'), ('GQ', 'Equatorial Guinea'), ('ER', 'Eritrea'), ('EE', 'Estonia'), ('ET', 'Ethiopia'), ('FK', 'Falkland Islands (Malvinas)'), ('FO', 'Faroe Islands'), ('FJ', 'Fiji'), ('FI', 'Finland'), ('FR', 'France'), ('GF', 'French Guiana'), ('PF', 'French Polynesia'), ('TF', 'French Southern Territories'), ('GA', 'Gabon'), ('GM', 'Gambia'), ('GE', 'Georgia'), ('DE', 'Germany'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Greece'), ('GL', 'Greenland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard Island and McDonald Islands'), ('VA', 'Holy See'), ('HN', 'Honduras'), ('HK', 'Hong Kong'), ('HU', 'Hungary'), ('IS', 'Iceland'), ('IN', 'India'), ('ID', 'Indonesia'), ('IR', 'Iran (Islamic Republic of)'), ('IQ', 'Iraq'), ('IE', 'Ireland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kazakhstan'), ('KE', 'Kenya'), ('KI', 'Kiribati'), ('KP', "Korea (Democratic People's Republic of)"), ('KR', 'Korea (Republic of)'), ('KW', 'Kuwait'), ('KG', 'Kyrgyzstan'), ('LA', "Lao People's Democratic Republic"), ('LV', 'Latvia'), ('LB', 'Lebanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libya'), ('LI', 'Liechtenstein'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MO', 'Macao'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexico'), ('FM', 'Micronesia (Federated States of)'), ('MD', 'Moldova (Republic of)'), ('MC', 'Monaco'), ('MN', 'Mongolia'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Morocco'), ('MZ', 'Mozambique'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Netherlands'), ('NC', 'New Caledonia'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norway'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palestine, State of'), ('PA', 'Panama'), ('PG', 'Papua New Guinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippines'), ('PN', 'Pitcairn'), ('PL', 'Poland'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Qatar'), ('RE', 'Réunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('BL', 'Saint Barthélemy'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('KN', 'Saint Kitts and Nevis'), ('LC', 'Saint Lucia'), ('MF', 'Saint Martin (French part)'), ('PM', 'Saint Pierre and Miquelon'), ('VC', 'Saint Vincent and the Grenadines'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome and Principe'), ('SA', 'Saudi Arabia'), ('SN', 'Senegal'), ('RS', 'Serbia'), ('SC', 'Seychelles'), ('SL', 'Sierra Leone'), ('SG', 'Singapore'), ('SX', 'Sint Maarten (Dutch part)'), ('SK', 'Slovakia'), ('SI', 'Slovenia'), ('SB', 'Solomon Islands'), ('SO', 'Somalia'), ('ZA', 'South Africa'), ('GS', 'South Georgia and the South Sandwich Islands'), ('SS', 'South Sudan'), ('ES', 'Spain'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard and Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Sweden'), ('CH', 'Switzerland'), ('SY', 'Syrian Arab Republic'), ('TW', 'Taiwan, Province of China[a]'), ('TJ', 'Tajikistan'), ('TZ', 'Tanzania, United Republic of'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad and Tobago'), ('TN', 'Tunisia'), ('TR', 'Turkey'), ('TM', 'Turkmenistan'), ('TC', 'Turks and Caicos Islands'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'United Arab Emirates'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('US', 'United States of America'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela (Bolivarian Republic of)'), ('VN', 'Viet Nam'), ('VG', 'Virgin Islands (British)'), ('VI', 'Virgin Islands (U.S.)'), ('WF', 'Wallis and Futuna'), ('EH', 'Western Sahara'), ('YE', 'Yemen'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')], default='US'), + ), + migrations.AlterField( + model_name='userprofile', + name='unit_scheme', + field=models.TextField(choices=[('METRIC', 'Metric'), ('USCUSTOMARY', 'US Customary')], default='METRIC'), + ), + migrations.AlterField( + model_name='userprofile', + name='user_type', + field=models.TextField(choices=[('Unspecified', 'Unspecified'), ('University Faculty', 'University Faculty'), ('University Professional or Research Staff', 'University Professional or Research Staff'), ('Post-Doctoral Fellow', 'Post-Doctoral Fellow'), ('University Graduate Student', 'University Graduate Student'), ('University Undergraduate Student', 'University Undergraduate Student'), ('Commercial/Professional', 'Commercial/Professional'), ('Government Official', 'Government Official'), ('School Student Kindergarten to 12th Grade', 'School Student Kindergarten to 12th Grade'), ('School Teacher Kindergarten to 12th Grade', 'School Teacher Kindergarten to 12th Grade'), ('Other', 'Other')], default='Unspecified'), + ), + ] diff --git a/src/mmw/apps/user/models.py b/src/mmw/apps/user/models.py index abfcdb98e..5346147a9 100644 --- a/src/mmw/apps/user/models.py +++ b/src/mmw/apps/user/models.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - from datetime import timedelta from django.conf import settings @@ -13,7 +12,7 @@ from apps.core.models import UnitScheme -import countries +from apps.user import countries @receiver(post_save, sender=settings.AUTH_USER_MODEL) @@ -40,7 +39,7 @@ class ItsiUser(models.Model): objects = ItsiUserManager() def __unicode__(self): - return unicode(self.user.username) + return str(self.user.username) class ConcordUser(models.Model): @@ -50,7 +49,7 @@ class ConcordUser(models.Model): concord_id = models.IntegerField() def __unicode__(self): - return unicode(self.user.username) + return str(self.user.username) class UserProfile(models.Model): diff --git a/src/mmw/apps/user/serializers.py b/src/mmw/apps/user/serializers.py index d84f4054e..05d3d4ff5 100644 --- a/src/mmw/apps/user/serializers.py +++ b/src/mmw/apps/user/serializers.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - from django.contrib.auth.models import User -from models import UserProfile from rest_framework import serializers +from apps.user.models import UserProfile + class UserSerializer(serializers.ModelSerializer): class Meta: diff --git a/src/mmw/apps/user/sso.py b/src/mmw/apps/user/sso.py index 3e025558f..de2dc9bf2 100644 --- a/src/mmw/apps/user/sso.py +++ b/src/mmw/apps/user/sso.py @@ -2,7 +2,7 @@ import json from django.conf import settings -from urlparse import urljoin +from urllib.parse import urljoin from rauth import OAuth2Service, OAuth2Session diff --git a/src/mmw/apps/user/templates/user/hydroshare-auth.html b/src/mmw/apps/user/templates/user/hydroshare-auth.html index 0dea4fa23..85f3d9aa3 100644 --- a/src/mmw/apps/user/templates/user/hydroshare-auth.html +++ b/src/mmw/apps/user/templates/user/hydroshare-auth.html @@ -1,5 +1,5 @@ {% include 'head.html' %} -{% load staticfiles %} +{% load static %}
-1) - { + if (name.startsWith('land_') || name.startsWith('drb_2100_land_')) { return 'nlcd-fill-' + item.nlcd; } else if (name === 'soil') { return 'soil-fill-' + item.code; @@ -1409,11 +1406,14 @@ var AnalyzeResultView = Marionette.LayoutView.extend({ }); var LandResultView = AnalyzeResultView.extend({ - onShowNlcd: function() { - var title = 'Land cover distribution', - source = 'National Land Cover Database (NLCD 2011)', + onShowNlcd: function(taskName) { + var year = taskName.substring(10), // land_2019_2011 => 2011 + nlcd = 'NLCD' + taskName.substring(7, 9), // land_2019_2011 => NLCD19 + nlcd_year = taskName.substring(5, 9), // land_2019_2011 => 2019 + title = 'Land Use/Cover ' + year + ' (' + nlcd + ')', + source = 'National Land Cover Database (NLCD ' + nlcd_year + ')', helpText = 'For more information and data sources, see Model My Watershed Technical Documentation on Coverage Grids', - associatedLayerCodes = ['nlcd']; + associatedLayerCodes = ['nlcd-' + taskName.substring(5)]; // land_2019_2011 => nlcd-2019_2011 this.showAnalyzeResults(coreModels.LandUseCensusCollection, TableView, ChartView, title, source, helpText, associatedLayerCodes); }, @@ -1454,9 +1454,9 @@ var LandResultView = AnalyzeResultView.extend({ case 'drb_2100_land_corridors': this.onShowFutureLandCorridors(); break; - case 'land': default: - this.onShowNlcd(); + // e.g. taskName === land_2019_2011 + this.onShowNlcd(taskName); } } }); @@ -1685,10 +1685,17 @@ var ClimateResultView = AnalyzeResultView.extend({ var StreamResultView = AnalyzeResultView.extend({ onShow: function() { - var title = 'Stream Network Statistics', - source = 'NHDplusV2', + var taskName = this.model.get('name'), + title = taskName === 'streams_nhdhr' ? + 'NHD High Resolution Stream Network Statistics' : + 'NHD Medium Resolution Stream Network Statistics', + source = taskName === 'streams_nhdhr' ? + 'NHDplusHR' : + 'NHDplusV2', helpText = 'For more information on the data source, see MMW Technical Documentation', - associatedLayerCodes = ['nhd_streams_v2'], + associatedLayerCodes = taskName === 'streams_nhdhr' ? + ['nhd_streams_hr_v1'] : + ['nhd_streams_v2'], chart = null, streamOrderHelpText = [ { @@ -1711,13 +1718,19 @@ var StreamResultView = AnalyzeResultView.extend({ }); var AnalyzeResultViews = { - land: LandResultView, + land_2019_2019: LandResultView, + land_2019_2016: LandResultView, + land_2019_2011: LandResultView, + land_2019_2006: LandResultView, + land_2019_2001: LandResultView, + land_2011_2011: LandResultView, soil: SoilResultView, animals: AnimalsResultView, pointsource: PointSourceResultView, catchment_water_quality: CatchmentWaterQualityResultView, climate: ClimateResultView, - streams: StreamResultView, + streams_nhd: StreamResultView, + streams_nhdhr: StreamResultView, terrain: TerrainResultView, protected_lands: LandResultView, drb_2100_land_centers: LandResultView, diff --git a/src/mmw/js/src/app.js b/src/mmw/js/src/app.js index 632be4857..37637c291 100644 --- a/src/mmw/js/src/app.js +++ b/src/mmw/js/src/app.js @@ -405,6 +405,13 @@ function initializeShutterbug() { } function fetchVersion() { + if (document.location.hostname === 'localhost') { + // Local development environment, don't fetch version.txt + settings.set('branch', 'local'); + settings.set('gitDescribe', null); + return; + } + $.get('/version.txt') .done(function(data) { var versions = data.match(/(\S+)\s+(\S+)/), @@ -415,15 +422,8 @@ function fetchVersion() { settings.set('gitDescribe', gitDescribe); }) .fail(function(error) { - if (error.status === 404) { - // No /version.txt found, this could be a development environment - settings.set('branch', 'local'); - settings.set('gitDescribe', null); - } else { - // Some other error occurred. Could indicate a faulty deployment - settings.set('branch', null); - settings.set('gitDescribe', null); - } + settings.set('branch', null); + settings.set('gitDescribe', null); }); } diff --git a/src/mmw/js/src/compare/views.js b/src/mmw/js/src/compare/views.js index 8a37f2d9e..22fb2d76e 100644 --- a/src/mmw/js/src/compare/views.js +++ b/src/mmw/js/src/compare/views.js @@ -517,7 +517,7 @@ var CompareModificationsPopoverView = Marionette.ItemView.extend({ if (modKey === 'entry_landcover') { name = _.find(GWLFE_LAND_COVERS, { id: parseInt(key.substring(6)) }).label; - value = value.toFixed(1); + value = coreUnits.get('AREA_L_FROM_HA', value).value.toFixed(1); input = coreUnits[scheme].AREA_L_FROM_HA.name; } else if (modKey === 'entry_landcover_preset') { var task = App.getAnalyzeCollection() @@ -533,6 +533,7 @@ var CompareModificationsPopoverView = Marionette.ItemView.extend({ value = null; input = task && task.get('displayName'); } else { + value = coreUnits.get(unit, value).value.toFixed(3); input = input.replace('AREAUNITNAME', areaUnit); } diff --git a/src/mmw/js/src/core/models.js b/src/mmw/js/src/core/models.js index 93530540f..0a716632b 100644 --- a/src/mmw/js/src/core/models.js +++ b/src/mmw/js/src/core/models.js @@ -609,13 +609,9 @@ var LayerTabCollection = Backbone.Collection.extend({ var TaskModel = Backbone.Model.extend({ defaults: { pollInterval: 1000, - /* The timeout is set to 160 seconds here. It may be set - differently in other parts of the app, in most cases less. - The front-end timeout is the highest to allow for patient - users time to let large processing finish (subbasin, large - areas of interest, etc). In most cases, back-end jobs will - finish or fail before this is hit. */ - timeout: 160000, + + // As many seconds as the max configured limit in the back-end + timeout: settings.get('celery_task_time_limit') * 1000, }, // Log a debug mesasge if available, plain otherwise diff --git a/src/mmw/js/src/core/settings.js b/src/mmw/js/src/core/settings.js index f945bc6d2..aae1fbc94 100644 --- a/src/mmw/js/src/core/settings.js +++ b/src/mmw/js/src/core/settings.js @@ -22,6 +22,7 @@ var defaultSettings = { enabled_features: [], branch: null, gitDescribe: null, + celery_task_time_limit: 120, }; var settings = (function() { diff --git a/src/mmw/js/src/draw/utils.js b/src/mmw/js/src/draw/utils.js index a56fe80b2..4e67b2bbd 100644 --- a/src/mmw/js/src/draw/utils.js +++ b/src/mmw/js/src/draw/utils.js @@ -262,6 +262,7 @@ module.exports = { getSquareKmBoxForPoint: getSquareKmBoxForPoint, loadAsyncShpFilesFromZip: loadAsyncShpFilesFromZip, NHD: 'nhd', + NHDHR: 'nhdhr', DRB: 'drb', CANCEL_DRAWING: CANCEL_DRAWING, }; diff --git a/src/mmw/js/src/draw/views.js b/src/mmw/js/src/draw/views.js index 884ef9d81..105bd39c2 100644 --- a/src/mmw/js/src/draw/views.js +++ b/src/mmw/js/src/draw/views.js @@ -142,6 +142,7 @@ function validatePointWithinDataSourceBounds(latlng, dataSource) { perimeter = _.find(streamLayers, {code: 'drb_streams_v2'}).perimeter; point_outside_message = 'Selected point is outside the Delaware River Basin'; break; + case utils.NHDHR: case utils.NHD: // Bounds checking disabled until #1656 is complete. d.resolve(latlng); diff --git a/src/mmw/js/src/modeling/controls.js b/src/mmw/js/src/modeling/controls.js index d0e21fc94..1990a344b 100644 --- a/src/mmw/js/src/modeling/controls.js +++ b/src/mmw/js/src/modeling/controls.js @@ -9,6 +9,7 @@ var $ = require('jquery'), settings = require('../core/settings'), coreUnits = require('../core/units'), models = require('./models'), + utils = require('./utils'), modificationConfigUtils = require('./modificationConfigUtils'), gwlfeConfig = require('./gwlfeModificationConfig'), entryViews = require('./gwlfe/entry/views'), @@ -506,7 +507,8 @@ var GwlfeLandCoverView = ControlView.extend({ this.model.get('dataModel'), currentScenario, App.currentProject.get('in_drb'), - App.getAnalyzeCollection() + App.getAnalyzeCollection(), + utils.layerOverrideToDefaultLandCoverType(App.currentProject.get('layer_overrides')) ); }, }); diff --git a/src/mmw/js/src/modeling/gwlfe/entry/templates/landCoverModal.html b/src/mmw/js/src/modeling/gwlfe/entry/templates/landCoverModal.html index 7b627bc81..ffa66c71c 100644 --- a/src/mmw/js/src/modeling/gwlfe/entry/templates/landCoverModal.html +++ b/src/mmw/js/src/modeling/gwlfe/entry/templates/landCoverModal.html @@ -24,11 +24,35 @@

{{ title }}

diff --git a/src/mmw/js/src/modeling/gwlfe/entry/views.js b/src/mmw/js/src/modeling/gwlfe/entry/views.js index 858b8e64c..1f8d00c6b 100644 --- a/src/mmw/js/src/modeling/gwlfe/entry/views.js +++ b/src/mmw/js/src/modeling/gwlfe/entry/views.js @@ -55,10 +55,22 @@ var LandCoverModal = modalViews.ModalBaseView.extend({ templateHelpers: function() { var presetMod = this.scenario.get('modifications').findWhere({ modKey: 'entry_landcover_preset' }), - preset = presetMod && presetMod.get('userInput').entry_landcover_preset; + preset = presetMod && presetMod.get('userInput').entry_landcover_preset, + landTasks = this.analyzeCollection.findWhere({ name: 'land' }).get('tasks'), + fields = ['name', 'displayName', 'status'], + defaultLandCoverType = this.model.get('defaultLandCoverType'), + defaultLandCover = landTasks.findWhere({ name: defaultLandCoverType }).pick(fields), + pickFields = function(x) { return x.pick(fields); }, + landCoverFilter = function(x) { return x.get('name').startsWith('land_'); }, + drbFilter = function(x) { return x.get('name').startsWith('drb_'); }, + landCovers = landTasks.filter(landCoverFilter).map(pickFields), + drbCovers = landTasks.filter(drbFilter).map(pickFields); return { preset: preset, + defaultLandCover: defaultLandCover, + landCovers: landCovers, + drbCovers: drbCovers, }; }, @@ -1089,7 +1101,7 @@ function showSettingsModal(title, dataModel, modifications, addModification) { }).render(); } -function showLandCoverModal(dataModel, scenario, in_drb, analyzeCollection) { +function showLandCoverModal(dataModel, scenario, in_drb, analyzeCollection, defaultLandCoverType) { var scheme = settings.get('unit_scheme'), areaLUnits = coreUnits[scheme].AREA_L_FROM_HA.name, landCovers = _(GWLFE_LAND_COVERS).sortBy('id').map(function(lc) { @@ -1109,6 +1121,7 @@ function showLandCoverModal(dataModel, scenario, in_drb, analyzeCollection) { title: 'Land Cover', fields: fields, in_drb: in_drb, + defaultLandCoverType: defaultLandCoverType, }); new LandCoverModal({ diff --git a/src/mmw/js/src/modeling/gwlfe/quality/templates/result.html b/src/mmw/js/src/modeling/gwlfe/quality/templates/result.html index bd26bb196..2fbafa291 100644 --- a/src/mmw/js/src/modeling/gwlfe/quality/templates/result.html +++ b/src/mmw/js/src/modeling/gwlfe/quality/templates/result.html @@ -24,7 +24,7 @@

{% if showSubbasinModelingButton %} - View subbasin attentuated results + View subbasin attenuated results {% endif %}
diff --git a/src/mmw/js/src/modeling/models.js b/src/mmw/js/src/modeling/models.js index dc2e87b36..e5a1ff193 100644 --- a/src/mmw/js/src/modeling/models.js +++ b/src/mmw/js/src/modeling/models.js @@ -366,6 +366,7 @@ var ProjectModel = Backbone.Model.extend({ sidebar_mode: utils.MODEL, // The current mode of the sidebar. ANALYZE, MONITOR, or MODEL. is_exporting: false, // Is the project currently exporting? hydroshare_errors: [], // List of errors from connecting to hydroshare + layer_overrides: {}, // Keys of tokens mapped to overriding layer names, e.g. {"__LAND__": "nlcd-2011-30m-epsg5070-512-int8"} }, initialize: function() { @@ -568,9 +569,13 @@ var ProjectModel = Backbone.Model.extend({ var aoi = this.get('area_of_interest'), wkaoi = this.get('wkaoi'), promise = $.Deferred(), - mapshedInput = utils.isWKAoIValid(wkaoi) ? - JSON.stringify({ 'wkaoi': wkaoi }) : - JSON.stringify({ 'area_of_interest': aoi }), + aoi_param = utils.isWKAoIValid(wkaoi) ? + { 'wkaoi': wkaoi } : + { 'area_of_interest': aoi }, + mapshedInput = JSON.stringify( + _.extend({ + layer_overrides: this.get('layer_overrides'), + }, aoi_param)), queryParams = isSubbasinMode ? { subbasin: true } : null, taskModel = isSubbasinMode ? createSubbasinTaskModel(utils.MAPSHED) : @@ -1678,7 +1683,8 @@ var ScenarioModel = Backbone.Model.extend({ aoi_census: self.get('aoi_census'), modification_censuses: self.get('modification_censuses'), inputmod_hash: self.get('inputmod_hash'), - modification_hash: self.get('modification_hash') + modification_hash: self.get('modification_hash'), + layer_overrides: project.get('layer_overrides'), }; if (utils.isWKAoIValid(wkaoi)) { @@ -1700,6 +1706,7 @@ var ScenarioModel = Backbone.Model.extend({ mapshed_job_uuid: isSubbasinMode ? project.get('subbasin_mapshed_job_uuid') : project.get('mapshed_job_uuid'), + layer_overrides: project.get('layer_overrides'), }; } } diff --git a/src/mmw/js/src/modeling/tests.js b/src/mmw/js/src/modeling/tests.js index 486801f14..3b2a35632 100644 --- a/src/mmw/js/src/modeling/tests.js +++ b/src/mmw/js/src/modeling/tests.js @@ -866,7 +866,8 @@ describe('Modeling', function() { self.scenarioModel.fetchResults().pollingPromise.always(function() { assert(self.setNullResultsSpy.calledOnce, 'setNullResults should have been called once'); assert.isFalse(self.setResultsSpy.called, 'setResults should not have been called'); - assert(saveSpy.calledTwice, 'attemptSave should have been called twice'); + // TODO: Re-enable tests https://github.com/WikiWatershed/model-my-watershed/issues/3442 + // assert(saveSpy.calledTwice, 'attemptSave should have been called twice'); fetchResultsAssertions(self); done(); }); @@ -877,7 +878,8 @@ describe('Modeling', function() { self.scenarioModel.fetchResults().pollingPromise.always(function() { assert(self.setResultsSpy.calledOnce, 'setResults should have been called'); assert.isFalse(self.setNullResultsSpy.called, 'setNullResults should not have been called'); - assert(saveSpy.calledTwice, 'attemptSave should have been called twice'); + // TODO: Re-enable tests https://github.com/WikiWatershed/model-my-watershed/issues/3442 + // assert(saveSpy.calledTwice, 'attemptSave should have been called twice'); fetchResultsAssertions(self); done(); }); diff --git a/src/mmw/js/src/modeling/utils.js b/src/mmw/js/src/modeling/utils.js index 9856de08d..bee17dd24 100644 --- a/src/mmw/js/src/modeling/utils.js +++ b/src/mmw/js/src/modeling/utils.js @@ -54,4 +54,32 @@ module.exports = { 0, // High Density Residential ]; }, + + /** + * Takes a layer_overrides object and converts it to a Land Cover Type + * identifier. Defaults to "land_2019_2019" + * + * e.g. {"__LAND__": "nlcd-2011-30m-epsg5070-512-int8"} -> "land_2011_2011" + */ + layerOverrideToDefaultLandCoverType: function(layer_overrides) { + var raster = layer_overrides && _.get(layer_overrides, "__LAND__"), + mapping = { + "nlcd-2019-30m-epsg5070-512-byte": "land_2019_2019", + "nlcd-2016-30m-epsg5070-512-byte": "land_2019_2016", + "nlcd-2011-30m-epsg5070-512-byte": "land_2019_2011", + "nlcd-2006-30m-epsg5070-512-byte": "land_2019_2006", + "nlcd-2001-30m-epsg5070-512-byte": "land_2019_2001", + "nlcd-2011-30m-epsg5070-512-int8": "land_2011_2011", + }; + + if (!raster) { + return "land_2019_2019"; + } + + if (_.has(mapping, raster)) { + return mapping[raster]; + } + + throw new Error('Invalid layer override: ' + raster); + }, }; diff --git a/src/mmw/js/src/modeling/views.js b/src/mmw/js/src/modeling/views.js index 4fffe7c9b..4780f7721 100644 --- a/src/mmw/js/src/modeling/views.js +++ b/src/mmw/js/src/modeling/views.js @@ -1472,17 +1472,14 @@ function getExpectedWaitTime(modelPackage, resultName, isGatheringData) { case coreUtils.TR55_PACKAGE: return null; case coreUtils.GWLFE: - if (resultName !== 'subbasin') { - if (isGatheringData) { - return 'This may take up to 30 seconds'; - } - + if (resultName !== 'subbasin' && !isGatheringData) { + // Running GWLFE for one shape return 'This may take a few seconds'; } - if (isGatheringData) { - return 'This may take up to a minute'; - } - return 'This may take up to 3 minutes'; + + // Running Mapshed for one or more shapes, + // running GWLFE for multiple shapes + return 'This may take a few minutes'; default: console.log('Model package ' + modelPackage + ' not supported.'); } diff --git a/src/mmw/js/src/user/templates/baseModal.html b/src/mmw/js/src/user/templates/baseModal.html index aaccededf..b3c6804ad 100644 --- a/src/mmw/js/src/user/templates/baseModal.html +++ b/src/mmw/js/src/user/templates/baseModal.html @@ -8,16 +8,16 @@ {% macro select(value, name, model, field, defaultChoice='', label='') %} + {% endmacro %}