diff --git a/.gitignore b/.gitignore index a570488c1..43e239725 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ docs/_build tethys_gizmos/static/tethys_gizmos/less/bower_components/* .coverage tests/coverage_html_report +.*.swp +.DS_Store diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..a73b42ac1 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,29 @@ +image: docker:git + +stages: + - build + +before_script: + # Set up ssh-agent for use when checking out other repos + - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" > key + - chmod 600 key + - ssh-add key + - ssh-add -l + # Add known host keys + - mkdir -p ~/.ssh + - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts' + +build: + stage: build + script: + # Docker Stuff + - docker build --squash -t $CI_REGISTRY_IMAGE/tethyscore:$CI_COMMIT_TAG -t $CI_REGISTRY_IMAGE/tethyscore:latest . + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker push $CI_REGISTRY_IMAGE/tethyscore:$CI_COMMIT_TAG + - docker push $CI_REGISTRY_IMAGE/tethyscore:latest + tags: + - docker + only: + - tags diff --git a/.stickler.yml b/.stickler.yml new file mode 100644 index 000000000..abb0584d3 --- /dev/null +++ b/.stickler.yml @@ -0,0 +1,5 @@ +linters: + flake8: + max-line-length: 120 + fixer: true + exclude: .git,build,dist,__pycache__,.eggs,*.egg-info \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..aeb94c97f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,57 @@ +# +language: c + +env: + - PYTHON_VERSION="2" + - PYTHON_VERSION="3" + +# Setting sudo to false opts in to Travis-CI container-based builds. +sudo: false + +# Turn off email notifications +notifications: + email: false + +os: + - linux + - osx + +install: + - cd .. + - mv tethys src + - bash ./src/scripts/install_tethys.sh -h + - bash ./src/scripts/install_tethys.sh --partial-tethys-install mesdiat -t $PWD --python-version $PYTHON_VERSION + + # activate conda environment + - export PATH="$PWD/miniconda/bin:$PATH" + - source activate tethys + - conda list + + # start database server + - pg_ctl -U postgres -D "${TETHYS_DB_DIR}/data" -l "${TETHYS_DB_DIR}/logfile" start -o "-p ${TETHYS_DB_PORT}" + + # generate new settings.py file with tethys_super user for tests + - rm ./src/tethys_portal/settings.py + - tethys gen settings --db-username tethys_super --db-password pass --db-port ${TETHYS_DB_PORT} + + # install test dependencies + - pip install python-coveralls + - pip install -e $TETHYS_HOME/src/[tests] + + # install test apps and extensions + - pushd ./src/tests/extensions/tethysext-test_extension + - python setup.py develop + - popd + + - pushd ./src/tests/apps/tethysapp-test_app + - python setup.py develop + - popd + +# command to run tests +script: + - tethys test -c -u + +# generate test coverage information +after_success: + - ls -al + - coveralls diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..a5e56c3c0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,124 @@ +# Use an official Python runtime as a parent image +FROM python:2-slim-stretch + +############### +# ENVIRONMENT # +############### +ENV TETHYS_HOME="/usr/lib/tethys" \ + TETHYS_PORT=80 \ + TETHYS_PUBLIC_HOST="127.0.0.1" \ + TETHYS_DB_USERNAME="tethys_default" \ + TETHYS_DB_PASSWORD="pass" \ + TETHYS_DB_HOST="172.17.0.1" \ + TETHYS_DB_PORT=5432 \ + TETHYS_SUPER_USER="" \ + TETHYS_SUPER_USER_EMAIL="" \ + TETHYS_SUPER_USER_PASS="" + +# Misc +ENV ALLOWED_HOSTS="['127.0.0.1', 'localhost']" \ + BASH_PROFILE=".bashrc" \ + CONDA_HOME="${TETHYS_HOME}/miniconda" \ + CONDA_ENV_NAME=tethys \ + MINICONDA_URL="https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh" \ + PYTHON_VERSION=2 \ + UWSGI_PROCESSES=10 \ + CLIENT_MAX_BODY_SIZE="75M" + +######### +# SETUP # +######### +RUN mkdir -p "${TETHYS_HOME}/src" +WORKDIR ${TETHYS_HOME} + +# Speed up APT installs +RUN echo "force-unsafe-io" > /etc/dpkg/dpkg.cfg.d/02apt-speedup \ + ; echo "Acquire::http {No-Cache=True;};" > /etc/apt/apt.conf.d/no-cache + +# Install APT packages +RUN apt-get update && apt-get -y install wget gnupg2 \ + && wget -O - https://repo.saltstack.com/apt/debian/9/amd64/latest/SALTSTACK-GPG-KEY.pub | apt-key add - \ + && echo "deb http://repo.saltstack.com/apt/debian/9/amd64/latest stretch main" > /etc/apt/sources.list.d/saltstack.list +RUN apt-get update && apt-get -y install bzip2 git nginx gcc salt-minion procps pv +RUN rm -f /etc/nginx/sites-enabled/default + +# Install Miniconda +RUN wget ${MINICONDA_URL} -O "${TETHYS_HOME}/miniconda.sh" \ + && bash ${TETHYS_HOME}/miniconda.sh -b -p "${CONDA_HOME}" + +# Setup Conda Environment +ADD environment_py2.yml ${TETHYS_HOME}/src/ +WORKDIR ${TETHYS_HOME}/src +RUN ${CONDA_HOME}/bin/conda env create -n "${CONDA_ENV_NAME}" -f "environment_py${PYTHON_VERSION}.yml" + +########### +# INSTALL # +########### +# ADD files from repo +ADD resources ${TETHYS_HOME}/src/resources/ +ADD templates ${TETHYS_HOME}/src/templates/ +ADD tethys_apps ${TETHYS_HOME}/src/tethys_apps/ +ADD tethys_compute ${TETHYS_HOME}/src/tethys_compute/ +ADD tethys_config ${TETHYS_HOME}/src/tethys_config/ +ADD tethys_gizmos ${TETHYS_HOME}/src/tethys_gizmos/ +ADD tethys_portal ${TETHYS_HOME}/src/tethys_portal/ +ADD tethys_sdk ${TETHYS_HOME}/src/tethys_sdk/ +ADD tethys_services ${TETHYS_HOME}/src/tethys_services/ +ADD README.rst ${TETHYS_HOME}/src/ +ADD *.py ${TETHYS_HOME}/src/ + +# Remove any apps that may have been installed in tethysapp +RUN rm -rf ${TETHYS_HOME}/src/tethys_apps/tethysapp \ + ; mkdir -p ${TETHYS_HOME}/src/tethys_apps/tethysapp +ADD tethys_apps/tethysapp/__init__.py ${TETHYS_HOME}/src/tethys_apps/tethysapp/ + +# Run Installer +RUN /bin/bash -c '. ${CONDA_HOME}/bin/activate ${CONDA_ENV_NAME} \ + ; python setup.py develop \ + ; conda install -c conda-forge uwsgi -y' +RUN mkdir ${TETHYS_HOME}/workspaces ${TETHYS_HOME}/apps ${TETHYS_HOME}/static + +# Add static files +ADD static ${TETHYS_HOME}/src/static/ + +# Generate Inital Settings Files +RUN /bin/bash -c '. ${CONDA_HOME}/bin/activate ${CONDA_ENV_NAME} \ + ; tethys gen settings --production --allowed-host "${ALLOWED_HOST}" --db-username ${TETHYS_DB_USERNAME} --db-password ${TETHYS_DB_PASSWORD} --db-port ${TETHYS_DB_PORT} --overwrite \ + ; sed -i -e "s:#TETHYS_WORKSPACES_ROOT = .*$:TETHYS_WORKSPACES_ROOT = \"/usr/lib/tethys/workspaces\":" ${TETHYS_HOME}/src/tethys_portal/settings.py \ + ; tethys gen nginx --overwrite \ + ; tethys gen uwsgi_settings --overwrite \ + ; tethys gen uwsgi_service --overwrite \ + ; tethys manage collectstatic' + +# Give NGINX Permission +RUN export NGINX_USER=$(grep 'user .*;' /etc/nginx/nginx.conf | awk '{print $2}' | awk -F';' '{print $1}') \ + ; find ${TETHYS_HOME} ! -user ${NGINX_USER} -print0 | pv | xargs -0 -I{} chown ${NGINX_USER}: {} + +############ +# CLEAN UP # +############ +RUN apt-get -y remove wget gcc gnupg2 \ + ; apt-get -y autoremove \ + ; apt-get -y clean + +######################### +# CONFIGURE ENVIRONMENT# +######################### +ENV PATH ${CONDA_HOME}/miniconda/envs/tethys/bin:$PATH +VOLUME ["${TETHYS_HOME}/workspaces", "${TETHYS_HOME}/keys"] +EXPOSE 80 + +###############* +# COPY IN SALT # +###############* +ADD docker/salt/ /srv/salt/ +ADD docker/run.sh ${TETHYS_HOME}/ + +######## +# RUN! # +######## +WORKDIR ${TETHYS_HOME} +# Create Salt configuration based on ENVs +CMD bash run.sh +HEALTHCHECK --start-period=240s \ + CMD ps $(cat $(grep 'pid .*;' /etc/nginx/nginx.conf | awk '{print $2}' | awk -F';' '{print $1}')) > /dev/null && ps $(cat $(grep 'pidfile2: .*' src/tethys_portal/tethys_uwsgi.yml | awk '{print $2}')) > /dev/null; diff --git a/README.rst b/README.rst index 39c5ee292..6d747f845 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,15 @@ Tethys Platform =============== +.. image:: https://travis-ci.org/tethysplatform/tethys.svg?branch=master + :target: https://travis-ci.org/tethysplatform/tethys + +.. image:: https://coveralls.io/repos/github/tethysplatform/tethys/badge.svg + :target: https://coveralls.io/github/tethysplatform/tethys + + +.. image:: https://readthedocs.org/projects/tethys-platform/badge/?version=stable + :target: http://docs.tethysplatform.org/en/stable/?badge=stable + :alt: Documentation Status Tethys Platform provides both a development environment and a hosting environment for water resources web apps. diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..bee919d9b --- /dev/null +++ b/docker/README.md @@ -0,0 +1,87 @@ +# Docker Support Files + +This project houses the docker file and scripts needed to make usable docker image. + +### Environment + +| Argument | Description |Phase| Default | +|-----------------------|-----------------------------|-----|-------------------------| +|ALLOWED_HOSTS | Django Setting |Run |['127.0.0.1', 'localhost']| +|BASH_PROFILE | Where to create aliases |Run |.bashrc | +|CONDA_HOME | Path to Conda Home Dir |Build|${TETHYS_HOME}/miniconda”| +|CONDA_ENV_NAME | Name of Conda environ |Build|tethys | +|MINICONDA_URL | URL of conda install script |Build|“https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh”| +|PYTHON_VERSION | Version of Python to use |Build|2 | +|TETHYS_HOME | Path to Tethys Home Dir |Build|/usr/lib/tethys | +|TETHYS_PORT | Port for external web access|Run |80 | +|TETHYS_PUBLIC_HOST | Public host and port |Run |127.0.0.1 | +|TETHYS_DB_USERNAME | Postgres connection username|Run |tethys_default | +|TETHYS_DB_PASSWORD | Postgres connection password|Run |pass | +|TETHYS_DB_HOST | Postgres connection address |Run |172.17.0.1 | +|TETHYS_DB_PORT | Postgres connection Port |Run |5432 | +|TETHYS_SUPER_USER | Default superuser username |Run |"" | +|TETHYS_SUPER_USER_EMAIL| Default superuser email |Run |“” | +|TETHYS_SUPER_USER_PASS | Default superuser password |Run |"" | +|UWSGI_PROCESSES | Number of uwsgi processes |Run |10 | +|CLIENT_MAX_BODY_SIZE | Maximum size of file uploads|Run |75M | + +### Building the Docker +To build the docker use the following commands in the terminal after +pulling the latest source code: + +1. Make sure that there isn't already a docker container or docker +images with the desired name +``` +> docker rm tethyscore +> docker rmi tethyscore +``` + +2. Build a new docker with the desired name and tag +``` +> docker build -t tethyscore:latest +``` +You can also use build arguments in this to change certain features +that you may find useful, such as the branch, and the configuration + +Use the following syntax with arguments listed in the table + +``` +> docker build [--build-arg ARG=VAL] -t tethyscore:latest +``` + +| Argument | Description | Default | +|--------------------------|---------------------------|------------------------| +|TETHYSBUILD_BRANCH | Tethys branch to be used | release | +|TETHYSBUILD_PY_VERSION | Version of python | 2 | +|TETHYSBUILD_TETHYS_HOME | Path to Tethys home dir | /usr/lib/tethys | +|TETHYSBUILD_CONDA_HOME | Path to Conda home dir | /usr/lib/tethys/conda/ | +|TETHYSBUILD_CONDA_ENV_NAME| Tethys environment name | tethys | + +### Running the docker +To run the docker you can use the following flags + +use the following flag with the arguments listed in the table. (NOTE: +args in the build arg table can be used here as well) + +``` +-e TETHYSBUILD_CONDA_ENV='tethys' +``` + +| Argument | Description | Default | +|--------------------------|----------------------------|---------------| +|TETHYSBUILD_ALLOWED_HOST | Django Allowed Hosts | 127.0.0.1 | +|TETHYSBUILD_DB_USERNAME | Database Username | tethys_default| +|TETHYSBUILD_DB_PASSWORD | Password for Database | pass | +|TETHYSBUILD_DB_HOST | IPAddress for Database host| 127.0.0.1 | +|TETHYSBUILD_DB_PORT | Port on Database host | 5432 | +|TETHYSBUILD_SUPERUSER | Tethys Superuser | tethys_super | +|TETHYSBUILD_SUPERUSER_PASS| Tethys Superuser Password | admin | + +Example of Command: +``` +> docker run -p 127.0.0.1:8000:8000 --name tethyscore \ + -e TETHYSBUILD_CONDA_ENV='tethys' -e TETHYSBUILD_CONFIG='develop' \ + -e TETHYSBUILD_DB_USERNAME='tethys_super' -e TETHYSBUILD_DB_PASSWORD='3x@m9139@$$' \ + -e TETHYSBUILD_DB_PORT='5432' TETHYSBUILD_SUPERUSER='admin' \ + -e TETHYSBUILD_SUPERUSER_PASS='admin' tethyscore +``` diff --git a/docker/run.sh b/docker/run.sh new file mode 100644 index 000000000..5f491f879 --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +echo_status() { + local args="${@}" + tput setaf 4 + tput bold + echo -e "- $args" + tput sgr0 +} + +echo_status "Starting up..." + +# Set extra ENVs +export NGINX_USER=$(grep 'user .*;' /etc/nginx/nginx.conf | awk '{print $2}' | awk -F';' '{print $1}') +export NGINX_PIDFILE=$(grep 'pid .*;' /etc/nginx/nginx.conf | awk '{print $2}' | awk -F';' '{print $1}') +export UWSGI_PIDFILE=$(grep 'pidfile2: .*' src/tethys_portal/tethys_uwsgi.yml | awk '{print $2}') + +# Create Salt Config +echo "file_client: local" > /etc/salt/minion +echo "postgres.host: '${TETHYS_DB_HOST}'" >> /etc/salt/minion +echo "postgres.port: '${TETHYS_DB_PORT}'" >> /etc/salt/minion +echo "postgres.user: '${TETHYS_DB_USERNAME}'" >> /etc/salt/minion +echo "postgres.pass: '${TETHYS_DB_PASSWORD}'" >> /etc/salt/minion +echo "postgres.bins_dir: '${CONDA_HOME}/envs/${CONDA_ENV_NAME}/bin'" >> /etc/salt/minion + +# Apply States +echo_status "Enforcing start state... (This might take a bit)" +salt-call --local state.apply +echo_status "Fixing permissions" +find ${TETHYS_HOME} ! -user ${NGINX_USER} -print0 | xargs -0 -I{} chown ${NGINX_USER}: {} +echo_status "Done!" + +# Watch Logs +echo_status "Watching logs" +tail -qF /var/log/nginx/* /var/log/uwsgi/* diff --git a/docker/salt/run.sls b/docker/salt/run.sls new file mode 100644 index 000000000..53682587f --- /dev/null +++ b/docker/salt/run.sls @@ -0,0 +1,14 @@ +{% set TETHYS_HOME = salt['environ.get']('TETHYS_HOME') %} +{% set NGINX_USER = salt['environ.get']('NGINX_USER') %} + +uwsgi: + cmd.run: + - name: {{ TETHYS_HOME }}/miniconda/envs/tethys/bin/uwsgi --yaml {{ TETHYS_HOME}}/src/tethys_portal/tethys_uwsgi.yml --uid {{ NGINX_USER }} --gid {{ NGINX_USER }} --enable-threads + - bg: True + - ignore_timeout: True + +nginx: + cmd.run: + - name: nginx -g 'daemon off;' + - bg: True + - ignore_timeout: True \ No newline at end of file diff --git a/docker/salt/tethyscore.sls b/docker/salt/tethyscore.sls new file mode 100644 index 000000000..af08809df --- /dev/null +++ b/docker/salt/tethyscore.sls @@ -0,0 +1,116 @@ +{% set ALLOWED_HOSTS = salt['environ.get']('ALLOWED_HOSTS') %} +{% set CONDA_ENV_NAME = salt['environ.get']('CONDA_ENV_NAME') %} +{% set CONDA_HOME = salt['environ.get']('CONDA_HOME') %} +{% set NGINX_USER = salt['environ.get']('NGINX_USER') %} +{% set CLIENT_MAX_BODY_SIZE = salt['environ.get']('CLIENT_MAX_BODY_SIZE') %} +{% set UWSGI_PROCESSES = salt['environ.get']('UWSGI_PROCESSES') %} +{% set TETHYS_BIN_DIR = [CONDA_HOME, "/envs/", CONDA_ENV_NAME, "/bin"]|join %} +{% set TETHYS_DB_HOST = salt['environ.get']('TETHYS_DB_HOST') %} +{% set TETHYS_DB_PASSWORD = salt['environ.get']('TETHYS_DB_PASSWORD') %} +{% set TETHYS_DB_PORT = salt['environ.get']('TETHYS_DB_PORT') %} +{% set TETHYS_DB_USERNAME = salt['environ.get']('TETHYS_DB_USERNAME') %} +{% set TETHYS_HOME = salt['environ.get']('TETHYS_HOME') %} +{% set TETHYS_PUBLIC_HOST = salt['environ.get']('TETHYS_PUBLIC_HOST') %} +{% set TETHYS_SUPER_USER = salt['environ.get']('TETHYS_SUPER_USER') %} +{% set TETHYS_SUPER_USER_EMAIL = salt['environ.get']('TETHYS_SUPER_USER_EMAIL') %} +{% set TETHYS_SUPER_USER_PASS = salt['environ.get']('TETHYS_SUPER_USER_PASS') %} + +~/.bashrc: + file.append: + - text: "alias t='. {{ CONDA_HOME }}/bin/activate {{ CONDA_ENV_NAME }}'" + +Generate_Tethys_Settings_TethysCore: + cmd.run: + - name: {{ TETHYS_BIN_DIR }}/tethys gen settings --production --allowed-hosts={{ ALLOWED_HOSTS }} --db-username {{ TETHYS_DB_USERNAME }} --db-password {{ TETHYS_DB_PASSWORD }} --db-port {{ TETHYS_DB_PORT }} --overwrite + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + +Edit_Tethys_Settings_File_(HOST)_TethysCore: + file.replace: + - name: {{ TETHYS_HOME }}/src/tethys_portal/settings.py + - pattern: "'HOST': '127.0.0.1'" + - repl: "'HOST': '{{ TETHYS_DB_HOST }}'" + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + +Edit_Tethys_Settings_File_(HOME_PAGE)_TethysCore: + file.replace: + - name: {{ TETHYS_HOME }}/src/tethys_portal/settings.py + - pattern: "BYPASS_TETHYS_HOME_PAGE = False" + - repl: "BYPASS_TETHYS_HOME_PAGE = True" + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + +Edit_Tethys_Settings_File_(SESSION_WARN)_TethysCore: + file.replace: + - name: {{ TETHYS_HOME }}/src/tethys_portal/settings.py + - pattern: "SESSION_SECURITY_WARN_AFTER = 840" + - repl: "SESSION_SECURITY_WARN_AFTER = 25 * 60" + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + +Edit_Tethys_Settings_File_(SESSION_EXPIRE)_TethysCore: + file.replace: + - name: {{ TETHYS_HOME }}/src/tethys_portal/settings.py + - pattern: "SESSION_SECURITY_EXPIRE_AFTER = 900" + - repl: "SESSION_SECURITY_EXPIRE_AFTER = 30 * 60" + +Edit_Tethys_Settings_File_(PUBLIC_HOST)_TethysCore: + file.append: + - name: {{ TETHYS_HOME }}/src/tethys_portal/settings.py + - text: "PUBLIC_HOST = \"{{ TETHYS_PUBLIC_HOST }}\"" + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + +Generate_NGINX_Settings_TethysCore: + cmd.run: + - name: {{ TETHYS_BIN_DIR }}/tethys gen nginx --client-max-body-size="{{ CLIENT_MAX_BODY_SIZE }}" --overwrite + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + +Generate_UWSGI_Settings_TethysCore: + cmd.run: + - name: {{ TETHYS_BIN_DIR }}/tethys gen uwsgi_settings --uwsgi-processes={{ UWSGI_PROCESSES }} --overwrite + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + +Generate_UWSGI_Service_TethysCore: + cmd.run: + - name: {{ TETHYS_BIN_DIR }}/tethys gen uwsgi_service --overwrite + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + +/run/uwsgi/tethys.pid: + file.managed: + - user: {{ NGINX_USER }} + - replace: False + - makedirs: True + +/var/log/uwsgi/tethys.log: + file.managed: + - user: {{ NGINX_USER }} + - replace: False + - makedirs: True + +Prepare_Database_TethysCore: + postgres_user.present: + - name: {{ TETHYS_DB_USERNAME }} + - password: {{ TETHYS_DB_PASSWORD }} + - login: True + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + postgres_database.present: + - name: {{ TETHYS_DB_USERNAME }} + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + cmd.run: + - name: . {{ CONDA_HOME }}/bin/activate {{ CONDA_ENV_NAME }} && {{ TETHYS_BIN_DIR }}/tethys manage syncdb + - shell: /bin/bash + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + +Create_Super_User_TethysCore: + cmd.run: + - name: "{{TETHYS_BIN_DIR }}/python {{ TETHYS_HOME }}/src/manage.py shell -c \"from django.contrib.auth.models import User;\nif '{{ TETHYS_SUPER_USER }}' and len(User.objects.filter(username='{{ TETHYS_SUPER_USER }}')) == 0:\n\tUser.objects.create_superuser('{{ TETHYS_SUPER_USER }}', '{{ TETHYS_SUPER_USER_EMAIL }}', '{{ TETHYS_SUPER_USER_PASS }}')\"" + - cwd: {{ TETHYS_HOME }}/src + - shell: /bin/bash + +Link_NGINX_Config_TethysCore: + file.symlink: + - name: /etc/nginx/sites-enabled/tethys_nginx.conf + - target: {{ TETHYS_HOME }}/src/tethys_portal/tethys_nginx.conf + - unless: /bin/bash -c "[ -f "/usr/lib/tethys/setup_complete" ];" + +Flag_Complete_Setup_TethysCore: + cmd.run: + - name: touch /usr/lib/tethys/setup_complete + - shell: /bin/bash diff --git a/docker/salt/top.sls b/docker/salt/top.sls new file mode 100644 index 000000000..59329b5ce --- /dev/null +++ b/docker/salt/top.sls @@ -0,0 +1,4 @@ +base: + '*': + - tethyscore + - run diff --git a/docs/conf.py b/docs/conf.py index 13726587d..c866fb30b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,35 +14,75 @@ import sys import os -import re +import mock import pbr.version import pbr.git +from django.conf import settings +import django + +# Mock Dependencies +# NOTE: No obvious way to automatically anticipate all the sub modules without +# installing the package, which is what we are trying to avoid. +MOCK_MODULES = [ + 'bokeh', 'bokeh.embed', 'bokeh.resources', + 'condorpy', + 'django_gravatar', + 'future', 'future.standard_library', 'future.utils', + 'guardian', 'guardian.admin', + 'model_utils', 'model_utils.managers', + 'past', 'past.builtins', 'past.types', 'past.utils', + 'plotly', 'plotly.offline', + 'social_core', 'social_core.exceptions', + 'social_django', + 'sqlalchemy', 'sqlalchemy.orm', + 'tethys_apps.harvester', # Mocked to prevent issues with loading apps during docs build. + 'tethys_compute.utilities' # Mocked to prevent issues with DictionaryField and List Field during docs build. +] + + +# Mock dependency modules so we don't have to install them +# See: https://docs.readthedocs.io/en/latest/faq.html#i-get-import-errors-on-libraries-that-depend-on-c-modules +class MockModule(mock.MagicMock): + @classmethod + def __getattr__(cls, name): + return mock.MagicMock() + + +print('NOTE: The following modules are mocked to prevent timeouts during the docs build process on RTD:') +print('{}'.format(', '.join(MOCK_MODULES))) +sys.modules.update((mod_name, MockModule()) for mod_name in MOCK_MODULES) # Fixes django settings module problem sys.path.insert(0, os.path.abspath('..')) -from django.conf import settings -# parse the installed apps list from the settings template -with open('../tethys_apps/cli/gen_templates/settings', 'r') as settings_file: - settings_str = settings_file.read() - match = re.search('INSTALLED_APPS = \(\n(.*?)\)', settings_str, re.DOTALL) - installed_apps = [app.strip('\'|,') for app in match.group(1).split()] +installed_apps = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'tethys_config', + 'tethys_apps', + 'tethys_gizmos', + 'tethys_services', + 'tethys_compute', +] -settings.configure(INSTALLED_APPS = installed_apps) -import django +settings.configure(INSTALLED_APPS=installed_apps) django.setup() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -56,7 +96,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -81,7 +121,8 @@ on_rtd = os.environ.get('READTHEDOCS') == 'True' if on_rtd: # Hack to try to get the branch name if possible, otherwise assume 'release' - branch = pbr.git._run_git_command(['branch'], pbr.git._get_git_directory()).split('*')[-1].split('\n')[0].strip(')').split('/') + branch = pbr.git._run_git_command(['branch'], pbr.git._get_git_directory()).split('*')[-1].split('\n')[0].\ + strip(')').split('/') branch = branch[-1] if len(branch) == 2 else 'release' print(branch) @@ -91,13 +132,13 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -105,27 +146,27 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output ---------------------------------------------- @@ -137,21 +178,21 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 @@ -166,49 +207,48 @@ # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -html_use_smartypants = False -smart_quotes = False +smartquotes = False # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'TethysPlatformdoc' @@ -217,14 +257,14 @@ # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples @@ -237,28 +277,29 @@ # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # markup to shorten external links (see: http://www.sphinx-doc.org/en/stable/ext/extlinks.html) -install_tethys_link = 'https://raw.githubusercontent.com/tethysplatform/tethys/{}/scripts/install_tethys.%s'.format(branch) +install_tethys_link = 'https://raw.githubusercontent.com/tethysplatform/tethys/{}/scripts/install_tethys.%s'.\ + format(branch) extlinks = {'install_tethys': (install_tethys_link, None), } # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -271,7 +312,7 @@ ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -286,16 +327,16 @@ ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' @@ -311,5 +352,5 @@ # If this is True, todo and todolist produce output, else they produce nothing. The default is False. todo_include_todos = True -#If this is True, todo emits a warning for each TODO entries. The default is False. -todo_emit_warnings = True \ No newline at end of file +# If this is True, todo emits a warning for each TODO entries. The default is False. +todo_emit_warnings = True diff --git a/docs/docs_environment.yml b/docs/docs_environment.yml new file mode 100644 index 000000000..4935bc3b1 --- /dev/null +++ b/docs/docs_environment.yml @@ -0,0 +1,22 @@ +# docs_environment.yml +# Configuration file for creating a Conda Environment with dependencies needed for Tethys Platform. +# Create the environment by running the following command (after installing Miniconda): +# $ conda env create -f docs_environment.yml + +name: tethys-docs + +channels: +- tethysplatform +- conda-forge +- defaults + +dependencies: +- python=3.5 +- tethys_dataset_services>=1.7.0 +- django=1.11.* +- pip: + - sphinx + - mock + - sphinx_rtd_theme + - sphinxcontrib-napoleon + - pbr diff --git a/docs/images/site_admin/app_settings_top.png b/docs/images/site_admin/app_settings_top.png new file mode 100644 index 000000000..bd9b860cc Binary files /dev/null and b/docs/images/site_admin/app_settings_top.png differ diff --git a/docs/images/site_admin/auth_token.png b/docs/images/site_admin/auth_token.png new file mode 100644 index 000000000..2f48386cc Binary files /dev/null and b/docs/images/site_admin/auth_token.png differ diff --git a/docs/images/site_admin/custom_settings.png b/docs/images/site_admin/custom_settings.png new file mode 100644 index 000000000..29279883e Binary files /dev/null and b/docs/images/site_admin/custom_settings.png differ diff --git a/docs/images/site_admin/home.png b/docs/images/site_admin/home.png index 89f303dc7..a63cc5863 100644 Binary files a/docs/images/site_admin/home.png and b/docs/images/site_admin/home.png differ diff --git a/docs/images/site_admin/service_settings.png b/docs/images/site_admin/service_settings.png new file mode 100644 index 000000000..c60f8503f Binary files /dev/null and b/docs/images/site_admin/service_settings.png differ diff --git a/docs/images/tethys_compute/tethys_compute_admin.png b/docs/images/tethys_compute/tethys_compute_admin.png index 1f6a4a4f8..ffce2fef0 100644 Binary files a/docs/images/tethys_compute/tethys_compute_admin.png and b/docs/images/tethys_compute/tethys_compute_admin.png differ diff --git a/docs/index.rst b/docs/index.rst index 1498306d9..4838b9f33 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,14 +7,18 @@ Tethys Platform |version| ************************* -**Last Updated:** December 12, 2016 +**Last Updated:** December 2018 -Tethys is a platform that can be used to develop and host environmental web apps. It includes a suite of free and open source software (FOSS) that has been carefully selected to address the unique development needs of water resources web apps. Tethys web apps are developed using a Python software development kit (SDK) which includes programmatic links to each software component. Tethys Platform is powered by the Django Python web framework giving it a solid web foundation with excellent security and performance. Refer to the :doc:`./features` article for an overview of the features of Tethys Platform. +Tethys is a platform that can be used to develop and host environmental web apps. It includes a suite of free and open source software (FOSS) that has been carefully selected to address the unique development needs of environmental web apps. Tethys web apps are developed using a Python software development kit (SDK) which includes programmatic links to each software component. Tethys Platform is powered by the Django Python web framework giving it a solid web foundation with excellent security and performance. Refer to the :doc:`./features` article for an overview of the features of Tethys Platform. .. important:: Tethys Platform |version| has arrived! Check out the :doc:`./whats_new` article for a description of the new features and changes. +.. warning:: + + Python 2 support is officially deprecated in this release. It will no longer be supported in the next release of Tethys Platform. Migrate now! + Contents ======== diff --git a/docs/installation/linux_and_mac.rst b/docs/installation/linux_and_mac.rst index 97ef66b23..3cd4d3d0f 100644 --- a/docs/installation/linux_and_mac.rst +++ b/docs/installation/linux_and_mac.rst @@ -29,7 +29,11 @@ For Systems with `curl` (e.g. Mac OSX and CentOS): curl :install_tethys:`sh` -o ./install_tethys.sh bash install_tethys.sh -b |branch| -.. note:: + +.. _install_script_options: + +Install Script Options +...................... You can customize your tethys installation by passing command line options to the installation script. The available options can be listed by running:: @@ -39,6 +43,8 @@ For Systems with `curl` (e.g. Mac OSX and CentOS): * `-t, --tethys-home `: Path for tethys home directory. Default is ~/tethys. + * `-s, --tethys-src `: + Path to the tethys source directory. Default is ${TETHYS_HOME}/src. * `-a, --allowed-host `: Hostname or IP address on which to serve Tethys. Default is 127.0.0.1. * `-p, --port `: @@ -54,14 +60,24 @@ For Systems with `curl` (e.g. Mac OSX and CentOS): * `-n, --conda-env-name `: Name for tethys conda environment. Default is 'tethys'. - * `--python-version `: - Main python version to install tethys environment into (2 or 3). Default is 2. + * `--python-version ` (deprecated): + Main python version to install tethys environment into (2-deprecated or 3). Default is 3. + .. note:: + + Support for Python 2 is deprecated and will be dropped in Tethys version 3.0. + * `--db-username `: Username that the tethys database server will use. Default is 'tethys_default'. * `--db-password `: Password that the tethys database server will use. Default is 'pass'. + * `--db-super-username `: + Username for super user on the tethys database server. Default is 'tethys_super'. + * `--db-super-password `: + Password for super user on the tethys database server. Default is 'pass'. * `--db-port `: Port that the tethys database server will use. Default is 5436. + * `--db-dir `: + Path where the local PostgreSQL database will be created. Default is ${TETHYS_HOME}/psql. * `-S, --superuser `: Tethys super user name. Default is 'admin'. * `-E, --superuser-email `: @@ -75,6 +91,36 @@ For Systems with `curl` (e.g. Mac OSX and CentOS): If conda home is not in the default location then the `--conda-home` options must also be specified with this option. + * `--partial-tethys-install `: + List of flags to indicate which steps of the installation to do. + + Flags: + * `m` - Install Miniconda + * `r` - Clone Tethys repository (the `--tethys-src` option is required if you omit this flag). + * `c` - Checkout the branch specified by the option `--branch` (specifying the flag `r` will also trigger this flag) + * `e` - Create Conda environment + * `s` - Create `settings.py` file + * `d` - Create a local database server + * `i` - Initialize database server with the Tethys database (specifying the flag `d` will also trigger this flag) + * `u` - Add a Tethys Portal Super User to the user database (specifying the flag `d` will also trigger this flag) + * `a` - Create activation/deactivation scripts for the Tethys Conda environment + * `t` - Create the `t` alias to activate the Tethys Conda environment + + For example, if you already have Miniconda installed and you have the repository cloned and have generated a `settings.py` file, but you want to use the install script to: + + * create a conda environment, + * setup a local database server, + * create the conda activation/deactivation scripts, and + * create the `t` shortcut, + + then you can run the following command:: + + bash install_tethys.sh --partial-tethys-install edat + + .. warning:: + + If `--skip-tethys-install` is used then this option will be ignored. + * `--install-docker`: Flag to include Docker installation as part of the install script (Linux only). See `2. Install Docker (OPTIONAL)`_ for more details. diff --git a/docs/installation/production.rst b/docs/installation/production.rst index 2c807f2c0..8d3022b8e 100644 --- a/docs/installation/production.rst +++ b/docs/installation/production.rst @@ -13,6 +13,3 @@ The following instructions can be used to install Tethys Platform on a productio production/installation production/app_installation production/distributed - production/update - - diff --git a/docs/installation/production/images/geoserver_ssl.png b/docs/installation/production/images/geoserver_ssl.png new file mode 100644 index 000000000..a078f11bd Binary files /dev/null and b/docs/installation/production/images/geoserver_ssl.png differ diff --git a/docs/installation/production/installation.rst b/docs/installation/production/installation.rst index 9c8f9f33a..478f31052 100644 --- a/docs/installation/production/installation.rst +++ b/docs/installation/production/installation.rst @@ -108,8 +108,171 @@ For more information about setting up email capabilities for Tethys Platform, re For an excellent guide on setting up Postfix on Ubuntu, refer to `How To Install and Setup Postfix on Ubuntu 14.04 `_. +.. _production_installation_ssl: -4. Install Apps +4. Setup SSL (https) on the Tethys and Geoserver (Recommended) +============================================================== + +SSL is the standard technology for establishing a secured connection between a web server and a browser. In order to create a secured connection, an SSL certificate and key are needed. An SSL certificate is simply a paragraph with letters and numbers that acts similar to a password. When users visit your website via https this certificate is verified and if it matches, then a connecton is established. An SSL certificate can be self-signed, or purchased from a Certificate Authority. Some of the top certificate authorities include: Digicert, VertiSign, GeoTrust, Comodo, Thawte, GoDaddy, and Nework Solutions. If your instance of Tethys is part of a larger organization, contact your IT to determine if an agreement with one of these authorities already exists. + +Once a certificate is obtained, it needs to be referenced in the Nginx configuration, which is the web server that Tethys uses in production. The configuration file can be found at: + +:: + + /home//tethys/src/tethys_portal/tethys_nginx.conf + +The file should look something like this: +:: + + # tethys_nginx.conf + + # the upstream component nginx needs to connect to + upstream django { + server unix://run/uwsgi/tethys.sock; # for a file socket + } + # configuration of the server + server { + # the port your site will be served on + listen 80; + # the domain name it will serve for + server_name ; # substitute your machine's IP address or FQDN + charset utf-8; + + # max upload size + client_max_body_size 75M; # adjust to taste + + # Tethys Workspaces + location /workspaces { + internal; + alias /home//tethys/workspaces; # your Tethys workspaces files - amend as required + } + + location /static { + alias /home//tethys/static; # your Tethys static files - amend as required + } + + # Finally, send all non-media requests to the Django server. + location / { + uwsgi_pass django; + include /etc/nginx/uwsgi_params; + } + } + +If you need your site to be accessible through both secured (https) and non-secured (http) connections, you will need a server block for each type of connection. Otherwise just edit the existing block. + +Make a copy of the existing non-secured server block and paste it below the original. Then modify it as shown below: + +:: + + server { + + listen 443; + + ssl on; + ssl_certificate /home//tethys/ssl/your_domain_name.pem; (or bundle.crt) + ssl_certificate_key /home//tethys/ssl/your_domain_name.key; + + + # the domain name it will serve for + server_name ; # substitute your machine's IP address or FQDN + charset utf-8; + + # max upload size + client_max_body_size 75M; # adjust to taste + + # Tethys Workspaces + location /workspaces { + internal; + alias /home//tethys/workspaces; # your Tethys workspaces files - amend as required + } + + location /static { + alias /home//tethys/static; # your Tethys static files - amend as required + } + + # Finally, send all non-media requests to the Django server. + location / { + uwsgi_pass django; + include /etc/nginx/uwsgi_params; + } + + +.. Note:: + + SSL works on port 443, hence the server block above listens on 443 instead of 80 + +Geoserver SSL +------------- + +A secured server can only communicate with other secured servers. Therefore to allow the secured Tethys Portal to communicate with Geoserver, the latter needs to be secured as well. To do this, add the following location at the end of your server block. +:: + + server { + + listen 443; + + ssl on; + ssl_certificate /home//tethys/ssl/your_domain_name.pem; (or bundle.crt) + ssl_certificate_key /home//tethys/ssl/your_domain_name.key; + + + # the domain name it will serve for + server_name ; # substitute your machine's IP address or FQDN + charset utf-8; + + # max upload size + client_max_body_size 75M; # adjust to taste + + # Tethys Workspaces + location /workspaces { + internal; + alias /home//tethys/workspaces; # your Tethys workspaces files - amend as required + } + + location /static { + alias /home//tethys/static; # your Tethys static files - amend as required + } + + # Finally, send all non-media requests to the Django server. + location / { + uwsgi_pass django; + include /etc/nginx/uwsgi_params; + } + + #Geoserver + location /geoserver { + proxy_pass http://127.0.0.1:8181/geoserver; + } + +Next, go to your Geoserver web interface (http://domain-name:8181/geoserver/web), sign in, and set the **Proxy Base URL** in Global settings to: +:: + + https:///geoserver + +.. image:: images/geoserver_ssl.png + :width: 600px + :align: center + +Finally, restart uWSGI and Nginx services to effect the changes:: + + $ sudo systemctl restart tethys.uwsgi.service + $ sudo systemctl restart nginx + +.. tip:: + + Use the alias `trestart` as a shortcut to doing the final step. + + +The portal should now be accessible from: https://domain-name + +Geoserver should now be accessible from: https://domain-name/geoserver + +.. Note:: + + Notice that the Geoserver port (8181) is not necessary once the proxy is configured + + +5. Install Apps =============== Download and install any apps that you want to host using this installation of Tethys Platform. For more information see: :doc:`./app_installation`. @@ -118,3 +281,5 @@ Download and install any apps that you want to host using this installation of T .. todo:: **Troubleshooting**: Here we try to provide some guidance on some of the most commonly encountered issues. If you are experiencing problems and can't find a solution here then please post a question on the `Tethys Platform Forum `_. + + diff --git a/docs/installation/production/update.rst b/docs/installation/production/update.rst deleted file mode 100644 index 68257f704..000000000 --- a/docs/installation/production/update.rst +++ /dev/null @@ -1,131 +0,0 @@ -******************** -Upgrade to |version| -******************** - -**Last Updated:** June 2017 - -.. warning:: - - UNDER CONSTRUCTION: Pardon our dust, this documentation has not been updated yet. These instructions will NOT work. We apologize for the inconvenience. - -1. Pull Repository -================== - -When you installed Tethys Platform you did so using it's remote Git repository on GitHub. To get the latest version of Tethys Platform, you will need to pull the latest changes from this repository: - -:: - - $ sudo su - $ cd /usr/lib/tethys/src - $ git pull origin master - $ exit - -2. Install Requirements and Run Setup Script -============================================ - -Install new dependencies and upgrade old ones: - -:: - - $ sudo su - $ . /usr/lib/tethys/bin/activate - (tethys) $ pip install --upgrade -r /usr/lib/tethys/src/requirements.txt - (tethys) $ python /usr/lib/tethys/src/setup.py develop - (tethys) $ exit - - - -3. Generate New Settings Script -=============================== - -Backup your old settings script (``settings.py``) and generate a new settings file to get the latest version of the settings. Then copy any settings (like database usernames and passwords) from the backed up settings script to the new settings script. - -:: - - $ sudo su - (tethys) $ mv /usr/lib/tethys/src/tethys_apps/settings.py /usr/lib/tethys/src/tethys_apps/settings.py_bak - (tethys) $ tethys gen settings -d /usr/lib/tethys/src/tethys_apps - (tethys) $ exit - -.. caution:: - - Don't forget to copy any settings from the backup settings script (``settings.py_bak``) to the new settings script. Common settings that need to be copied include: - - * DEBUG - * ALLOWED_HOSTS - * DATABASES, TETHYS_DATABASES - * STATIC_ROOT, TETHYS_WORKSPACES_ROOT - * EMAIL_HOST, EMAIL_PORT, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_USE_TLS, DEFAULT_FROM_EMAIL - * SOCIAL_OAUTH_XXXX_KEY, SOCIAL_OAUTH_XXXX_SECRET - * BYPASS_TETHYS_HOME_PAGE - - After you have copied these settings, you can delete the backup settings script. - -4. Setup Social Authentication (optional) -========================================= - -If you would like to allow users to signup using their social credentials from Facebook, Google, LinkedIn, and/or HydroShare, follow the :doc:`../../tethys_portal/social_auth` instructions. - -5. Sync the Database -==================== - -Start the database docker if not already started and apply any changes to the database that may have been issued with the new release: - -:: - - $ . /usr/lib/tethys/bin/activate - (tethys) $ tethys docker start -c postgis - (tethys) $ tethys manage syncdb - -.. note:: - - For migration errors use: - - :: - - $ cd ~/usr/lib/tethys/src - $ python manage.py makemigrations --merge - $ tethys manage syncdb - -6. Collect Static Files -======================= - -Collect the new static files and update the old ones: - -:: - - $ sudo su - (tethys) $ tethys manage collectstatic - (tethys) $ exit - -7. Transfer Ownership to Apache -=============================== - -Assign ownership of Tethys Platform files and resources to the Apache user: - -:: - - $ sudo chown -R www-data:www-data /usr/lib/tethys/src /var/www/tethys - -.. note:: - - The name of the Apache user in RedHat or CentOS flavored systems is ``apache``, not ``www-data``. - -8. Restart Apache -================= - -Restart Apache to effect the changes: - -:: - - $ sudo service apache2 restart - -.. note:: - - The command for managing Apache on CentOS or RedHat flavored systems is ``httpd``. Restart as follows: - - :: - - $ sudo service httpd restart - - diff --git a/docs/installation/update.rst b/docs/installation/update.rst index 15887ec0e..35afa6baa 100644 --- a/docs/installation/update.rst +++ b/docs/installation/update.rst @@ -2,42 +2,23 @@ Upgrade to |version| ******************** -**Last Updated:** June 2017 +**Last Updated:** December 2018 -.. warning:: +This document provides a recommendation for how to upgrade Tethys Platform from the last release version. If you have not updated Tethys Platform to the last release version previously, please revisit the documentation for that version and follow those upgrade instructions first. - UNDER CONSTRUCTION: Pardon our dust, this documentation has not been updated yet. These instructions will NOT work. We apologize for the inconvenience. -1. Get the Latest Version -========================= - -When you installed Tethys Platform you did so using it's remote Git repository on GitHub. To get the latest version of Tethys Platform, you will need to pull the latest changes from this repository: - -:: - - $ cd /usr/lib/tethys/src - $ git pull origin master - -2. Install Requirements and Run Setup Script -============================================ - -Install new dependencies and upgrade old ones: +1. Activate Tethys environment and start your Tethys Database: :: - $ . /usr/lib/tethys/bin/activate - (tethys) $ pip install --upgrade -r /usr/lib/tethys/src/requirements.txt - (tethys) $ python /usr/lib/tethys/src/setup.py develop + . t + tstartdb -3. Generate New Settings Script -=============================== - -Backup your old settings script (``settings.py``) and generate a new settings file to get the latest version of the settings. Then copy any settings (like database usernames and passwords) from the backed up settings script to the new settings script. +2. Backup your ``settings.py`` (Note: If you do not backup your ``settings.py`` you will be prompted to overwrite your settings file during upgrade): :: - (tethys) $ mv /usr/lib/tethys/src/tethys_apps/settings.py /usr/lib/tethys/src/tethys_apps/settings.py_bak - (tethys) $ tethys gen settings -d /usr/lib/tethys/src/tethys_apps + mv $TETHYS_HOME/src/tethys_portal/settings.py $TETHYS_HOME/src/tethys_portal/settings_20.py .. caution:: @@ -51,24 +32,48 @@ Backup your old settings script (``settings.py``) and generate a new settings fi * SOCIAL_OAUTH_XXXX_KEY, SOCIAL_OAUTH_XXXX_SECRET * BYPASS_TETHYS_HOME_PAGE - After you have copied these settings, you can delete or archive the backup settings script. +3. (Optional) If you want the new environment to be called ``tethys`` remove the old environment: + +:: + + conda activate base + conda env remove -n tethys + +.. tip:: + + If these commands don't work, you may need to update your conda installation: + + :: + + conda update conda -n root -c defaults -4. Sync the Database -==================== +4. Download and execute the new install tethys script with the following options (Note: if you removed your previous tethys environment, then you can omit the ``-n tethys21`` option to have the new environment called ``tethys``): -Start the database docker if not already started and apply any changes to the database that may have been issued with the new release: +.. parsed-literal:: + + wget :install_tethys:`sh` + bash install_tethys.sh -b |branch| --partial-tethys-install cieast -n tethys21 + +5. (Optional) If you have a locally installed database server then you need to downgrade postgresql to the version that the database was created with. If it was created by the 2.0 Tethys install script then it was created with postgresql version 9.5. (Note: be sure to open a new terminal so that the newly created tethys environment is activated): :: - (tethys) $ tethys docker start -c postgis - (tethys) $ tethys manage syncdb + t + conda install -c conda-forge postgresql=9.5 + + +.. tip:: + + These instructions assume your previous installation was done using the install script with the default configuration. If you used any custom options when installing the environment initially, you will need to specify those same options. For an explanation of the installation script options, see: :ref:`install_script_options`. + + + + + + + + -.. note:: - For migration errors use: - :: - $ cd ~/usr/lib/tethys/src - $ python manage.py makemigrations --merge - $ tethys manage syncdb diff --git a/docs/installation/windows.rst b/docs/installation/windows.rst index 039fa77cf..c4f3be48f 100644 --- a/docs/installation/windows.rst +++ b/docs/installation/windows.rst @@ -35,6 +35,8 @@ As long as the :file:`install_tethys.bat` and the :file:`Miniconda3-latest-Windo * `-t, --tethys-home `: Path for tethys home directory. Default is C:\%HOMEPATH%\tethys. + * `-s, --tethys-src `: + Path for tethys source directory. Default is %TETHYS_HOME%\src. * `-a, --allowed-host `: Hostname or IP address on which to serve Tethys. Default is 127.0.0.1. * `-p, --port `: @@ -52,8 +54,12 @@ As long as the :file:`install_tethys.bat` and the :file:`Miniconda3-latest-Windo Path to Miniconda installer executable. Default is '.\Miniconda3-latest-Windows-x86_64.exe'. * `-n, --conda-env-name `: Name for tethys conda environment. Default is 'tethys'. - * `--python-version `: - Main python version to install tethys environment into (2 or 3). Default is 2. + * `--python-version ` (deprecated): + Main python version to install tethys environment into (2-deprecated or 3). Default is 3. + .. note:: + + Support for Python 2 is deprecated and will be dropped in Tethys version 3.0. + * `--db-username `: Username that the tethys database server will use. Default is 'tethys_default'. * `--db-password `: diff --git a/docs/tethys_portal/admin_pages.rst b/docs/tethys_portal/admin_pages.rst index 75da65beb..9d0bacb98 100644 --- a/docs/tethys_portal/admin_pages.rst +++ b/docs/tethys_portal/admin_pages.rst @@ -2,12 +2,12 @@ Administrator Pages ******************* -**Last Updated:** August 4, 2015 +**Last Updated:** December 2018 -Tethys Portal includes administration pages that can be used to manage the website (see Figure 1). The administration dashboard is only available to administrator users. You should have created a default administrator user when you installed Tethys Platform. If you are logged in as an administrator, you will be able to access the administrator dashboard by selecting the "Site Admin" option from the user drop down menu in the top right-hand corner of the page. +Tethys Portal includes administration pages that can be used to manage the website (see Figure 1). The administration dashboard is only available to administrator users (staff users). You should have created a default administrator user when you installed Tethys Platform. If you are logged in as an administrator, you will be able to access the administrator dashboard by selecting the "Site Admin" option from the user drop down menu in the top right-hand corner of the page (when you are not in an app). .. figure:: ../images/site_admin/home.png - :width: 500px + :width: 675px **Figure 1.** Administrator dashboard for Tethys Portal. @@ -17,19 +17,29 @@ Tethys Portal includes administration pages that can be used to manage the websi :: - $ python /usr/lib/tethys/src/manage.py createsuperuser + $ tethys manage createsuperuser .. _tethys_portal_permissions: -Manage Users and Permissions -============================ +Auth Token +========== + +Tethys REST API tokens for individual users can be managed using the ``Tokens`` link under the ``AUTH TOKEN`` heading (see Figure 2). + +.. figure:: ../images/site_admin/auth_token.png + :width: 675px -Permissions and users can be managed from the administrator dashboard using ``Users`` link under the ``Authentication and Authorization`` heading. Figure 4 shows an example of the user management page for a user named John. +**Figure 2.** Auth Token management page for Tethys Portal. + +Authentication and Authorization +================================ + +Permissions and users can be managed from the administrator dashboard using ``Users`` link under the ``AUTHENTICATION AND AUTHORIZATION`` heading. Figure 3 shows an example of the user management page for a user named John. .. figure:: ../images/tethys_portal/tethys_portal_user_management.png - :width: 500px + :width: 675px -**Figure 4.** User management for Tethys Portal. +**Figure 3.** User management for Tethys Portal. Assign App Permission Groups ---------------------------- @@ -50,39 +60,121 @@ Anonymous User The ``AnonymousUser`` can be used to assign permissions and permission groups to users who are not logged in. This means that you can define permissions for each feature of your app, but then assign them all to the ``AnonymousUser`` if you want the app to be publicly accessible. -Manage Tethys Services -====================== +Python Social Auth +================== + +Tethys leverages the excellent `Python Social Auth `_ to provide support for authenticating with popular servies such as Facebook, Google, LinkedIn, and HydroShare. The links under the ``PYTHON SOCIAL AUTH`` heading can be used to manually manage the social associations and data that is linked to users when they authenticate using Python Social Auth. + +.. tip:: + + For more detailed information on using Python Social Auth in Tethys see the :doc:`./social_auth` documentation. -The administrator pages provide a simple mechanism for linking to the other services of Tethys Platform. Use the ``Spatial Dataset Services`` link to connect your Tethys Portal to GeoServer, the ``Dataset Services`` link to connect to CKAN instances or HydroShare, or the ``Web Processing Services`` link to connect to WPS instances. For detailed instructions on how to perform each of these tasks, refer to the :doc:`../tethys_sdk/tethys_services/spatial_dataset_services`, :doc:`../tethys_sdk/tethys_services/dataset_services`, and :doc:`../tethys_sdk/tethys_services/web_processing_services` documentation, respectively. .. _tethys_portal_terms_and_conditions: -Manage Terms and Conditions -=========================== +Terms and Conditions +==================== Portal administrators can manage and enforce portal wide terms and conditions and other legal documents via the administrator pages. -Use the ``Terms and Conditions`` link to create new legal documents (see Figure 5). To issue an update to a particular document, create a new entry with the same slug (e.g. 'site-terms'), but a different version number (e.g.: 1.10). This allows you to track multiple versions of the legal document and which users have accepted each. The document will not become active until the ``Date active`` field has been set and the date has past. +Use the ``Terms and Conditions`` link to create new legal documents (see Figure 4). To issue an update to a particular document, create a new entry with the same slug (e.g. 'site-terms'), but a different version number (e.g.: 1.10). This allows you to track multiple versions of the legal document and which users have accepted each. The document will not become active until the ``Date active`` field has been set and the date has past. .. figure:: ../images/tethys_portal/tethys_portal_toc_new.png - :width: 500px + :width: 675px -**Figure 5.** Creating a new legal document using the terms and conditions feature. +**Figure 4.** Creating a new legal document using the terms and conditions feature. -When a new document becomes active, users will be presented with a modal prompting them to review and accept the new terms and conditions (see Figure 6). The modal can be dismissed, but will reappear each time a page is refreshed until the user accepts the new versions of the legal documents. +When a new document becomes active, users will be presented with a modal prompting them to review and accept the new terms and conditions (see Figure 5). The modal can be dismissed, but will reappear each time a page is refreshed until the user accepts the new versions of the legal documents. The ``User Terms and Conditions`` link shows a record of which users have accepted the terms and conditions. .. figure:: ../images/tethys_portal/tethys_portal_toc_modal.png - :width: 500px + :width: 675px + +**Figure 5.** Terms and conditions modal. + +Tethys Apps +=========== + +The links under the ``TETHYS APPS`` heading can be used to manage settings for installed apps and extensions. Clicking on the ``Installed Apps`` or ``Installed Extensions`` links will show a list of installed apps or extensions. Clicking on a link for an installed app or extension will bring you to the settings page for that app or extension. There are several different types of app settings: Common Settings, Custom Settings, and Service Settings. + +Common Settings +--------------- + +The Common Settings include those settings that are common to all apps or extension such as the ``Name``, ``Description``, ``Tags``, ``Enabled``, ``Show in apps library``, and ``Enable feedback`` (see Figure 6). Many of these settings correspond with attributes of the term:`app class` and can be overridden by the portal administrator. Other control the visibility or accessibility of the app. + +.. figure:: ../images/site_admin/app_settings_top.png + :width: 675px + +**Figure 6.** App settings page showing Common Settings. + +Custom Settings +--------------- + +Custom Settings appear under the ``CUSTOM SETTINGS`` heading and are defined by the app developer (see Figure 7). Custom Settings have simple values such as strings, integers, floats, or booleans, but all are entered as text. For boolean type Custom Settings, type a valid boolean value such as ``True`` or ``False``. + +.. figure:: ../images/site_admin/custom_settings.png + :width: 675px + +**Figure 7.** Custom Settings section of an app. -**Figure 6.** Terms and conditions modal. +.. _tethys_portal_service_settings: -Manage Computing Resources -========================== +Service Settings +---------------- -Computing resources can be managed using the ``Tethys Compute`` admin pages, which Tethys Portal to link to computing clusters that are managed with HTCondor either locally or on the Cloud. These computational resources are accessed in apps through the :doc:`../tethys_sdk/jobs` and the :doc:`../tethys_sdk/compute`. For more detailed documentation refer to the links below. +There are several different types of Service Settings including: ``Persistent Store Connection Settings``, ``Persistent Store Database Settings``, ``Dataset Service Settings``, ``Spatial Dataset Service Settings``, and ``Web Processing Service Settings`` (see Figure 8). These settings specify the types of services that the apps require. Use the drop down next to each Service Setting to assign a pre-registered ``Tethys Service`` to that app or use the *plus* button to create a new one. + +.. figure:: ../images/site_admin/service_settings.png + :width: 675px + +**Figure 8.** Service Settings sections of an app. + +.. tip:: + + For information on how to define settings for your app see the :doc:`../tethys_sdk/app_settings` documentation. See :ref:`tethys_portal_tethys_services` for how to configure different ``Tethys Services``. + +Tethys Compute +============== + +The links under the ``TETHYS COMPUTE`` heading can be used to manage ``Jobs`` and ``Schedulers``: .. toctree:: :maxdepth: 2 tethys_compute_admin_pages +.. tip:: + + For more information on Tethys Jobs see the :doc:`../tethys_sdk/jobs` and :doc:`../tethys_sdk/compute` documentation. + +Tethys Portal +============= + +The links under the ``TETHYS PORTAL`` heading can be used to customize the look of the Tethys Portal. For example, you can change the name, logo, and color theme of the portal (see Figure 9). + +.. figure:: ../images/tethys_portal/tethys_portal_home_page_settings.png + :width: 500px + +**Figure 9.** Home page settings for Tethys Portal. + +.. tip:: + + For more information on customizing the Tethys Portal see the :doc:`./customize` documentation. + +.. _tethys_portal_tethys_services: + +Tethys Services +=============== + +The links under the ``TETHYS SERVICES`` heading can be used to register external services with Tethys Platform for use by apps and extensions. Use the ``Spatial Dataset Services`` link to register your Tethys Portal to GeoServer, the ``Dataset Services`` link to register to CKAN or HydroShare instances, the ``Web Processing Services`` link to register to WPS instances, or the ``Persistent Store Services`` link to register a database. + + +.. tip:: + + For detailed instructions on how to use each of these services in apps, refer to these docs: + + * :doc:`../tethys_sdk/tethys_services/spatial_dataset_services` + * :doc:`../tethys_sdk/tethys_services/dataset_services` + * :doc:`../tethys_sdk/tethys_services/web_processing_services` + * :doc:`../tethys_sdk/tethys_services/persistent_store` + * :doc:`../tethys_sdk/tethys_services/spatial_persistent_store` + * :ref:`tethys_portal_service_settings` diff --git a/docs/tethys_portal/customize.rst b/docs/tethys_portal/customize.rst index 1abd3b23f..802ca8db1 100644 --- a/docs/tethys_portal/customize.rst +++ b/docs/tethys_portal/customize.rst @@ -57,11 +57,6 @@ Call to Action Text that appears in the call to action banner at the bot Call to Action Button Text that appears on the call to action button in the call to action banner (only visible when user is not logged in). ====================== ================================================================================= -.. figure:: ../images/tethys_portal/tethys_portal_home_page_settings.png - :width: 500px - -**Figure 3.** Home page settings for Tethys Portal. - Bypass the Home Page ==================== diff --git a/docs/tethys_portal/social_auth.rst b/docs/tethys_portal/social_auth.rst index ea9776aba..5220ed1a4 100644 --- a/docs/tethys_portal/social_auth.rst +++ b/docs/tethys_portal/social_auth.rst @@ -340,7 +340,7 @@ For more detailed information about using HydroShare social authentication see t Social Auth Settings ==================== -Social authentication requires Tethys Platform 1.2.0 or later. If you are using an older version of Tethys Platform, you will need to upgrade by following either the :doc:`../installation/update` or the :doc:`../installation/production/update` instructions. The ``settings.py`` script is unaffected by the upgrade. You will need to either generate a new ``settings.py`` script using ``tethys gen settings`` or add the following settings to your existing ``settings.py`` script to support social login. +Social authentication requires Tethys Platform 1.2.0 or later. If you are using an older version of Tethys Platform, you will need to upgrade by following either the :doc:`../installation/update` instructions. The ``settings.py`` script is unaffected by the upgrade. You will need to either generate a new ``settings.py`` script using ``tethys gen settings`` or add the following settings to your existing ``settings.py`` script to support social login. :: diff --git a/docs/tethys_portal/tethys_compute_admin_pages.rst b/docs/tethys_portal/tethys_compute_admin_pages.rst index 84e39995e..81e7c2550 100644 --- a/docs/tethys_portal/tethys_compute_admin_pages.rst +++ b/docs/tethys_portal/tethys_compute_admin_pages.rst @@ -12,6 +12,7 @@ The Tethys Compute settings in site admin allows an administrator to manage comp **Figure 1.** Dashboard for Tethys Compute admin pages. +.. _jobs-label: Jobs ---- @@ -30,7 +31,7 @@ Schedulers are HTCondor nodes that have scheduling rights in the pool they belon :width: 700px :align: left -**Figure 3.** Form for creating a new Scheduler. +**Figure 2.** Form for creating a new Scheduler. .. _scheduler-name-label: diff --git a/docs/tethys_sdk.rst b/docs/tethys_sdk.rst index c07f2f064..b61fa09a4 100644 --- a/docs/tethys_sdk.rst +++ b/docs/tethys_sdk.rst @@ -2,26 +2,24 @@ Software Development Kit ************************ -**Last Updated:** May 2017 +**Last Updated:** February 22, 2018 -The Tethys Platform provides a Python Software Development Kit (SDK) to make it easier to incorporate the functionality -of the various supporting software packages into apps. The SDK is includes an Application Programming Interface (API) -for each of the major software components of Tethys Platform. This section contains the documentation for each API that -is included in the SDK: +The Tethys Platform provides a Python Software Development Kit (SDK) to make it easier to incorporate the functionality of the various supported software packages into apps. The SDK is includes an Application Programming Interface (API) for each of the major software components of Tethys Platform. This section contains the documentation for each API that is included in the SDK: .. toctree:: :maxdepth: 2 + tethys_sdk/tethys_cli tethys_sdk/app_class - tethys_sdk/app_settings - tethys_sdk/tethys_services tethys_sdk/templating - tethys_sdk/gizmos + tethys_sdk/app_settings tethys_sdk/compute - tethys_sdk/jobs - tethys_sdk/workspaces tethys_sdk/handoff - tethys_sdk/rest_api + tethys_sdk/jobs tethys_sdk/permissions - tethys_sdk/tethys_cli + tethys_sdk/rest_api + tethys_sdk/gizmos tethys_sdk/testing + tethys_sdk/extensions + tethys_sdk/tethys_services + tethys_sdk/workspaces diff --git a/docs/tethys_sdk/extensions.rst b/docs/tethys_sdk/extensions.rst new file mode 100644 index 000000000..cf43e0b9e --- /dev/null +++ b/docs/tethys_sdk/extensions.rst @@ -0,0 +1,17 @@ +********************* +Tethys Extensions API +********************* + +**Last Updated:** February 22, 2018 + +Tethys Extensions provide a way for app developers to extend Tethys Platform and modularize functionality that is used accross multiple apps. For example, models, templates and static resources that are used by multiple apps could be created in a Tethys Extension and imported/referenced in the apps that use that functionality. Developers can also create custom gizmos using Tethys Extensions. This section will provide an overview of how to develop Tethys Extensions and use them in apps. + +.. toctree:: + :maxdepth: 2 + + extensions/scaffold + extensions/structure + extensions/templates_and_static_files + extensions/url_maps + extensions/models + extensions/custom_gizmos diff --git a/docs/tethys_sdk/extensions/custom_gizmos.rst b/docs/tethys_sdk/extensions/custom_gizmos.rst new file mode 100644 index 000000000..7d5eac350 --- /dev/null +++ b/docs/tethys_sdk/extensions/custom_gizmos.rst @@ -0,0 +1,265 @@ +************* +Custom Gizmos +************* + +**Last Updated:** February 22, 2018 + +Tethys Extensions can be used to create custom gizmos, which can then be used by any app in portals where the extension is installed. This document will provide a brief overview of how to create a gizmo. + +Anatomy of a Gizmo +------------------ + +Gizmos are essentially mini-templates that can be embedded in other templates using the ``gizmo`` tag. They are composed of three primary components: + + #. Gizmo Options Class + #. Gizmo Template + #. JavaScript and CSS Dependencies + +Each component will be briefly introduced. To illustrate, we will show how a simplified version of the ``SelectInput`` gizmo could be implemented as a custom Gizmo in an extension. + +Gizmo Organization +------------------ + +The files used to define custom gizmos must be organized in a specific way in your app extension. Each gizmo options class must be located in its own python module and the file should be located in the ``gizmos`` package of your extension. The template for the gizmo must an HTML file located within the ``templates/gizmos/`` folder of your extension. + +Gizmo files must follow a specific naming convention: the python module containing the gizmo options class and the name of the gizmo template must have the same name as the gizmo. For example, if the name of the gizmo you are creating is ``custom_select_input`` then the name of the gizmo template would be ``custom_select_input.html`` and the name of the gizmo options module would be ``custom_select_input.py``. + +JavaScript and CSS dependencies should be stored in the ``public`` directory of your extension as usual or be publicly available from a CDN or similar. Dependencies stored locally can be organized however you prefer within the ``public`` directory. + +Finally, you must import the gizmo options class in the ``gizmos/__init__.py`` module. Only Gizmos imported here will be accessible. For the custom select input example, the file structure would look something like this: + +:: + + tethysext-my_first_extension/ + |-- tethysext/ + | |-- my_first_extension/ + | | |-- gizmos/ + | | | |-- custom_select_input.py + | | |-- public/ + | | | |-- gizmos/ + | | | | |-- custom_select_input/ + | | | | | |-- custom_select_input.css + | | | | | |-- custom_select_input.js + | | |-- templates/ + | | | |-- gizmos/ + | | | | |-- custom_select_input.html + +.. important:: + + Gizmo names must be globally unique within a portal. If a portal has two extensions that implement gizmos with the same name, they will conflict and likely not work properly. + + +Gizmo Options Class +------------------- + +A gizmo options class is a class that inherits from the ``TethysGizmoOptions`` base class. It can be thought of as the context for the gizmo template. Any property or attribute of the gizmo options class will be available as a variable in the Gizmo Template. + +For the custom select input gizmo, create a new python module in the ``gizmos`` package called ``custom_select_input.py`` and add the following contents: + +:: + + from tethys_sdk.gizmos import TethysGizmoOptions + + + class CustomSelectInput(TethysGizmoOptions): + """ + Custom select input gizmo. + """ + gizmo_name = 'custom_select_input' + + def __init__(self, name, display_text='', options=(), initial=(), multiselect=False, + disabled=False, error='', **kwargs): + """ + constructor + """ + # Initialize parent + super(CustomSelectInput, self).__init__(**kwargs) + + # Initialize Attributes + self.name = name + self.display_text = display_text + self.options = options + self.initial = initial + self.multiselect = multiselect + self.disabled = disabled + self.error = error + +It is important that ``gizmo_name`` property is the same as the name of the python module and template for the gizmo. Also, it is important to include ``**kwargs`` as an argument to your contstructor and use it to initialize the parent ``TethysGizmoOptions`` object. This will catch any of the parameters that are common to all gizmos like ``attributes`` and ``classes``. + +After defining the gizmo options class, import it in the ``gizmos/__init__.py`` module: + +:: + + from custom_select_input import CustomSelectInput + + +Gizmo Template +-------------- + +Gizmo templates are similar to the templates used for Tethys apps, but much simpler. + +For the custom select input gizmo, create a new template in the ``templates/gizmos/`` directory with the same name as your gizmo, ``custom_select_input.html``, with the following contents: + +.. code-block:: django + + {% load staticfiles %} + +
+ {% if display_text %} + + {% endif %} + + {% if error %} +

{{ error }}

+ {% endif %} +
+ +The variables in this template are defined by the attributes of the gizmo options object. Notice how the ``classes`` and ``attributes`` variables are handled. It is a good idea to handle these variables for each of your gizmos, because most gizmos support them and developers will expect them. + + +JavaScript and CSS Dependencies +------------------------------- + +Some gizmos have JavaScript and/or CSS dependencies. The ``TethysGizmoOptions`` base class provides methods for specifying different types of dependencies: + +* ``get_vendor_js``: For vendor/3rd party javascript. +* ``get_vendor_css``: For vendor/3rd party css. +* ``get_gizmo_js``: For your custom javascript. +* ``get_gizmo_css``: For your custom css. +* ``get_tethys_gizmos_js``: For global gizmo javascript. Changing this could cause other gizmos to stop working. Best not to mess with it unless you know what you are doing. +* ``get_tethys_gizmos_css``: For global gizmo css. Changing this could cause other gizmos to stop working. Best not to mess with it unless you know what you are doing. + +.. note:: + Tethys provides ``Twitter Bootstrap`` and ``jQuery``, so you don't need to include these as gizmo dependencies. + +The custom select input depends on the select2 libraries and some custom javascript and css. Create ``custom_select_input.js`` and ``custom_select_input.css`` in the ``public/gizmos/custom_select_input/`` directory, creating the directory as well. Add the following contents to each file: + +Add this content to the ``custom_select_input.css`` file: + +.. code-block:: css + + .select2 { + width: 100%; + } + +Add this content to the ``custom_select_input.js`` file: + +.. code-block:: javascript + + $(document).ready(function() { + $('.select2').select2(); + }); + + +Modify the gizmo options class to include these dependencies: + +:: + + from tethys_sdk.gizmos import TethysGizmoOptions + + + class CustomSelectInput(TethysGizmoOptions): + """ + Custom select input gizmo. + """ + gizmo_name = 'custom_select_input' + + def __init__(self, name, display_text='', options=(), initial=(), multiselect=False, + disabled=False, error='', **kwargs): + """ + constructor + """ + # Initialize parent + super(CustomSelectInput, self).__init__(**kwargs) + + # Initialize Attributes + self.name = name + self.display_text = display_text + self.options = options + self.initial = initial + self.multiselect = multiselect + self.disabled = disabled + self.error = error + + @staticmethod + def get_vendor_js(): + """ + JavaScript vendor libraries. + """ + return ('https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js',) + + @staticmethod + def get_vendor_css(): + """ + CSS vendor libraries. + """ + return ('https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css',) + + @staticmethod + def get_gizmo_js(): + """ + JavaScript specific to gizmo. + """ + return ('my_first_extension/gizmos/custom_select_input/custom_select_input.js',) + + @staticmethod + def get_gizmo_css(): + """ + CSS specific to gizmo . + """ + return ('my_first_extension/gizmos/custom_select_input/custom_select_input.css',) + +Using a Custom Gizmo +-------------------- + +To use a custom gizmo in an app, import the gizmo options object from the extension and create a new instance fo the gizmo in the app controller. Then use it with the ``gizmo`` template tag as normal. + + +Import and create a new instance of the gizmo in your controller: + +:: + + from tethysext.my_first_extension.gizmos import CustomSelectInput + + + def my_app_controller(request): + """ + Example controller using extension gizmo + """ + my_select = CustomSelectInput( + name = 'my_select', + display_text = 'Select One:', + options = (('Option 1', '1'), ('Option 2', '2'), ('Option 3', '3')), + initial = ('2') + ) + + context = { + 'my_select': my_select, + } + return render(request, 'my_first_app/a_template.html', context) + +Then use the gizmo as usual in ``a_template.html``: + +.. code-block::django + + {% load tethys_gizmos %} + + {% gizmo my_select %} + diff --git a/docs/tethys_sdk/extensions/models.rst b/docs/tethys_sdk/extensions/models.rst new file mode 100644 index 000000000..d7bdff37a --- /dev/null +++ b/docs/tethys_sdk/extensions/models.rst @@ -0,0 +1,66 @@ +****** +Models +****** + +**Last Updated:** February 22, 2018 + +Extensions are not able to be linked to databases, but they can be used to store SQLAlchemy models that are used by multiple apps. Define the SQLAlchemy model as you would normally: + +:: + + import datetime + from sqlalchemy import Column + from sqlalchemy.types import Integer, String, DateTime + from sqlalchemy.ext.declarative import declarative_base + + + MyFirstExtensionBase = declarative_base() + + + class Project(MyFirstExtensionBase): + """ + SQLAlchemy interface for projects table + """ + __tablename__ = 'projects' + + id = Column(Integer, autoincrement=True, primary_key=True) + name = Column(String) + description = Column(String) + date_created = Column(DateTime, default=datetime.datetime.utcnow) + + +To initialize the tables using a model defined in an extension, import the declarative base from the extension in the initializer function for the persistent store database you'd like to initialize: + +:: + + from tethyext.my_first_extension.models import MyFirstExtensionBase + + + def init_primary_db(engine, first_time): + """ + Initializer for the primary database. + """ + # Create all the tables + MyFirstExtensionBase.metadata.create_all(engine) + +To use the extension models to query the database, import them from the extension and use like usual: + +:: + + from tethysapp.my_first_app.app import MyFirstApp as app + from tethysext.my_first_extension.models import Project + + + def my_controller(request, project_id): + """ + My app controller. + """ + SessionMaker = app.get_persistent_store_database('primary_db', as_sessionmaker=True) + session = SessionMaker() + project = session.query(Project).get(project_id) + + context = { + 'project': project + } + + return render(request, 'my_first_app/some_template.html', context) \ No newline at end of file diff --git a/docs/tethys_sdk/extensions/scaffold.rst b/docs/tethys_sdk/extensions/scaffold.rst new file mode 100644 index 000000000..2787698aa --- /dev/null +++ b/docs/tethys_sdk/extensions/scaffold.rst @@ -0,0 +1,50 @@ +************************* +Scaffold and Installation +************************* + +**Last Updated:** February 22, 2018 + +Scaffolding an Extension +------------------------ + +Scaffolding Tethys Extensions is done in the same way scaffolding of apps is performed. Just specify the extension option when scaffolding: + +:: + + $ tethys scaffold -e my_first_extension + +Installing an Extension +----------------------- + +This will create a new directory called ``tethysext-my_first_extension``. To install the extension for development into your Tethys Portal: + +:: + + $ cd tethysext-my_first_extension + $ python setup.py develop + +Alternatively, to install the extension on a production Tethys Portal: + +:: + + $ cd tethysext-my_first_extension + $ python setup.py install + +If the installation was successful, you should see something similar to this when Tethys Platform loads: + +:: + + Loading Tethys Extensions... + Tethys Extensions Loaded: my_first_extension + +You can also confirm the installation of an extension by navigating to the *Site Admin* page and selecting the ``Installed Extensions`` link under the ``Tethys Apps`` heading. + +Uninstalling an Extension +------------------------- + +An extension can be easily uninstalled using the ``uninstall`` command provided in the Tethys CLI: + +:: + + $ tethys uninstall -e my_first_extension + diff --git a/docs/tethys_sdk/extensions/structure.rst b/docs/tethys_sdk/extensions/structure.rst new file mode 100644 index 000000000..02f7dd8fb --- /dev/null +++ b/docs/tethys_sdk/extensions/structure.rst @@ -0,0 +1,38 @@ +************************ +Extension File Structure +************************ + +**Last Updated:** February 22, 2018 + +The Tethys Extension file structure mimics that of Tethys Apps. Like apps, extensions have the ``templates`` and ``public`` directories and the ``controllers.py`` and ``model.py`` modules. These modules and directories are used in the same way as they are in apps. + +There are some notable differences between apps and extensions, however. Rather than an ``app.py`` module, the configuration file for extensions is called ``ext.py``. Like the ``app.py``, the ``ext.py`` module contains a class that is used to configure the extension. Extensions also contain additional packages and directories such as the ``gizmos`` package and ``templates/gizmos`` directory. + +.. note:: + + Although extensions and apps are similar, extension classes do not support as many operations as the app classes. For example, you cannot specify any service settings (persistent store, spatial dataset, etc.) for extensions, nor can you perform the syncstores on an extension. The capabilities of extensions will certainly grow over time, but some limitations are deliberate. + +The sturcture of a freshly scaffolded extension should looks something like this: + +:: + + tethysext-my_first_extension/ + |-- tethysext/ + | |-- my_first_extension/ + | | |-- gizmos/ + | | | |-- __init__.py + | | |-- public/ + | | | |-- js/ + | | | | |-- main.js + | | | |-- css/ + | | | | |-- main.css + | | |-- templates/ + | | | |-- my_first_extension/ + | | | |-- gizmos/ + | | |-- __init__.py + | | |-- contollers.py + | | |-- ext.py + | | |-- model.py + | |-- __init__.py + |-- .gitignore + |-- setup.py \ No newline at end of file diff --git a/docs/tethys_sdk/extensions/templates_and_static_files.rst b/docs/tethys_sdk/extensions/templates_and_static_files.rst new file mode 100644 index 000000000..7bff8f622 --- /dev/null +++ b/docs/tethys_sdk/extensions/templates_and_static_files.rst @@ -0,0 +1,28 @@ +************************** +Templates and Static Files +************************** + +**Last Updated:** February 22, 2018 + +Templates and static files in extensions can be used in other apps. The advantage to using templates and static files from extensions in your apps is that when you update the template or static file in the extension, all the apps that use them will automatically be updated. Just as with apps, store the templates in the ``templates`` directory and store the static files (css, js, images, etc.) in the ``public`` directory. Then reference the template or static file in your app's controllers and templates using the namespaced path. + +For example, to use an extension template in one of your app's controllers: + +:: + + def my_controller(request): + """ + A controller in my app, not the extension. + """ + ... + return render(request, 'my_first_extension/a_template.html', context) + +You can reference static files in your app's templates using the ``static`` tag, just as you would any other static resource: + +.. code-block:: html + + {% load staticfiles %} + + + + \ No newline at end of file diff --git a/docs/tethys_sdk/extensions/url_maps.rst b/docs/tethys_sdk/extensions/url_maps.rst new file mode 100644 index 000000000..eb57f757e --- /dev/null +++ b/docs/tethys_sdk/extensions/url_maps.rst @@ -0,0 +1,37 @@ +*********************** +UrlMaps and Controllers +*********************** + +**Last Updated:** February 22, 2018 + +Although ``UrlMaps`` and controllers defined in extensions are loaded, it is not recommended that you use them to load normal html pages. Rather, use ``UrlMaps`` in extensions to define REST endpoints that handle any dynamic calls used by your custom gizmos and templates. ``UrlMaps`` are defined in extensions in the ``ext.py`` in the same way that they are defined in apps: + +:: + + from tethys_sdk.base import TethysExtensionBase + from tethys_sdk.base import url_map_maker + + + class MyFirstExtension(TethysExtensionBase): + """ + Tethys extension class for My First Extension. + """ + name = 'My First Extension' + package = 'my_first_extension' + root_url = 'my-first-extension' + description = 'This is my first extension.' + + def url_maps(): + """ + Map controllers to URLs. + """ + UrlMap = url_map_maker(self.root_url) + + return ( + UrlMap( + name='get_data', + url='my-first-extension/rest/get-data', + controller='my_first_extension.controllers.get_data + ), + ) + diff --git a/docs/tethys_sdk/jobs.rst b/docs/tethys_sdk/jobs.rst index e6236a5ab..a08da0b77 100644 --- a/docs/tethys_sdk/jobs.rst +++ b/docs/tethys_sdk/jobs.rst @@ -2,75 +2,102 @@ Jobs API ******** -**Last Updated:** September 12, 2016 +**Last Updated:** December 27, 2018 The Jobs API provides a way for your app to run asynchronous tasks (meaning that after starting a task you don't have to wait for it to finish before moving on). As an example, you may need to run a model that takes a long time (potentially hours or days) to complete. Using the Jobs API you can create a job that will run the model, and then leave it to run while your app moves on and does other stuff. You can check the job's status at any time, and when the job is done the Jobs API will help retrieve the results. Key Concepts ============ -To facilitate interacting with jobs asynchronously, they are stored in a database. The Jobs API provides a job manager to handle the details of working with the database, and provides a simple interface for creating and retrieving jobs. The first step to creating a job is to define a job template. A job template is like a blue print that describes certain key characteristics about the job, such as the job type and where the job will be run. The job manager uses a job template to create a new job that has all of the attributes that were defined in the template. Once a job has been created from a template it can then be customized with any additional attributes that are needed for that specific job. +To facilitate interacting with jobs asynchronously, the details of the jobs are stored in a database. The Jobs API provides a job manager to handle the details of working with the database, and provides a simple interface for creating and retrieving jobs. The Jobs API supports various types of jobs (see `Job Types`_). +.. deprecated::2.1 + Creating jobs used to be done via job templates. This method is now deprecated. -The Jobs API supports various types of jobs (see `Job Types`_). +.. _job-manager-label: -.. note:: - The real power of the jobs API comes when it is combined with the :doc:`compute`. This make it possible for jobs to be offloaded from the main web server to a scalable computing cluster, which in turn enables very large scale jobs to be processed. This is done through the :doc:`jobs/condor_job_type` or the :doc:`jobs/condor_workflow_type`. +Job Manager +=========== +The Job Manager is used in your app to interact with the jobs database. It facilitates creating and querying jobs. -.. seealso:: - The Condor Job and the Condor Workflow job types use the CondorPy library to submit jobs to HTCondor compute pools. For more information on CondorPy and HTCondor see the `CondorPy documentation `_ and specifically the `Overview of HTCondor `_. +Using the Job Manager in your App +--------------------------------- +To use the Job Manager in your app you first need to import the TethysAppBase subclass from the app.py module: + +:: -Defining Job Templates -====================== -To create jobs in an app you first need to define job templates. A job template specifies the type of job, and also defines all of the static attributes of the job that will be the same for all instances of that template. These attributes often include the names of the executable, input files, and output files. Job templates are defined in a method on the ``TethysAppBase`` subclass in ``app.py`` module. The following code sample shows how this is done: + from app import MyFirstApp as app + +You can then get the job manager by calling the method ``get_job_manager`` on the app. :: - from tethys_sdk.jobs import CondorJobTemplate, CondorJobDescription - from tethys_sdk.compute import list_schedulers + job_manager = app.get_job_manager() + +You can now use the job manager to create a new job, or retrieve an existing job or jobs. + +Creating and Executing a Job +---------------------------- +To create a new job call the ``create_job`` method on the job manager. The required arguments are: + + * ``name``: A unique string identifying the job + * ``user``: A user object, usually from the request argument: `request.user` + * ``job_type``: A string specifying on of the supported job types (see `Job Types`_) + +Any other job attributes can also be passed in as `kwargs`. - def job_templates(cls): - """ - Example job_templates method. - """ - my_scheduler = list_schedulers()[0] +:: - my_job_description = CondorJobDescription(condorpy_template_name='vanilla_transfer_files', - remote_input_files=('$(APP_WORKSPACE)/my_script.py', '$(APP_WORKSPACE)/input_1', '$(USER_WORKSPACE)/input_2'), - executable='my_script.py', - transfer_input_files=('../input_1', '../input_2'), - transfer_output_files=('example_output1', example_output2), - ) + # get the path to the app workspace to reference job files + app_workspace = app.get_app_workspace().path - job_templates = (CondorJobTemplate(name='example', - job_description=my_job_description, - scheduler=my_scheduler, - ), - ) + # create a new job from the job manager + job = job_manager.create_job( + name='myjob_{id}', # required + user=request.user, # required + job_type='CONDOR', # required - return job_templates + # any other properties can be passed in as kwargs + attributes=dict(attribute1='attr1'), + condorpy_template_name='vanilla_transfer_files', + remote_input_files=( + os.path.join(app_workspace, 'my_script.py'), + os.path.join(app_workspace, 'input_1'), + os.path.join(app_workspace, 'input_2') + ) + ) -.. note:: - To define job templates the appropriate template class and any supporting classes must be imported from ``tethys_sdk.jobs``. In this case the template class `CondorJobTemplate` is imported along with the supporting class `CondorJobDescription`. + # properties can also be added after the job is created + job.extended_properties = {'one': 1, 'two': 2} -There is a corresponding job template class for every job type. In this example the `CondorJobTemplate` class is used, which corresponds to the :doc:`jobs/condor_job_type`. For a list of all possible job types see `Job Types`_. + # each job type may provided methods to further specify the job + job.set_attribute('executable', 'my_script.py') -When instantiating any job template class there is a required ``name`` parameter, which is used used to identify the template to the job manager (see `Using the Job Manager in your App`_). The template class for each job type may have additional required and/or optional parameters. In the above example the `job_description` and the `scheduler` parameters are required because the the `CondorJobTemplate` class is being instantiated. Job template classes may also support setting job attributes as parameters in the template. See the `Job Types`_ documentation for a list of acceptable parameters for the template class of each job type. + # save or execute the job + job.save() + # or + job.execute() + +Before a controller returns a response the job must be saved or else all of the changes made to the job will be lost (executing the job automatically saves it). If submitting the job takes a long time (e.g. if a large amount of data has to be uploaded to a remote scheduler) then it may be best to use AJAX to execute the job. -.. warning:: - The generic template class ``JobTemplate`` allong with the dictionary ``JOB_TYPES`` have been used to define job templates in the past but are being deprecated in favor of job-type specific templates classes (e.g. `CondorJobTemplate` or `CondorWorkflowTemplate`). +.. tip:: + The `Jobs Table Gizmo`_ has a built-in mechanism for submitting jobs with AJAX. If the `Jobs Table Gizmo`_ is used to submit the jobs then be sure to save the job after it is created. Job Types --------- -The Jobs API is designed to support multiple job types. Each job type provides a different framework and environment for executing jobs. To create a job of a particular job type, you must first create a job template from the template class corresponding to that job type (see `Defining Job Templates`_). After the job template for the job type you want has been instantiated you can create a new job instance using the job manager (see `Using the Job Manager in your App`_). +The Jobs API is designed to support multiple job types. Each job type provides a different framework and environment for executing jobs. When creating a new job you must specify its type by passing in the `job_type` argument. Currently the supported job types are: + + * 'BASIC' + * 'CONDOR' or 'CONDORJOB' + * 'CONDORWORKFLOW' -Once you have a newly created job from the job manager you can then customize the job by setting job attributes. All jobs have a common set of attributes, and then each job type may add additional attributes. +Additional job attributes can be passed into the `create_job` method of the job manager or they can be specified after the job is instantiated. All jobs have a common set of attributes, and then each job type may add additional attributes. The following attributes can be defined for all job types: * ``name`` (string, required): a unique identifier for the job. This should not be confused with the job template name. The template name identifies a template from which jobs can be created and is set when the template is created. The job ``name`` attribute is defined when the job is created (see `Creating and Executing a Job`_). * ``description`` (string): a short description of the job. - * ``workspace`` (string): a path to a directory that will act as the workspace for the job. Each job type may interact with the workspace differently. By default the workspace is set to the user's workspace in the app that is creating the job (see `Workspaces`_). + * ``workspace`` (string): a path to a directory that will act as the workspace for the job. Each job type may interact with the workspace differently. By default the workspace is set to the user's workspace in the app that is creating the job. * ``extended_properties`` (dict): a dictionary of additional properties that can be used to create custom job attributes. @@ -95,9 +122,6 @@ All job types also have the following read-only attributes: \*used for job types with multiple sub-jobs (e.g. CondorWorkflow). -.. note:: - Job template classes may support passing in job attributes as additional arguments. See the documentation for each job type for a list of acceptable parameters for each template class add if additional arguments are supported. - Specific job types may define additional attributes. The following job types are available. .. toctree:: @@ -108,55 +132,6 @@ Specific job types may define additional attributes. The following job types are jobs/condor_workflow_type - -Workspaces ----------- -Each job has it's own workspace, which by default is set to the user's workspace in the app where the job is created. However, the job may need to reference files that are in other workspaces. To make it easier to interact with workspaces in job templates, two special variables are defined: ``$(APP_WORKSPACE)`` and ``$(USER_WORKSPACE)``. These two variables are resolved to absolute paths when the job is created. These variables can only be used in job templates. To access the app's workspace and the user's workspace when working with a job in other places in your app use the :doc:`workspaces`. - -.. _job-manager-label: - -Job Manager -=========== -The Job Manager is used in your app to interact with the jobs database. It facilitates creating and querying jobs. - -Using the Job Manager in your App -================================= -To use the Job Manager in your app you first need to import the TethysAppBase subclass from the app.py module: - -:: - - from app import MyFirstApp as app - -You can then get the job manager by calling the method ``get_job_manager`` on the app. - -:: - - job_manager = app.get_job_manager() - -You can now use the job manager to create a new job, or retrieve an existing job or jobs. - -Creating and Executing a Job ----------------------------- -To create a job call the ``create_job`` method on the job manager. The required parameters are ``name``, ``user`` and ``template_name``. Any other job attributes can also be passed in as `kwargs`. - -:: - - # create a new job - job = job_manager.create_job(name='job_name', user=request.user, template_name='example', description='my first job') - - # customize the job using methods provided by the job type - job.set_attribute('arguments', 'input_2') - - # save or execute the job - job.save() - # or - job.execute() - -Before a controller returns a response the job must be saved or else all of the changes made to the job will be lost (executing the job automatically saves it). If submitting the job takes a long time (e.g. if a large amount of data has to be uploaded to a remote scheduler) then it may be best to use AJAX to execute the job. - -.. tip:: - The `Jobs Table Gizmo`_ has a built-in mechanism for submitting jobs with AJAX. If the `Jobs Table Gizmo`_ is used to submit the jobs then be sure to save the job after it is created. - Retrieving Jobs --------------- Two methods are provided to retrieve jobs: ``list_jobs`` and ``get_job``. Jobs are automatically filtered by app. An optional ``user`` parameter can be passed in to these methods to further filter jobs by the user. @@ -232,7 +207,7 @@ API Documentation .. autoclass:: tethys_compute.job_manager.JobManager :members: create_job, list_jobs, get_job, get_job_status_callback_url -.. autoclass:: tethys_sdk.jobs.JobTemplate +.. autoclass:: tethys_compute.models.TethysJob References ========== @@ -240,4 +215,4 @@ References .. toctree:: :maxdepth: 1 - jobs/condor_job_description \ No newline at end of file + jobs/condor_job_description diff --git a/docs/tethys_sdk/jobs/basic_job_type.rst b/docs/tethys_sdk/jobs/basic_job_type.rst index 5016dcbef..9d4dcee1b 100644 --- a/docs/tethys_sdk/jobs/basic_job_type.rst +++ b/docs/tethys_sdk/jobs/basic_job_type.rst @@ -2,45 +2,32 @@ Basic Job Type ************** -**Last Updated:** March 29, 2016 +**Last Updated:** December 27, 2018 -The Basic Job type is a sample job type for creating dummy jobs. It has all of the basic properties and methods of a job, but it doesn't have any mechanism for running jobs. It's primary purpose is for demonstration. There are no additional attributes for the BasicJob type other than the common set of job attributes. The only required parameter for the `BasicJobTemplate` class is ``name``, but it also supports passing in other job attributes as additional arguments. +The Basic Job type is a sample job type for creating dummy jobs. It has all of the basic properties and methods of a job, but it doesn't have any mechanism for running jobs. It's primary purpose is for demonstration. There are no additional attributes for the BasicJob type other than the common set of job attributes. -Setting up a BasicJobTemplate -============================= -:: - - from tethys_sdk.jobs import BasicJobTemplate - - def job_templates(cls): - """ - Example job_templates method with a BasicJob type. - """ - - job_templates = (BasicJobTemplate(name='example', - description='This is a sample basic job. It can't actually compute anything.', - extended_properties={'app_spcific_property': 'default_value', - 'persistent_store_id': None, # Will be defined when job is created - } - ), - ) - - return job_templates - -Creating and Customizing a Job -============================== -To create a job call the ``create_job`` method on the job manager. The required parameters are ``name``, ``user`` and ``template_name``. Any other job attributes can also be passed in as `kwargs`. +Creating a Basic Job +==================== +To create a job call the ``create_job`` method on the job manager. The required parameters are ``name``, ``user`` and ``job_type``. Any other job attributes can also be passed in as `kwargs`. :: # create a new job - job = job_manager.create_job(name='unique_job_name', user=request.user, template_name='example', description='my first job') + job = job_manager.create_job( + name='unique_job_name', + user=request.user, + template_name='BASIC', + description='This is a sample basic job. It can't actually compute anything.', + extended_properties={ + 'app_spcific_property': 'default_value', + } + ) Before a controller returns a response the job must be saved or else all of the changes made to the job will be lost (executing the job automatically saves it). If submitting the job takes a long time (e.g. if a large amount of data has to be uploaded to a remote scheduler) then it may be best to use AJAX to execute the job. API Documentation ================= -.. autoclass:: tethys_sdk.jobs.BasicJobTemplate +.. autoclass:: tethys_compute.models.BasicJob -.. autoclass:: tethys_compute.models.BasicJob \ No newline at end of file +.. autoclass:: tethys_sdk.jobs.BasicJobTemplate \ No newline at end of file diff --git a/docs/tethys_sdk/jobs/condor_job_description.rst b/docs/tethys_sdk/jobs/condor_job_description.rst index 602cfd05d..7f852dc22 100644 --- a/docs/tethys_sdk/jobs/condor_job_description.rst +++ b/docs/tethys_sdk/jobs/condor_job_description.rst @@ -4,6 +4,8 @@ Condor Job Description **Last Updated:** March 29, 2016 +**DEPRECATED** + Both the :doc:`./condor_job_type` or the :doc:`./condor_workflow_type` facilitate running jobs with HTCondor using the CondorPy library, and both use ``CondorJobDescription`` objects which stores attributes used to initialize the CondorPy job. The ``CondorJobDescription`` accepts as parameters any HTCondor job attributes. .. note:: diff --git a/docs/tethys_sdk/jobs/condor_job_type.rst b/docs/tethys_sdk/jobs/condor_job_type.rst index 42df3f530..d94e87c9c 100644 --- a/docs/tethys_sdk/jobs/condor_job_type.rst +++ b/docs/tethys_sdk/jobs/condor_job_type.rst @@ -2,60 +2,68 @@ Condor Job Type *************** -**Last Updated:** March 29, 2016 +**Last Updated:** December 27, 2018 +The :doc:`condor_job_type` (and :doc:`condor_workflow_type`) enable the real power of the jobs API by combining it with the :doc:`../compute`. This make it possible for jobs to be offloaded from the main web server to a scalable computing cluster, which in turn enables very large scale jobs to be processed. -Setting up a CondorJobTemplate -============================== -:: +.. seealso:: + The Condor Job and the Condor Workflow job types use the CondorPy library to submit jobs to HTCondor compute pools. For more information on CondorPy and HTCondor see the `CondorPy documentation `_ and specifically the `Overview of HTCondor `_. - from tethys_sdk.jobs import CondorJobTemplate, CondorJobDescription - from tethys_sdk.compute import list_schedulers - def job_templates(cls): - """ - Example job_templates method. - """ - my_scheduler = list_schedulers()[0] +Creating a Condor Job +===================== +To create a job call the ``create_job`` method on the job manager. The required parameters are ``name``, ``user`` and ``job_type``. Any other job attributes can also be passed in as `kwargs`. - my_job_description = CondorJobDescription(condorpy_template_name='vanilla_transfer_files', - remote_input_files=('$(APP_WORKSPACE)/my_script.py', '$(APP_WORKSPACE)/input_1', '$(USER_WORKSPACE)/input_2'), - executable='my_script.py', - transfer_input_files=('../input_1', '../input_2'), - transfer_output_files=('example_output1', example_output2), - ) +:: - job_templates = (CondorJobTemplate(name='example', - job_description=my_job_description, - scheduler=my_scheduler, - ), - ) + from tethys_sdk.compute import list_schedulers + from .app import MyApp as app - return job_templates + def some_controller(request): -Creating and Customizing a Job -============================== -To create a job call the ``create_job`` method on the job manager. The required parameters are ``name``, ``user`` and ``template_name``. Any other job attributes can also be passed in as `kwargs`. + # get the path to the app workspace to reference job files + app_workspace = app.get_app_workspace().path -:: + # create a new job from the job manager + job = job_manager.create_job( + name='myjob_{id}', # required + user=request.user, # required + job_type='CONDOR', # required + + # any other properties can be passed in as kwargs + attributes=dict( + transfer_input_files=('../input_1', '../input_2'), + transfer_output_files=('example_output1', example_output2), + ), + condorpy_template_name='vanilla_transfer_files', + remote_input_files=( + os.path.join(app_workspace, 'my_script.py'), + os.path.join(app_workspace, 'input_1'), + os.path.join(app_workspace, 'input_2') + ) + ) + + # properties can also be added after the job is created + job.extended_properties = {'one': 1, 'two': 2} - # create a new job - job = job_manager.create_job(name='job_name', user=request.user, template_name='example', description='my first job') + # each job type may provided methods to further specify the job + job.set_attribute('executable', 'my_script.py') - # customize the job using methods provided by the job type - job.set_attribute('arguments', 'input_2') + # get a scheduler for the job + my_scheduler = list_schedulers()[0] + job.scheduler = my_scheduler - # save or execute the job - job.save() - # or - job.execute() + # save or execute the job + job.save() + # or + job.execute() Before a controller returns a response the job must be saved or else all of the changes made to the job will be lost (executing the job automatically saves it). If submitting the job takes a long time (e.g. if a large amount of data has to be uploaded to a remote scheduler) then it may be best to use AJAX to execute the job. API Documentation ================= -.. autoclass:: tethys_sdk.jobs.CondorJobTemplate +.. autoclass:: tethys_compute.models.CondorJob -.. autoclass:: tethys_compute.models.CondorJob \ No newline at end of file +.. autoclass:: tethys_sdk.jobs.CondorJobTemplate \ No newline at end of file diff --git a/docs/tethys_sdk/jobs/condor_workflow_type.rst b/docs/tethys_sdk/jobs/condor_workflow_type.rst index 806767974..d89a5d789 100644 --- a/docs/tethys_sdk/jobs/condor_workflow_type.rst +++ b/docs/tethys_sdk/jobs/condor_workflow_type.rst @@ -2,155 +2,113 @@ Condor Workflow Job Type ************************ -**Last Updated:** March 29, 2016 +**Last Updated:** December 27, 2018 A Condor Workflow provides a way to run a group of jobs (which can have hierarchical relationships) as a single (Tethys) job. The hierarchical relationships are defined as parent-child relationships. For example, suppose a workflow is defined with three jobs: ``JobA``, ``JobB``, and ``JobC``, which must be run in that order. These jobs would be defined with the following relationships: ``JobA`` is the parent of ``JobB``, and ``JobB`` is the parent of ``JobC``. .. seealso:: The Condor Workflow job type uses the CondorPy library to submit jobs to HTCondor compute pools. For more information on CondorPy and HTCondor see the `CondorPy documentation `_ and specifically the `Overview of HTCondor `_. -Setting up a CondorWorkflowTemplate -=================================== -Creating a `CondorWorkflowTemplate` involves 3 steps: +Creating a Condor Workflow +========================== +Creating a Condor Workflow job involves 3 steps: - 1. Define job descriptions for each of the sub-jobs using `CondorJobDescription` (see :doc:`condor_job_description`). - 2. Create the sub-jobs and define relationships using `CondorWorkflowJobTemplate`. - 3. Create the `CondorWorkflowTemplate`. - -.. note:: - The `CondorWorkflowJobTemplate` is similar to a `CondorJobTemplate` in that it represents a single HTCondor job and requires a `CondorJobDescription` to define the attributes of that job. However, unlike a `CondorJobTemplate` a `CondorWorkflowJobTemplate` cannot be run independently; it can only be part of a `CondorWorkflowTemplate`. Also, note that the `CondorWorkflowJobTemplate` has a `parents` parameter, which is used to define relationships between jobs. - -The following code sample demonstrates how to set up a `CondorWorkflowTemplate`: - -:: - - Example job_templates method with a CondorWorkflow type. - """ - - job_a_description = CondorJobDescription(condorpy_template_name='vanilla_transfer_files', - remote_input_files=('$(APP_WORKSPACE)/my_script.py', '$(APP_WORKSPACE)/input_1', '$(USER_WORKSPACE)/input_2'), - executable='my_script.py', - transfer_input_files=('../input_1', '../input_2'), - transfer_output_files=('example_output1', example_output2), - ) - job_b_description = CondorJobDescription(condorpy_template_name='vanilla_transfer_files', - remote_input_files=('$(APP_WORKSPACE)/my_script.py', '$(APP_WORKSPACE)/input_1', '$(USER_WORKSPACE)/input_2'), - executable='my_script.py', - transfer_input_files=('../input_1', '../input_2'), - transfer_output_files=('example_output1', example_output2), - ) - job_c_description = CondorJobDescription(condorpy_template_name='vanilla_transfer_files', - remote_input_files=('$(APP_WORKSPACE)/my_script.py', '$(APP_WORKSPACE)/input_1', '$(USER_WORKSPACE)/input_2'), - executable='my_script.py', - transfer_input_files=('../input_1', '../input_2'), - transfer_output_files=('example_output1', example_output2), - ) - job_a = CondorWorkflowJobTemplate(name='JobA', - job_description=job_a_description, - ) - job_b = CondorWorkflowJobTemplate(name='JobB', - job_description=job_b_description, - parents=[job_a] - ) - job_c = CondorWorkflowJobTemplate(name='JobC', - job_description=job_c_description, - parents=[job_b] - ) - job_templates = (CondorWorkflowTemplate(name='WorkflowABC', - job_list=[job_a, job_b, job_c], - scheduler=None, - ), - ) - -If the you want to use the same job both as part of a workflow and as a stand alone job then use the same job description in setting up the `CondorJobTemplate` and the `CondorWorkflowJobTemplate`. This process is demonstrated below: + 1. Create an empty Workflow job from the job manager. + 2. Create the jobs that will make up the workflow with `CondorWorkflowJobNode` + 3. Define the relationships among the nodes :: - from tethys_sdk.jobs import CondorJobTemplate, CondorWorkflowTemplate, CondorWorkflowJobTemplate, CondorJobDescription - from tethys_sdk.compute import list_schedulers - - def job_templates(cls): - """ - Example job_templates method with a CondorWorkflow type. - """ - - reusable_job_a_description = CondorJobDescription(condorpy_template_name='vanilla_transfer_files', - remote_input_files=('$(APP_WORKSPACE)/my_script.py', '$(APP_WORKSPACE)/input_1', '$(USER_WORKSPACE)/input_2'), - executable='my_script.py', - transfer_input_files=('../input_1', '../input_2'), - transfer_output_files=('example_output1', example_output2), - ) - job_b1_description = CondorJobDescription(condorpy_template_name='vanilla_transfer_files', - remote_input_files=('$(APP_WORKSPACE)/my_script.py', '$(APP_WORKSPACE)/input_1', '$(USER_WORKSPACE)/input_2'), - executable='my_script.py', - transfer_input_files=('../input_1', '../input_2'), - transfer_output_files=('example_output1', example_output2), - ) - job_b2_description = CondorJobDescription(condorpy_template_name='vanilla_transfer_files', - remote_input_files=('$(APP_WORKSPACE)/my_script.py', '$(APP_WORKSPACE)/input_1', '$(USER_WORKSPACE)/input_2'), - executable='my_script.py', - transfer_input_files=('../input_1', '../input_2'), - transfer_output_files=('example_output1', example_output2), - ) - job_c_description = CondorJobDescription(condorpy_template_name='vanilla_transfer_files', - remote_input_files=('$(APP_WORKSPACE)/my_script.py', '$(APP_WORKSPACE)/input_1', '$(USER_WORKSPACE)/input_2'), - executable='my_script.py', - transfer_input_files=('../input_1', '../input_2'), - transfer_output_files=('example_output1', example_output2), - ) - job_a = CondorWorkflowJobTemplate(name='JobA', - job_description=reusable_job_a_description, - ) - job_b1 = CondorWorkflowJobTemplate(name='JobB1', - job_description=reusable_job_a_description, - parents=[job_a] - ) - job_b2 = CondorWorkflowJobTemplate(name='JobB2', - job_description=reusable_job_a_description, - parents=[job_a] - ) - job_c = CondorWorkflowJobTemplate(name='JobC', - job_description=reusable_job_a_description, - parents=[job_b1, job_b2] - ) - job_templates = (CondorWorkflowTemplate(name='DiamondWorkflow', - job_list=[job_a, job_b1, job_b2, job_c], - scheduler=None, - ), - CondorJobTemplate(name='JobAStandAlone', - job_description=reusable_job_a_description, - scheduler=None, - ), - ) - - -Creating and Customizing a Job -============================== -To create a job call the ``create_job`` method on the job manager. The required parameters are ``name``, ``user`` and ``template_name``. Any other job attributes can also be passed in as `kwargs`. - -:: + from tethys_sdk.jobs import CondorWorkflowJobNode + from .app import MyApp as app + + def some_controller(request): + + # get the path to the app workspace to reference job files + app_workspace = app.get_app_workspace().path + + workflow = job_manager.create_job( + name='MyWorkflowABC', + user=request.user, + job_type='CONDORWORKFLOW', + scheduler=None, + ) + workflow.save() + + job_a = CondorWorkflowJobNode( + name='JobA', + workflow=workflow, + condorpy_template_name='vanilla_transfer_files', + remote_input_files=( + os.path.join(app_workspace, 'my_script.py'), + os.path.join(app_workspace, 'input_1'), + os.path.join(app_workspace, 'input_2') + ), + attributes=dict( + executable='my_script.py', + transfer_input_files=('../input_1', '../input_2'), + transfer_output_files=('example_output1', 'example_output2'), + ) + ) + job_a.save() + + job_b = CondorWorkflowJobNode( + name='JobB', + workflow=workflow, + condorpy_template_name='vanilla_transfer_files', + remote_input_files=( + os.path.join(app_workspace, 'my_script.py'), + os.path.join(app_workspace, 'input_1'), + os.path.join(app_workspace, 'input_2') + ), + attributes=dict( + executable='my_script.py', + transfer_input_files=('../input_1', '../input_2'), + transfer_output_files=('example_output1', 'example_output2'), + ), + ) + job_b.save() + + job_c = CondorWorkflowJobNode( + name='JobC', + workflow=workflow, + condorpy_template_name='vanilla_transfer_files', + remote_input_files=( + os.path.join(app_workspace, 'my_script.py'), + os.path.join(app_workspace, 'input_1'), + os.path.join(app_workspace, 'input_2') + ), + attributes=dict( + executable='my_script.py', + transfer_input_files=('../input_1', '../input_2'), + transfer_output_files=('example_output1', 'example_output2'), + ), + ) + job_c.save() + + job_b.add_parent(job_a) + job_c.add_parent(job_b) + + workflow.save() + # or + workflow.execute() - # create a new job - job = job_manager.create_job(name='job_name', user=request.user, template_name='example', description='my first job') - - # customize the job using methods provided by the job type - job.set_attribute('arguments', 'input_2') +.. note:: - # save or execute the job - job.save() - # or - job.execute() + The `CondorWorkflow` object must be saved before the `CondorWorkflowJobNode` objects can be instantiated, and the `CondorWorkflowJobNode` objects must be saved before you can define the relationships. Before a controller returns a response the job must be saved or else all of the changes made to the job will be lost (executing the job automatically saves it). If submitting the job takes a long time (e.g. if a large amount of data has to be uploaded to a remote scheduler) then it may be best to use AJAX to execute the job. API Documentation ================= -.. autoclass:: tethys_sdk.jobs.CondorWorkflowTemplate +.. autoclass:: tethys_compute.models.CondorWorkflow -.. autoclass:: tethys_sdk.jobs.CondorWorkflowJobTemplate +.. autoclass:: tethys_compute.models.CondorWorkflowNode -.. autoclass:: tethys_compute.models.CondorWorkflow +.. autoclass:: tethys_compute.models.CondorWorkflowJobNode + +.. autoclass:: tethys_sdk.jobs.CondorWorkflowTemplate -.. autoclass:: tethys_compute.models.CondorWorkflowJobNode \ No newline at end of file +.. autoclass:: tethys_sdk.jobs.CondorWorkflowJobTemplate \ No newline at end of file diff --git a/docs/tethys_sdk/tethys_cli.rst b/docs/tethys_sdk/tethys_cli.rst index 6d6b10b92..f2d902de5 100644 --- a/docs/tethys_sdk/tethys_cli.rst +++ b/docs/tethys_sdk/tethys_cli.rst @@ -78,6 +78,8 @@ Aids the installation of Tethys by automating the creation of supporting files. $ tethys gen apache $ tethys gen apache -d /path/to/destination +.. _tethys_manage_cmd: + manage [options] ----------------------------- @@ -85,14 +87,15 @@ This command contains several subcommands that are used to help manage Tethys Pl **Arguments:** -* **subcommand**: The management command to run. Either "start", "syncdb", or "collectstatic". +* **subcommand**: The management command to run. - * *start*: Starts the Django development server. Wrapper for ``manage.py runserver``. + * *start*: Start the Django development server. Wrapper for ``manage.py runserver``. * *syncdb*: Initialize the database during installation. Wrapper for ``manage.py syncdb``. - * *collectstatic*: Link app static/public directories to STATIC_ROOT directory and then run Django's collectstatic command. Preprocessor and wrapper for ``manage.py collectstatic``. + * *sync*: Sync installed apps and extensions with the TethysApp database. + * *collectstatic*: Link app and extension static/public directories to STATIC_ROOT directory and then run Django's collectstatic command. Preprocessor and wrapper for ``manage.py collectstatic``. * *collectworkspaces*: Link app workspace directories to TETHYS_WORKSPACES_ROOT directory. * *collectall*: Convenience command for running both *collectstatic* and *collectworkspaces*. - * *superuser*: Create a new superuser/website admin for your Tethys Portal. + * *createsuperuser*: Create a new superuser/website admin for your Tethys Portal. **Optional Arguments:** @@ -110,6 +113,9 @@ This command contains several subcommands that are used to help manage Tethys Pl # Sync the database $ tethys manage syncdb + # Sync installed apps with the TethysApp database. + $ tethys manage sync + # Collect static files $ tethys manage collectstatic @@ -168,7 +174,7 @@ Management command for Persistent Stores. To learn more about persistent stores list ---- -Use this command to list all installed apps. +Use this command to list all installed apps and extensions. **Examples:** @@ -179,11 +185,14 @@ Use this command to list all installed apps. uninstall --------------- -Use this command to uninstall apps. +Use this command to uninstall apps and extensions. **Arguments:** -* **app**: Name the app to uninstall. +* **name**: Name the app or extension to uninstall. + +**Optional Arguments:** +* **-e, --extension**: Flag used to indicate that the item being uninstalled is an extension. **Examples:** @@ -192,6 +201,9 @@ Use this command to uninstall apps. # Uninstall my_first_app $ tethys uninstall my_first_app + # Uninstall extension + $ tethys uninstall -e my_extension + .. _tethys_cli_docker: docker [options] @@ -285,3 +297,153 @@ Management commands for running tests for Tethys Platform and Tethys Apps. See : # Run tests for a single app tethys test -f tethys_apps.tethysapp.my_first_app + + +.. _tethys_cli_app_settings: + +app_settings +----------------------- + +This command is used to list the Persistent Store and Spatial Dataset Settings that an app has requested. + +**Arguments:** + +* **app_name**: Name of app for which Settings will be listed + +**Optional Arguments:** + +* **-p --persistent**: A flag indicating that only Persistent Store Settings should be listed +* **-s --spatial**: A flag indicating that only Spatial Dataset Settings should be listed + +**Examples:** + +:: + + $ tethys app_settings my_first_app + +.. _tethys_cli_services: + +services [ | options] +------------------------------------------------- + +This command is used to interact with Tethys Services from the command line, rather than the App Admin interface. + +**Arguments:** + +* **subcommand**: The services command to run. One of the following: + + * *list*: List all existing Tethys Services (Persistent Store and Spatial Dataset Services) + * *create*: Create a new Tethys Service + * **subcommand**: The service type to create + * *persistent*: Create a new Persistent Store Service + **Arguments:** + + * **-n, --name**: A unique name to identify the service being created + * **-c, --connection**: The connection endpoint associated with this service, in the form ":@:" + * *spatial*: Create a new Spatial Dataset Service + **Arguments:** + + * **-n, --name**: A unique name to identify the service being created + * **-c, --connection**: The connection endpoint associated with this service, in the form ":@//:" + + **Optional Arguments:** + + * **-p, --public-endpoint**: The public-facing endpoint of the Service, if different than what was provided with the "--connection" argument, in the form "//:". + * **-k, --apikey**: The API key, if any, required to establish a connection. + * *remove*: Remove a Tethys Service + * **subcommand**: The service type to remove + * *persistent*: Remove a Persistent Store Service + **Arguments:** + * **service_uid**: A unique identifier of the Service to be removed, which can either be the database ID, or the service name + * *spatial*: Remove a Spatial Dataset Service + **Arguments:** + * **service_uid**: A unique identifier of the Service to be removed, which can either be the database ID, or the service name + +**Examples:** + +:: + + # List all Tethys Services + $ tethys services list + + # List only Spatial Dataset Tethys Services + $ tethys services list -s + + # List only Persistent Store Tethys Services + $ tethys services list -p + + # Create a new Spatial Dataset Tethys Service + + $ tethys services create spatial -n my_spatial_service -c my_username:my_password@http://127.0.0.1:8081 -p https://mypublicdomain.com -k mysecretapikey + + # Create a new Persistent Store Tethys Service + $ tethys services create persistent -n my_persistent_service -c my_username:my_password@http://127.0.0.1:8081 + + # Remove a Spatial Dataset Tethys Service + $ tethys services remove my_spatial_service + + # Remove a Persistent Store Tethys Service + $ tethys services remove my_persistent_service + +.. _tethys_cli_link: + +link +-------------------------------------------------- + +This command is used to link a Tethys Service with a TethysApp Setting + +**Arguments:** + +* **service_identifier**: An identifier of the Tethys Service being linked, of the form ":", where can be either "spatial" or "persistent", and must be either the database ID or name of the Tethys Service. +* **app_setting_identifier**: An identifier of the TethysApp Setting being linked, of the form "::", where must be one of "ds_spatial," "ps_connection", or "ps_database" and can be either the database ID or name of the TethysApp Setting. + +**Examples:** + +:: + + # Link a Persistent Store Service to a Persistent Store Connection Setting + $ tethys link persistent:my_persistent_service my_first_app:ps_connection:my_ps_connection + + # Link a Persistent Store Service to a Persistent Store Database Setting + $ tethys link persistent:my_persistent_service my_first_app:ps_database:my_ps_connection + + # Link a Spatial Dataset Service to a Spatial Dataset Service Setting + $ tethys link spatial:my_spatial_service my_first_app:ds_spatial:my_spatial_connection + +.. _tethys_cli_schedulers: + +schedulers +----------------------- + +This command is used to interact with Schedulers from the command line, rather than through the App Admin interface + +**Arguments:** + +* **subcommand**: The schedulers command to run. One of the following: + + * *list*: List all existing Schedulers + * *create*: Create a new Scheduler + **Arguments:** + * **-n, --name**: A unique name to identify the Scheduler being created + * **-d, --endpoint**: The endpoint of the remote host the Scheduler will connect with in the form //" + * **-u, --username**: The username that will be used to connect to the remote endpoint" + **Optional Arguments:** + * **-p, --password**: The password associated with the username (required if "-f (--private-key-path)" not specified. + * **-f, --private-key-path**: The path to the private ssh key file (required if "-p (--password)" not specified. + * **-k, --private-key-pass**: The password to the private ssh key file (only meaningful if "-f (--private-key-path)" is specified. + * *remove*: Remove a Scheduler + **Arguments:** + * **scheduler_name**: The unique name of the Scheduler being removed. + +**Examples:** + +:: + + # List all Schedulers + $ tethys schedulers list + + # Create a new scheduler + $ tethys schedulers create -n my_scheduler -e http://127.0.0.1 -u my_username -p my_password + + # Remove a scheduler + $ tethys schedulers remove my_scheduler diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 4f0ebc1fd..5cf580868 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -2,153 +2,122 @@ What's New ********** -**Last Updated:** May 2017 +**Last Updated:** December 2018 Refer to this article for information about each new release of Tethys Platform. Release |version| ================= -Powered by Miniconda Environment --------------------------------- +Python 3 Support +---------------- -* Tethys Platform is now installed in a Miniconda environment. -* Using the Miniconda includes Conda, an open source Python package management system -* Conda can be used to install Python dependencies as well as system dependencies -* Installing packages like GDAL or NetCDF4 are as easy as ``conda install gdal`` -* Conda is cross platform: it works on Windows, Linux, and MacOS +* Python 3 officially supported in Tethys Platform. +* Python 2 support officially deprecated and will be dropped when Tethys Platform 3.0 is released. +* Tethys needs to migrate to Python 3 only so we can upgrade to Django 2.0, which only supports Python 3. -See: `Miniconda `_ and `Conda `_ +.. important:: -Cross Platform Support ----------------------- + Migrate your apps to Python 3. After Tethys Platform 3.0 is released, Python 2 will no longer be supported by Tethys Platform. -* Develop natively on Windows, Mac, or Linux! -* No more virtual machines. -* Be careful with your paths. -See: :doc:`installation` +100% Unit Test Coverage +----------------------- -Installation Scripts --------------------- - -* Completely automated installation of Tethys -* Scripts provided for Mac, Linux, and Windows. +* Tests pass in Python 2 and Python 3. +* Unit tests cover 100% of testable code. +* Code base linted using flake8 to enforce PEP-8 and other Python coding best practices. +* Automated test execution on Travis-CI and Stickler-CI whenever a Pull Request is submitted. +* Added badges to the README to display build/testing, coverage, and docs status on github repository. +* All of this will lead to increased stability in this and future releases. -See: :doc:`installation` +See: `Tethys Platform Repo `_ for build and coverage information. -Python 3 --------- +Tethys Extensions +----------------- -* Experimental Python 3 Support in 2.0.0 -* Tethys Dataset Services is not completely Python 3 compatible -* Use ``--python-version 3`` option on the installation script -* Python 2 support will be dropped in version 2.1 +* Customize Tethys Platform functionality. +* Create your own gizmos. +* Centralize app logic that is common to multiple apps in an extension. -See: :doc:`installation` +See: :doc:`./tethys_sdk/extensions` -Templating API +Map View Gizmo -------------- -* Leaner, updated theme for app base template. -* New ``header_buttons`` block for adding custom buttons to app header. - -See: :doc:`tethys_sdk/templating` - -App Settings ------------- - -* Developers can create App Settings, which are configured in the admin interface of the Tethys Portal. -* Types of settings that can be created include Custom Settings, Persistent Store Settings, Dataset Service Settings, Spatial Dataset Service Settings, and Web Processing Service Settings. -* The way Tethys Services are allocated to apps is now done through App Settings. -* All apps using the Persistent Stores APIs, Dataset Services APIs, or Web Processing Services APIs prior to version 2.0.0 will need to be refactored to use the new App settings approach. - -See: :doc:`./tethys_sdk/app_settings` - -Commandline Interface ---------------------- - -* Added ``tethys list`` command that lists installed apps. -* Completely overhauled scaffold command that works cross-platform. -* New options for scaffold command that allow automatically accepting the defaults and overwriting project if it already exists. - -See: :ref:`tethys_list_cmd` and :ref:`tethys_scaffold_cmd` - -Tutorials ---------- - -* Brand new Getting Started Tutorial -* Demonstration of most Tethys SDK APIs - -See: :doc:`./tutorials/getting_started` - -Gizmos ------- - -* New way to call them -* New load dependencies Method -* Updated select_gizmo to allow Select2 options to be passed in. - -See: :doc:`tethys_sdk/gizmos` - -Map View --------- - -* Updated OpenLayers libraries to version 4.0 -* Fixes to make MapView compatible with Internet Explorer -* Can configure styling of MVDraw overlay layer -* New editable attribute for MVLayers to lock layers from being edited -* Added data attribute to MVLayer to allow passing custom attributes with layers for use in custom JavaScript -* A basemap switcher tool is now enabled on the map with the capability to configure multiple basemaps, including turning the basemap off. -* Added the ability to customize some styles of vector MVLayers. +* Added support for many more basemaps. +* Added Esri, Stamen, CartoDB. +* Support for custom XYZ services as basemaps. +* User can set OpenLayers version. +* Uses jsdelivr to load custom versions (see: ``_) +* Default OpenLayers version updated to 5.3.0. See: :doc:`tethys_sdk/gizmos/map_view` -Esri Map View -------------- +Class-based Controllers +----------------------- -* New map Gizmo that uses ArcGIS for JavaScript API. +* Added ``TethysController`` to SDK to support class-based views in Tethys apps. +* Inherits from django ``View`` class. +* Includes ``as_controller`` method, which is a thin wrapper around ``as_view`` method to better match Tethys terminology. +* UrlMaps can take class-based Views as the controller argument: ``MyClassBasedController.as_controller(...)`` +* More to come in the future. -See: :doc:`tethys_sdk/gizmos/esri_map` +See: `Django Class-based views `_ to get started. -Plotly View and Bokeh View Gizmos ---------------------------------- +Partial Install Options +----------------------- -* True open source options for plotting in Tethys +* The Tethys Platform installation scripts now allow for partial installation. +* Install in existing Conda environment or against existing database. +* Upgrade using the install script! +* Linux and Mac only. -See: :doc:`tethys_sdk/gizmos/bokeh_view` and :doc:`tethys_sdk/gizmos/plotly_view` +See: :doc:`./installation/linux_and_mac` and :doc:`./installation/update` -DataTable View Gizmos +Commandline Interface --------------------- -* Interactive table gizmo based on Data Tables. - -See: :doc:`tethys_sdk/gizmos/datatable_view` - -Security --------- +* New commands to manage app settings and services. +* ``tethys app_settings`` - List settings for an app. +* ``tethys services`` - List, create, and remove Tethys services (only supports persistent store services and spatial dataset services for now). +* ``tethys link`` - Link/Assign a Tethys service to a corresponding app setting. +* ``tethys schedulers`` - List, create, and remove job Schedulers. +* ``tethys manage sync`` - Sync app and extensions with Tethys database without a full Tethys start. -* Sessions will now timeout and log user out after period of inactivity. -* When user closes browser, they are automatically logged out now. -* Expiration times can be configured in settings. +See: :ref:`tethys_cli_app_settings`, :ref:`tethys_cli_services`, :ref:`tethys_cli_link`, :ref:`tethys_cli_schedulers`, and :ref:`tethys_manage_cmd` -See: :doc:`installation/platform_settings` +Dockerfile +---------- -HydroShare OAuth Backend and Helper Function --------------------------------------------- +* New Dockerfile for Tethys Platform. +* Use it to build Docker images. +* Use it as a base for your own Docker images that have your apps installed. +* Includes supporting salt files. +* Dockerfile has been optimized to minimize the size of the produced image. +* Threading is enabled in the Docker container. -* Refactor default HydroShare OAuth backend; Token refresh is available; Add backends for HydroShare-beta and HydroShare-playground. -* Include hs_restclient library in requirements.txt; Provide a helper function to help initialize the ``hs`` object based on HydroShare social account. -* Update python-social-auth to 0.2.21. - -See: :doc:`tethys_portal/social_auth` +See: `Docker Documentation `_ to learn how to use Docker in your workflows. +API Tokens for Users +-------------------- +* API tokens are automatically generated for users when they are created. +* Use User API tokens to access protected REST API views. +Documentation +------------- +* Added SSL setup instruction to Production Installation (see: :ref:`production_installation_ssl`) Bugs ---- -* Fixed issue where ``tethys uninstall `` command was not uninstalling fully. +* Fixed grammar in forget password link. +* Refactored various methods and decorators to use new way of using Django methods ``is_authenticated`` and ``is_anonymous``. +* Fixed bug with Gizmos that was preventing errors from being displayed when in debug mode. +* Fixed various bugs with uninstalling apps and extensions. +* Fixed bugs with get_persistent_store_setting methods. +* Fixed a naming conflict in the SelectInput gizmo. +* Fixed numerous bugs identified by new tests. Prior Release Notes =================== diff --git a/docs/whats_new/prior_releases.rst b/docs/whats_new/prior_releases.rst index e1ab0c4e2..011e9897b 100644 --- a/docs/whats_new/prior_releases.rst +++ b/docs/whats_new/prior_releases.rst @@ -2,10 +2,155 @@ Prior Release Notes ******************* -**Last Updated:** December 10, 2016 +**Last Updated:** December 2017 Information about prior releases is shown here. +Release 2.0.0 +============= + +Powered by Miniconda Environment +-------------------------------- + +* Tethys Platform is now installed in a Miniconda environment. +* Using the Miniconda includes Conda, an open source Python package management system +* Conda can be used to install Python dependencies as well as system dependencies +* Installing packages like GDAL or NetCDF4 are as easy as ``conda install gdal`` +* Conda is cross platform: it works on Windows, Linux, and MacOS + +See: `Miniconda `_ and `Conda `_ + +Cross Platform Support +---------------------- + +* Develop natively on Windows, Mac, or Linux! +* No more virtual machines. +* Be careful with your paths. + +See: :doc:`../installation` + +Installation Scripts +-------------------- + +* Completely automated installation of Tethys +* Scripts provided for Mac, Linux, and Windows. + +See: :doc:`../installation` + +Python 3 +-------- + +* Experimental Python 3 Support in 2.0.0 +* Tethys Dataset Services is not completely Python 3 compatible +* Use ``--python-version 3`` option on the installation script +* Python 2 support will be dropped in version 2.1 + +See: :doc:`../installation` + +Templating API +-------------- + +* Leaner, updated theme for app base template. +* New ``header_buttons`` block for adding custom buttons to app header. + +See: :doc:`../tethys_sdk/templating` + +App Settings +------------ + +* Developers can create App Settings, which are configured in the admin interface of the Tethys Portal. +* Types of settings that can be created include Custom Settings, Persistent Store Settings, Dataset Service Settings, Spatial Dataset Service Settings, and Web Processing Service Settings. +* The way Tethys Services are allocated to apps is now done through App Settings. +* All apps using the Persistent Stores APIs, Dataset Services APIs, or Web Processing Services APIs prior to version 2.0.0 will need to be refactored to use the new App settings approach. + +See: :doc:`../tethys_sdk/app_settings` + +Commandline Interface +--------------------- + +* Added ``tethys list`` command that lists installed apps. +* Completely overhauled scaffold command that works cross-platform. +* New options for scaffold command that allow automatically accepting the defaults and overwriting project if it already exists. + +See: :ref:`tethys_list_cmd` and :ref:`tethys_scaffold_cmd` + +Tutorials +--------- + +* Brand new Getting Started Tutorial +* Demonstration of most Tethys SDK APIs + +See: :doc:`../tutorials/getting_started` + +Gizmos +------ + +* New way to call them +* New load dependencies Method +* Updated select_gizmo to allow Select2 options to be passed in. + +See: :doc:`../tethys_sdk/gizmos` + +Map View +-------- + +* Updated OpenLayers libraries to version 4.0 +* Fixes to make MapView compatible with Internet Explorer +* Can configure styling of MVDraw overlay layer +* New editable attribute for MVLayers to lock layers from being edited +* Added data attribute to MVLayer to allow passing custom attributes with layers for use in custom JavaScript +* A basemap switcher tool is now enabled on the map with the capability to configure multiple basemaps, including turning the basemap off. +* Added the ability to customize some styles of vector MVLayers. + +See: :doc:`../tethys_sdk/gizmos/map_view` + +Esri Map View +------------- + +* New map Gizmo that uses ArcGIS for JavaScript API. + +See: :doc:`../tethys_sdk/gizmos/esri_map` + +Plotly View and Bokeh View Gizmos +--------------------------------- + +* True open source options for plotting in Tethys + +See: :doc:`../tethys_sdk/gizmos/bokeh_view` and :doc:`../tethys_sdk/gizmos/plotly_view` + +DataTable View Gizmos +--------------------- + +* Interactive table gizmo based on Data Tables. + +See: :doc:`../tethys_sdk/gizmos/datatable_view` + +Security +-------- + +* Sessions will now timeout and log user out after period of inactivity. +* When user closes browser, they are automatically logged out now. +* Expiration times can be configured in settings. + +See: :doc:`../installation/platform_settings` + +HydroShare OAuth Backend and Helper Function +-------------------------------------------- + +* Refactor default HydroShare OAuth backend; Token refresh is available; Add backends for HydroShare-beta and HydroShare-playground. +* Include hs_restclient library in requirements.txt; Provide a helper function to help initialize the ``hs`` object based on HydroShare social account. +* Update python-social-auth to 0.2.21. + +See: :doc:`../tethys_portal/social_auth` + + + +Bugs +---- + +* Fixed issue where ``tethys uninstall `` command was not uninstalling fully. + + Release 1.4.0 ============= diff --git a/environment_py2.yml b/environment_py2.yml index fc4f63eb5..7156c7c5a 100644 --- a/environment_py2.yml +++ b/environment_py2.yml @@ -6,6 +6,7 @@ name: tethys channels: +- tethysplatform - conda-forge - defaults @@ -16,6 +17,7 @@ dependencies: - djangorestframework=3.6* - bokeh=0.12* - future=0.16* +- hs_restclient - paste=2.0* - psycopg2=2.7* - sqlalchemy=1.1* @@ -26,11 +28,15 @@ dependencies: - geoalchemy2=0.4* - owslib=0.14* - pip=9.0* -- pillow=4.1* -- plotly=1.12* -- postgresql=9.5* +- pillow +- plotly +- postgresql +- postgis - pycrypto=2.6* - pyopenssl=16.2* +- mock +- factory_boy +- tethys_dataset_services>=1.7.0 - pip: - django-gravatar2>=1.4.0,<1.5.0 - django-bootstrap3>=8.2.0,<8.3.0 @@ -39,6 +45,7 @@ dependencies: - django-simple-captcha>=0.5.0,<0.6.0 - django-termsandconditions>=1.1.0,<1.2.0 - django-session-security>=2.5.0,<2.6.0 - - tethys_dataset_services>=1.6.0,<1.7.0 - condorpy>=0.3.0,<0.4.0 - social-auth-app-django>=1.2.0,<1.3.0 + - python_social_auth + - requests-mock diff --git a/environment_py3.yml b/environment_py3.yml index be54e0f49..319851d7d 100644 --- a/environment_py3.yml +++ b/environment_py3.yml @@ -6,6 +6,7 @@ name: tethys channels: +- tethysplatform - conda-forge - defaults @@ -16,6 +17,7 @@ dependencies: - djangorestframework - bokeh - future +- hs_restclient - psycopg2 - sqlalchemy - requests @@ -26,10 +28,14 @@ dependencies: - owslib - pip - pillow -- plotly=1.12* +- plotly - postgresql +- postgis - pycrypto - pyopenssl +- mock +- factory_boy +- tethys_dataset_services>=1.7.0 - pip: - django-gravatar2 - django-bootstrap3 @@ -42,3 +48,5 @@ dependencies: - condorpy - PasteScript - social-auth-app-django + - python_social_auth + - requests-mock diff --git a/readthedocs.yml b/readthedocs.yml index 979fd7aab..723d8585b 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -1,7 +1,5 @@ conda: - file: environment_py2.yml + file: docs/docs_environment.yml python: - version: 2 - pip_install: true - extra_requirements: - - docs \ No newline at end of file + version: 3 + pip_install: true \ No newline at end of file diff --git a/scripts/install_tethys.bat b/scripts/install_tethys.bat index b488af7ed..3748b788b 100644 --- a/scripts/install_tethys.bat +++ b/scripts/install_tethys.bat @@ -6,6 +6,7 @@ IF %ERRORLEVEL% NEQ 0 (SET ERRORLEVEL=0) :: Set defaults SET ALLOWED_HOST=127.0.0.1 SET TETHYS_HOME=C:%HOMEPATH%\tethys +SET TETHYS_SRC=%TETHYS_HOME%\src SET TETHYS_PORT=8000 SET TETHYS_DB_USERNAME=tethys_default SET TETHYS_DB_PASSWORD=pass @@ -13,7 +14,7 @@ SET TETHYS_DB_PORT=5436 SET CONDA_HOME= SET CONDA_EXE=Miniconda3-latest-Windows-x86_64.exe SET CONDA_ENV_NAME=tethys -SET PYTHON_VERSION=2 +SET PYTHON_VERSION=3 SET BRANCH=release SET TETHYS_SUPER_USER=admin @@ -34,6 +35,16 @@ IF NOT "%1"=="" ( SHIFT SET OPTION_RECOGNIZED=TRUE ) + IF "%1"=="-s" ( + SET TETHYS_SRC=%2 + SHIFT + SET OPTION_RECOGNIZED=TRUE + ) + IF "%1"=="--tethys-src" ( + SET TETHYS_SRC=%2 + SHIFT + SET OPTION_RECOGNIZED=TRUE + ) IF "%1"=="-a" ( SET ALLOWED_HOST=%2 SHIFT @@ -242,8 +253,8 @@ IF %ERRORLEVEL% NEQ 0 ( :: clone Tethys repo ECHO Cloning the Tethys Platform repo... conda install --yes git -git clone https://github.com/tethysplatform/tethys "!TETHYS_HOME!\src" -CD "!TETHYS_HOME!\src" +git clone https://github.com/tethysplatform/tethys "!TETHYS_SRC!" +CD "!TETHYS_SRC!" git checkout !BRANCH! IF %ERRORLEVEL% NEQ 0 ( @@ -343,14 +354,15 @@ EXIT /B %ERRORLEVEL% ECHO USAGE: install_tethys.bat [options] ECHO. ECHO OPTIONS: -ECHO -t, --tethys-home [PATH] Path for tethys home directory. Default is 'C:\%HOMEPATH%\tehtys'. +ECHO -t, --tethys-home [PATH] Path for tethys home directory. Default is 'C:\%HOMEPATH%\tethys'. +ECHO -s, --tethys-src [PATH] Path for tethys source directory. Default is %%TETHYS_HOME%%\src. ECHO -a, --allowed-host [HOST] Hostname or IP address on which to serve tethys. Default is 127.0.0.1. ECHO -p, --port [PORT] Port on which to serve tethys. Default is 8000. ECHO -b, --branch [BRANCH_NAME] Branch to checkout from version control. Default is 'release'. ECHO -c, --conda-home [PATH] Path to conda home directory where Miniconda will be installed. Default is %%TETHYS_HOME%%\miniconda. ECHO -C, --conda-exe [PATH] Path to Miniconda installer executable. Default is '.\Miniconda3-latest-Windows-x86_64.exe'. ECHO -n, --conda-env-name [NAME] Name for tethys conda environment. Default is 'tethys'. -ECHO --python-version [PYTHON_VERSION] Main python version to install tethys environment into (2 or 3). Default is 2. +ECHO --python-version [PYTHON_VERSION] Main python version to install tethys environment into (2-deprecated or 3). Default is 3. ECHO --db-username [USERNAME] Username that the tethys database server will use. Default is 'tethys_default'. ECHO --db-password [PASSWORD] Password that the tethys database server will use. Default is 'pass'. ECHO --db-port [PORT] Port that the tethys database server will use. Default is 5436. diff --git a/scripts/install_tethys.sh b/scripts/install_tethys.sh index 567c6787a..2d6d1cad1 100644 --- a/scripts/install_tethys.sh +++ b/scripts/install_tethys.sh @@ -1,28 +1,53 @@ #!/bin/bash +RED=`tput setaf 1` +GREEN=`tput setaf 2` +YELLOW=`tput setaf 3` +RESET_COLOR=`tput sgr0` + USAGE="USAGE: . install_tethys.sh [options]\n \n OPTIONS:\n - -t, --tethys-home Path for tethys home directory. Default is ~/tethys.\n - -a, --allowed-host Hostname or IP address on which to serve tethys. Default is 127.0.0.1.\n - -p, --port Port on which to serve tethys. Default is 8000.\n - -b, --branch Branch to checkout from version control. Default is 'release'.\n - -c, --conda-home Path where Miniconda will be installed, or to an existing installation of Miniconda. Default is \${TETHYS_HOME}/miniconda.\n - -n, --conda-env-name Name for tethys conda environment. Default is 'tethys'. - --python-version Main python version to install tethys environment into (2 or 3). Default is 2.\n - --db-username Username that the tethys database server will use. Default is 'tethys_default'.\n - --db-password Password that the tethys database server will use. Default is 'pass'.\n - --db-port Port that the tethys database server will use. Default is 5436.\n - -S, --superuser Tethys super user name. Default is 'admin'.\n - -E, --superuser-email Tethys super user email. Default is ''.\n - -P, --superuser-pass Tethys super user password. Default is 'pass'.\n - --skip-tethys-install Flag to skip the Tethys installation so that the Docker installation or production installation can be added to an existing Tethys installation.\n - --install-docker Flag to include Docker installation as part of the install script (Linux only).\n - --docker-options Command line options to pass to the 'tethys docker init' call if --install-docker is used. Default is \"'-d'\".\n - --production Flag to install Tethys in a production configuration.\n - --configure-selinux Flag to perform configuration of SELinux for production installation. (Linux only).\n - -x Flag to turn on shell command echoing.\n - -h, --help Print this help information.\n +\t -t, --tethys-home \t\t Path for tethys home directory. Default is ~/tethys.\n +\t -s, --tethys-src \t\t Path for tethys source directory. Default is ~/tethys/src.\n +\t -a, --allowed-host \t\t Hostname or IP address on which to serve tethys. Default is 127.0.0.1.\n +\t -p, --port \t\t\t Port on which to serve tethys. Default is 8000.\n +\t -b, --branch \t\t Branch to checkout from version control. Default is 'release'.\n +\t -c, --conda-home \t\t Path where Miniconda will be installed, or to an existing installation of Miniconda. Default is \${TETHYS_HOME}/miniconda.\n +\t -n, --conda-env-name \t\t Name for tethys conda environment. Default is 'tethys'.\n +\t --python-version \t Main python version to install tethys environment into (2-deprecated or 3). Default is 3.\n +\t --db-username \t\t Username that the tethys database server will use. Default is 'tethys_default'.\n +\t --db-password \t\t Password that the tethys database server will use. Default is 'pass'.\n +\t --db-super-username \t Username for super user on the tethys database server. Default is 'tethys_super'.\n +\t --db-super-password \t Password for super user on the tethys database server. Default is 'pass'.\n +\t --db-port \t\t\t Port that the tethys database server will use. Default is 5436.\n +\t --db-dir \t\t\t Path where the local PostgreSQL database will be created. Default is \${TETHYS_HOME}/psql.\n +\t -S, --superuser \t\t Tethys super user name. Default is 'admin'.\n +\t -E, --superuser-email \t\t Tethys super user email. Default is ''.\n +\t -P, --superuser-pass \t Tethys super user password. Default is 'pass'.\n +\t --skip-tethys-install \t\t\t Flag to skip the Tethys installation so that the Docker installation or production installation can be added to an existing Tethys installation.\n +\t --partial-tethys-install \t List of flags to indicate which steps of the installation to do (e.g. --partial-tethys-install mresdat).\n\n + +\t \t FLAGS:\n +\t \t\t m - Install Miniconda\n +\t \t\t r - Clone Tethys repository\n +\t \t\t c - Checkout the branch specified by the option '--branch' (specifying the flag 'r' will also trigger this flag)\n +\t \t\t e - Create Conda environment\n +\t \t\t s - Create 'settings.py' file\n +\t \t\t d - Create a local database server\n +\t \t\t i - Initialize database server with the Tethys database (specifying the flag 'd' will also trigger this flag)\n +\t \t\t u - Add a Tethys Portal Super User to the user database (specifying the flag 'd' will also trigger this flag)\n +\t \t\t a - Create activation/deactivation scripts for the Tethys Conda environment\n +\t \t\t t - Create the 't' alias t\n\n + +\t \t NOTE: if --skip-tethys-install is used then this option will be ignored.\n\n + +\t --install-docker \t\t\t Flag to include Docker installation as part of the install script (Linux only).\n +\t --docker-options \t\t Command line options to pass to the 'tethys docker init' call if --install-docker is used. Default is \"'-d'\".\n +\t --production \t\t\t\t Flag to install Tethys in a production configuration.\n +\t --configure-selinux \t\t\t Flag to perform configuration of SELinux for production installation. (Linux only).\n +\t -x \t\t\t\t\t Flag to turn on shell command echoing.\n +\t -h, --help \t\t\t\t Print this help information.\n " print_usage () @@ -65,9 +90,11 @@ TETHYS_HOME=~/tethys TETHYS_PORT=8000 TETHYS_DB_USERNAME='tethys_default' TETHYS_DB_PASSWORD='pass' +TETHYS_DB_SUPER_USERNAME='tethys_super' +TETHYS_DB_SUPER_PASSWORD='pass' TETHYS_DB_PORT=5436 CONDA_ENV_NAME='tethys' -PYTHON_VERSION='2' +PYTHON_VERSION='3' BRANCH='release' TETHYS_SUPER_USER='admin' @@ -76,6 +103,17 @@ TETHYS_SUPER_USER_PASS='pass' DOCKER_OPTIONS='-d' +INSTALL_MINICONDA="true" +CLONE_REPO="true" +CHECKOUT_BRANCH="true" +CREATE_ENV="true" +CREATE_SETTINGS="true" +SETUP_DB="true" +INITIALIZE_DB="true" +CREATE_TETHYS_SUPER_USER="true" +CREATE_ENV_SCRIPTS="true" +CREATE_SHORTCUTS="true" + # parse command line options set_option_value () { @@ -96,6 +134,10 @@ case $key in set_option_value TETHYS_HOME "$2" shift # past argument ;; + -s|--tethys-src) + set_option_value TETHYS_SRC "$2" + shift # past argument + ;; -a|--allowed-host) set_option_value ALLOWED_HOST "$2" shift # past argument @@ -125,13 +167,25 @@ case $key in shift # past argument ;; --db-password) - set_option_value TETHYS_DB_PASS "$2" + set_option_value TETHYS_DB_PASSWORD "$2" + shift # past argument + ;; + --db-super-username) + set_option_value TETHYS_DB_SUPER_USERNAME "$2" + shift # past argument + ;; + --db-super-password) + set_option_value TETHYS_DB_SUPER_PASSWORD "$2" shift # past argument ;; --db-port) set_option_value TETHYS_DB_PORT "$2" shift # past argument ;; + --db-dir) + set_option_value TETHYS_DB_DIR "$2" + shift # past argument + ;; -S|--superuser) set_option_value TETHYS_SUPER_USER "$2" shift # past argument @@ -147,6 +201,51 @@ case $key in --skip-tethys-install) SKIP_TETHYS_INSTALL="true" ;; + --partial-tethys-install) + # Set all steps to false be default and then activate only those steps that have been specified. + INSTALL_MINICONDA= + CLONE_REPO= + CHECKOUT_BRANCH= + CREATE_ENV= + CREATE_SETTINGS= + SETUP_DB= + INITIALIZE_DB= + CREATE_TETHYS_SUPER_USER= + CREATE_ENV_SCRIPTS= + CREATE_SHORTCUTS= + + if [[ "$2" = *"m"* ]]; then + INSTALL_MINICONDA="true" + fi + if [[ "$2" = *"r"* ]]; then + CLONE_REPO="true" + fi + if [[ "$2" = *"c"* ]]; then + CHECKOUT_BRANCH="true" + fi + if [[ "$2" = *"e"* ]]; then + CREATE_ENV="true" + fi + if [[ "$2" = *"s"* ]]; then + CREATE_SETTINGS="true" + fi + if [[ "$2" = *"d"* ]]; then + SETUP_DB="true" + fi + if [[ "$2" = *"i"* ]]; then + INITIALIZE_DB="true" + fi + if [[ "$2" = *"u"* ]]; then + CREATE_TETHYS_SUPER_USER="true" + fi + if [[ "$2" = *"a"* ]]; then + CREATE_ENV_SCRIPTS="true" + fi + if [[ "$2" = *"t"* ]]; then + CREATE_SHORTCUTS="true" + fi + shift # past argument + ;; --install-docker) if [ "$(uname)" = "Linux" ] then @@ -182,7 +281,8 @@ case $key in print_usage ;; *) # unknown option - echo Ignoring unrecognized option: $key + echo Unrecognized option: $key + print_usage ;; esac shift # past argument or value @@ -192,14 +292,28 @@ done resolve_relative_path TETHYS_HOME ${TETHYS_HOME} # set CONDA_HOME relative to TETHYS_HOME if not already set -if [ -z ${CONDA_HOME} ] +if [ -z "${CONDA_HOME}" ] then CONDA_HOME="${TETHYS_HOME}/miniconda" else resolve_relative_path CONDA_HOME ${CONDA_HOME} fi +# set TETHYS_SRC relative to TETHYS_HOME if not already set +if [ -z "${TETHYS_SRC}" ] +then + TETHYS_SRC="${TETHYS_HOME}/src" +else + resolve_relative_path TETHYS_SRC ${TETHYS_SRC} +fi +# set TETHYS_DB_DIR relative to TETHYS_HOME if not already set +if [ -z "${TETHYS_DB_DIR}" ] +then + TETHYS_DB_DIR="${TETHYS_HOME}/psql" +else + resolve_relative_path TETHYS_DB_DIR ${TETHYS_DB_DIR} +fi if [ -n "${ECHO_COMMANDS}" ] then @@ -219,94 +333,150 @@ then mkdir -p "${TETHYS_HOME}" - # install miniconda - # first see if Miniconda is already installed - if [ -f "${CONDA_HOME}/bin/activate" ] + if [ -n "${INSTALL_MINICONDA}" ] + then + echo "MINICONDA" ${INSTALL_MINICONDA} + # install miniconda + # first see if Miniconda is already installed + if [ -f "${CONDA_HOME}/bin/activate" ] + then + echo "Using existing Miniconda installation..." + else + echo "Installing Miniconda..." + wget ${MINICONDA_URL} -O "${TETHYS_HOME}/miniconda.sh" || (echo -using curl instead; curl ${MINICONDA_URL} -o "${TETHYS_HOME}/miniconda.sh") + pushd ./ + cd "${TETHYS_HOME}" + bash miniconda.sh -b -p "${CONDA_HOME}" + popd + fi + fi + + source "${CONDA_HOME}/etc/profile.d/conda.sh" + + if [ -n "${CLONE_REPO}" ] + then + # clone Tethys repo + echo "Cloning the Tethys Platform repo..." + conda activate + conda install --yes git + git clone https://github.com/tethysplatform/tethys.git "${TETHYS_SRC}" + fi + + if [ -n "${CHECKOUT_BRANCH}" ] || [ -n "${CLONE_REPO}" ] + then + cd "${TETHYS_SRC}" + conda activate + git checkout ${BRANCH} + fi + + if [ -n "${CREATE_ENV}" ] then - echo "Using existing Miniconda installation..." + # create conda env and install Tethys + echo "Setting up the ${CONDA_ENV_NAME} environment..." + if [ "${PYTHON_VERSION}" == "2" ] + then + echo "${YELLOW}WARNING: Support for Python 2 is deprecated and will be removed in Tethys version 3.${RESET_COLOR}" + fi + conda env create -n ${CONDA_ENV_NAME} -f "${TETHYS_SRC}/environment_py${PYTHON_VERSION}.yml" + conda activate ${CONDA_ENV_NAME} + python "${TETHYS_SRC}/setup.py" develop else - echo "Installing Miniconda..." - wget ${MINICONDA_URL} -O "${TETHYS_HOME}/miniconda.sh" || (echo -using curl instead; curl ${MINICONDA_URL} -o "${TETHYS_HOME}/miniconda.sh") - pushd ./ - cd "${TETHYS_HOME}" - bash miniconda.sh -b -p "${CONDA_HOME}" - popd + echo "Activating the ${CONDA_ENV_NAME} environment..." + conda activate ${CONDA_ENV_NAME} fi - export PATH="${CONDA_HOME}/bin:$PATH" - - # clone Tethys repo - echo "Cloning the Tethys Platform repo..." - conda install --yes git - git clone https://github.com/tethysplatform/tethys.git "${TETHYS_HOME}/src" - cd "${TETHYS_HOME}/src" - git checkout ${BRANCH} - - # create conda env and install Tethys - echo "Setting up the ${CONDA_ENV_NAME} environment..." - conda env create -n ${CONDA_ENV_NAME} -f "environment_py${PYTHON_VERSION}.yml" - . activate ${CONDA_ENV_NAME} - python setup.py develop - - # only pass --allowed-hosts option to gen settings command if it is not the default - if [ ${ALLOWED_HOST} != "127.0.0.1" ] + + if [ -n "${CREATE_SETTINGS}" ] + then + # only pass --allowed-hosts option to gen settings command if it is not the default + if [ ${ALLOWED_HOST} != "127.0.0.1" ] + then + ALLOWED_HOST_OPT="--allowed-host ${ALLOWED_HOST}" + fi + tethys gen settings ${ALLOWED_HOST_OPT} --db-username ${TETHYS_DB_USERNAME} --db-password ${TETHYS_DB_PASSWORD} --db-port ${TETHYS_DB_PORT} + fi + + if [ -n "${SETUP_DB}" ] + then + # Setup local database + export TETHYS_DB_PORT="${TETHYS_DB_PORT}" + echo ${TETHYS_DB_PORT} + echo "Setting up the Tethys database..." + initdb -U postgres -D "${TETHYS_DB_DIR}/data" + pg_ctl -U postgres -D "${TETHYS_DB_DIR}/data" -l "${TETHYS_DB_DIR}/logfile" start -o "-p ${TETHYS_DB_PORT}" + echo "Waiting for databases to startup..."; sleep 10 + psql -U postgres -p ${TETHYS_DB_PORT} --command "CREATE USER ${TETHYS_DB_USERNAME} WITH NOCREATEDB NOCREATEROLE NOSUPERUSER PASSWORD '${TETHYS_DB_PASSWORD}';" + createdb -U postgres -p ${TETHYS_DB_PORT} -O ${TETHYS_DB_USERNAME} ${TETHYS_DB_USERNAME} -E utf-8 -T template0 + psql -U postgres -p ${TETHYS_DB_PORT} --command "CREATE USER ${TETHYS_DB_SUPER_USERNAME} WITH CREATEDB NOCREATEROLE SUPERUSER PASSWORD '${TETHYS_DB_SUPER_PASSWORD}';" + createdb -U postgres -p ${TETHYS_DB_PORT} -O ${TETHYS_DB_SUPER_USERNAME} ${TETHYS_DB_SUPER_USERNAME} -E utf-8 -T template0 + fi + + if [ -n "${INITIALIZE_DB}" ] || [ -n "${SETUP_DB}" ] then - ALLOWED_HOST_OPT="--allowed-host ${ALLOWED_HOST}" + # Initialize Tethys database + tethys manage syncdb + fi + + if [ -n "${CREATE_TETHYS_SUPER_USER}" ] || [ -n "${SETUP_DB}" ] + then + echo "from django.contrib.auth.models import User; User.objects.create_superuser('${TETHYS_SUPER_USER}', '${TETHYS_SUPER_USER_EMAIL}', '${TETHYS_SUPER_USER_PASS}')" | python "${TETHYS_SRC}/manage.py" shell + fi + + if [ -n "${SETUP_DB}" ] + then + pg_ctl -U postgres -D "${TETHYS_DB_DIR}/data" stop + fi + + if [ -n "${CREATE_ENV_SCRIPTS}" ] + then + # Create environment activatescripts + mkdir -p "${ACTIVATE_DIR}" + + echo "export TETHYS_HOME='${TETHYS_HOME}'" >> "${ACTIVATE_SCRIPT}" + echo "export TETHYS_PORT='${TETHYS_PORT}'" >> "${ACTIVATE_SCRIPT}" + echo "export TETHYS_DB_PORT='${TETHYS_DB_PORT}'" >> "${ACTIVATE_SCRIPT}" + echo "export TETHYS_DB_DIR='${TETHYS_DB_DIR}'" >> "${ACTIVATE_SCRIPT}" + echo "export CONDA_HOME='${CONDA_HOME}'" >> "${ACTIVATE_SCRIPT}" + echo "export CONDA_ENV_NAME='${CONDA_ENV_NAME}'" >> "${ACTIVATE_SCRIPT}" + echo "alias tethys_start_db='pg_ctl -U postgres -D \"\${TETHYS_DB_DIR}/data\" -l \"\${TETHYS_DB_DIR}/logfile\" start -o \"-p \${TETHYS_DB_PORT}\"'" >> "${ACTIVATE_SCRIPT}" + echo "alias tstartdb=tethys_start_db" >> "${ACTIVATE_SCRIPT}" + echo "alias tethys_stop_db='pg_ctl -U postgres -D \"\${TETHYS_DB_DIR}/data\" stop'" >> "${ACTIVATE_SCRIPT}" + echo "alias tstopdb=tethys_stop_db" >> "${ACTIVATE_SCRIPT}" + echo "alias tms='tethys manage start -p ${ALLOWED_HOST}:\${TETHYS_PORT}'" >> "${ACTIVATE_SCRIPT}" + echo "alias tstart='tstartdb; tms'" >> "${ACTIVATE_SCRIPT}" + fi + + if [ -n "${CREATE_SHORTCUTS}" ] + then + echo "# Tethys Platform" >> ~/${BASH_PROFILE} + echo "alias t='source ${CONDA_HOME}/etc/profile.d/conda.sh; conda activate ${CONDA_ENV_NAME}'" >> ~/${BASH_PROFILE} + fi + + echo "Deactivating the ${CONDA_ENV_NAME} environment..." + conda deactivate + + if [ -n "${CREATE_ENV_SCRIPTS}" ] + then + # Create environment deactivate scripts + mkdir -p "${DEACTIVATE_DIR}" + + echo "unset TETHYS_HOME" >> "${DEACTIVATE_SCRIPT}" + echo "unset TETHYS_PORT" >> "${DEACTIVATE_SCRIPT}" + echo "unset TETHYS_DB_PORT" >> "${DEACTIVATE_SCRIPT}" + echo "unset TETHYS_DB_DIR" >> "${DEACTIVATE_SCRIPT}" + echo "unset CONDA_HOME" >> "${DEACTIVATE_SCRIPT}" + echo "unset CONDA_ENV_NAME" >> "${DEACTIVATE_SCRIPT}" + echo "unalias tethys_start_db" >> "${DEACTIVATE_SCRIPT}" + echo "unalias tstartdb" >> "${DEACTIVATE_SCRIPT}" + echo "unalias tethys_stop_db" >> "${DEACTIVATE_SCRIPT}" + echo "unalias tstopdb" >> "${DEACTIVATE_SCRIPT}" + echo "unalias tms" >> "${DEACTIVATE_SCRIPT}" + echo "unalias tstart" >> "${DEACTIVATE_SCRIPT}" fi - tethys gen settings ${ALLOWED_HOST_OPT} --db-username ${TETHYS_DB_USERNAME} --db-password ${TETHYS_DB_PASSWORD} --db-port ${TETHYS_DB_PORT} - - # Setup local database - echo "Setting up the Tethys database..." - initdb -U postgres -D "${TETHYS_HOME}/psql/data" - pg_ctl -U postgres -D "${TETHYS_HOME}/psql/data" -l "${TETHYS_HOME}/psql/logfile" start -o "-p ${TETHYS_DB_PORT}" - echo "Waiting for databases to startup..."; sleep 10 - psql -U postgres -p ${TETHYS_DB_PORT} --command "CREATE USER ${TETHYS_DB_USERNAME} WITH NOCREATEDB NOCREATEROLE NOSUPERUSER PASSWORD '${TETHYS_DB_PASSWORD}';" - createdb -U postgres -p ${TETHYS_DB_PORT} -O ${TETHYS_DB_USERNAME} ${TETHYS_DB_USERNAME} -E utf-8 -T template0 - - # Initialze Tethys database - tethys manage syncdb - echo "from django.contrib.auth.models import User; User.objects.create_superuser('${TETHYS_SUPER_USER}', '${TETHYS_SUPER_USER_EMAIL}', '${TETHYS_SUPER_USER_PASS}')" | python manage.py shell - pg_ctl -U postgres -D "${TETHYS_HOME}/psql/data" stop - . deactivate - - - # Create environment activate/deactivate scripts - mkdir -p "${ACTIVATE_DIR}" "${DEACTIVATE_DIR}" - - echo "export TETHYS_HOME='${TETHYS_HOME}'" >> "${ACTIVATE_SCRIPT}" - echo "export TETHYS_PORT='${TETHYS_PORT}'" >> "${ACTIVATE_SCRIPT}" - echo "export TETHYS_DB_PORT='${TETHYS_DB_PORT}'" >> "${ACTIVATE_SCRIPT}" - echo "export CONDA_HOME='${CONDA_HOME}'" >> "${ACTIVATE_SCRIPT}" - echo "export CONDA_ENV_NAME='${CONDA_ENV_NAME}'" >> "${ACTIVATE_SCRIPT}" - echo "alias tethys_start_db='pg_ctl -U postgres -D \"\${TETHYS_HOME}/psql/data\" -l \"\${TETHYS_HOME}/psql/logfile\" start -o \"-p \${TETHYS_DB_PORT}\"'" >> "${ACTIVATE_SCRIPT}" - echo "alias tstartdb=tethys_start_db" >> "${ACTIVATE_SCRIPT}" - echo "alias tethys_stop_db='pg_ctl -U postgres -D \"\${TETHYS_HOME}/psql/data\" stop'" >> "${ACTIVATE_SCRIPT}" - echo "alias tstopdb=tethys_stop_db" >> "${ACTIVATE_SCRIPT}" - echo "alias tms='tethys manage start -p ${ALLOWED_HOST}:\${TETHYS_PORT}'" >> "${ACTIVATE_SCRIPT}" - echo "alias tstart='tstartdb; tms'" >> "${ACTIVATE_SCRIPT}" - - echo "unset TETHYS_HOME" >> "${DEACTIVATE_SCRIPT}" - echo "unset TETHYS_PORT" >> "${DEACTIVATE_SCRIPT}" - echo "unset TETHYS_DB_PORT" >> "${DEACTIVATE_SCRIPT}" - echo "unset CONDA_HOME" >> "${DEACTIVATE_SCRIPT}" - echo "unset CONDA_ENV_NAME" >> "${DEACTIVATE_SCRIPT}" - echo "unalias tethys_start_db" >> "${DEACTIVATE_SCRIPT}" - echo "unalias tstartdb" >> "${DEACTIVATE_SCRIPT}" - echo "unalias tethys_stop_db" >> "${DEACTIVATE_SCRIPT}" - echo "unalias tstopdb" >> "${DEACTIVATE_SCRIPT}" - echo "unalias tms" >> "${DEACTIVATE_SCRIPT}" - echo "unalias tstart" >> "${DEACTIVATE_SCRIPT}" - - echo "# Tethys Platform" >> ~/${BASH_PROFILE} - echo "alias t='. ${CONDA_HOME}/bin/activate ${CONDA_ENV_NAME}'" >> ~/${BASH_PROFILE} fi # Install Docker (if flag is set) set +e # don't exit on error anymore -# Rename some variables for reference after deactivating tethys environment. -TETHYS_CONDA_HOME=${CONDA_HOME} -TETHYS_CONDA_ENV_NAME=${CONDA_ENV_NAME} - # Install Production configuration if flag is set ubuntu_debian_production_install() { @@ -343,17 +513,17 @@ centos_production_install() { configure_selinux() { sudo yum install setroubleshoot -y - sudo semanage fcontext -a -t httpd_config_t ${TETHYS_HOME}/src/tethys_portal/tethys_nginx.conf - sudo restorecon -v ${TETHYS_HOME}/src/tethys_portal/tethys_nginx.conf + sudo semanage fcontext -a -t httpd_config_t ${TETHYS_SRC}/tethys_portal/tethys_nginx.conf + sudo restorecon -v ${TETHYS_SRC}/tethys_portal/tethys_nginx.conf sudo semanage fcontext -a -t httpd_sys_content_t "${TETHYS_HOME}(/.*)?" sudo semanage fcontext -a -t httpd_sys_content_t "${TETHYS_HOME}/static(/.*)?" sudo semanage fcontext -a -t httpd_sys_rw_content_t "${TETHYS_HOME}/workspaces(/.*)?" sudo restorecon -R -v ${TETHYS_HOME} > /dev/null - echo $'module tethys-selinux-policy 1.0;\nrequire {type httpd_t; type init_t; class unix_stream_socket connectto; }\n#============= httpd_t ==============\nallow httpd_t init_t:unix_stream_socket connectto;' > ${TETHYS_HOME}/src/tethys_portal/tethys-selinux-policy.te + echo $'module tethys-selinux-policy 1.0;\nrequire {type httpd_t; type init_t; class unix_stream_socket connectto; }\n#============= httpd_t ==============\nallow httpd_t init_t:unix_stream_socket connectto;' > ${TETHYS_SRC}/tethys_portal/tethys-selinux-policy.te - checkmodule -M -m -o ${TETHYS_HOME}/src/tethys_portal/tethys-selinux-policy.mod ${TETHYS_HOME}/src/tethys_portal/tethys-selinux-policy.te - semodule_package -o ${TETHYS_HOME}/src/tethys_portal/tethys-selinux-policy.pp -m ${TETHYS_HOME}/src/tethys_portal/tethys-selinux-policy.mod - sudo semodule -i ${TETHYS_HOME}/src/tethys_portal/tethys-selinux-policy.pp + checkmodule -M -m -o ${TETHYS_SRC}/tethys_portal/tethys-selinux-policy.mod ${TETHYS_SRC}/tethys_portal/tethys-selinux-policy.te + semodule_package -o ${TETHYS_SRC}/tethys_portal/tethys-selinux-policy.pp -m ${TETHYS_SRC}/tethys_portal/tethys-selinux-policy.mod + sudo semodule -i ${TETHYS_SRC}/tethys_portal/tethys-selinux-policy.pp } if [ -n "${LINUX_DISTRIBUTION}" -a "${PRODUCTION}" = "true" ] @@ -383,9 +553,9 @@ then ;; esac - - . ${CONDA_HOME}/bin/activate ${CONDA_ENV_NAME} - pg_ctl -U postgres -D "${TETHYS_HOME}/psql/data" -l "${TETHYS_HOME}/psql/logfile" start -o "-p ${TETHYS_DB_PORT}" + source "${CONDA_HOME}/etc/profile.d/conda.sh" + conda activate ${CONDA_ENV_NAME} + pg_ctl -U postgres -D "${TETHYS_DB_DIR}/data" -l "${TETHYS_DB_DIR}/logfile" start -o "-p ${TETHYS_DB_PORT}" echo "Waiting for databases to startup..."; sleep 5 conda install -c conda-forge uwsgi -y tethys gen settings --production --allowed-host=${ALLOWED_HOST} --db-username ${TETHYS_DB_USERNAME} --db-password ${TETHYS_DB_PASSWORD} --db-port ${TETHYS_DB_PORT} --overwrite @@ -401,25 +571,25 @@ then sudo chmod 705 ~ sudo mkdir /var/log/uwsgi sudo touch /var/log/uwsgi/tethys.log - sudo ln -s ${TETHYS_HOME}/src/tethys_portal/tethys_nginx.conf /etc/nginx/${NGINX_SITES_DIR}/ + sudo ln -s ${TETHYS_SRC}/tethys_portal/tethys_nginx.conf /etc/nginx/${NGINX_SITES_DIR}/ if [ -n "${SELINUX}" ] then configure_selinux fi - sudo chown -R ${NGINX_USER}:${NGINX_GROUP} ${TETHYS_HOME}/src /var/log/uwsgi/tethys.log - sudo systemctl enable ${TETHYS_HOME}/src/tethys_portal/tethys.uwsgi.service + sudo chown -R ${NGINX_USER}:${NGINX_GROUP} ${TETHYS_SRC} /var/log/uwsgi/tethys.log + sudo systemctl enable ${TETHYS_SRC}/tethys_portal/tethys.uwsgi.service sudo systemctl start tethys.uwsgi.service sudo systemctl restart nginx set +x - . deactivate + conda deactivate echo "export NGINX_USER='${NGINX_USER}'" >> "${ACTIVATE_SCRIPT}" echo "export NGINX_HOME='${NGINX_HOME}'" >> "${ACTIVATE_SCRIPT}" - echo "alias tethys_user_own='sudo chown -R \${USER} \"\${TETHYS_HOME}/src\" \"\${TETHYS_HOME}/static\" \"\${TETHYS_HOME}/workspaces\" \"\${TETHYS_HOME}/apps\"'" >> "${ACTIVATE_SCRIPT}" + echo "alias tethys_user_own='sudo chown -R \${USER} \"\${TETHYS_SRC}\" \"\${TETHYS_HOME}/static\" \"\${TETHYS_HOME}/workspaces\" \"\${TETHYS_HOME}/apps\"'" >> "${ACTIVATE_SCRIPT}" echo "alias tuo=tethys_user_own" >> "${ACTIVATE_SCRIPT}" - echo "alias tethys_server_own='sudo chown -R \${NGINX_USER}:\${NGINX_USER} \"\${TETHYS_HOME}/src\" \"\${TETHYS_HOME}/static\" \"\${TETHYS_HOME}/workspaces\" \"\${TETHYS_HOME}/apps\"'" >> "${ACTIVATE_SCRIPT}" + echo "alias tethys_server_own='sudo chown -R \${NGINX_USER}:\${NGINX_USER} \"\${TETHYS_SRC}\" \"\${TETHYS_HOME}/static\" \"\${TETHYS_HOME}/workspaces\" \"\${TETHYS_HOME}/apps\"'" >> "${ACTIVATE_SCRIPT}" echo "alias tso=tethys_server_own" >> "${ACTIVATE_SCRIPT}" echo "alias tethys_server_restart='tso; sudo systemctl restart tethys.uwsgi.service; sudo systemctl restart nginx'" >> "${ACTIVATE_SCRIPT}" echo "alias tsr=tethys_server_restart" >> "${ACTIVATE_SCRIPT}" @@ -444,9 +614,10 @@ installation_warning(){ finalize_docker_install(){ sudo groupadd docker sudo gpasswd -a ${USER} docker - . ${TETHYS_CONDA_HOME}/bin/activate ${TETHYS_CONDA_ENV_NAME} + source "${CONDA_HOME}/etc/profile.d/conda.sh" + conda activate ${CONDA_ENV_NAME} sg docker -c "tethys docker init ${DOCKER_OPTIONS}" - . deactivate + conda deactivate echo "Docker installation finished!" echo "You must re-login for Docker permissions to be activated." echo "(Alternatively you can run 'newgrp docker')" @@ -533,7 +704,10 @@ if [ -z "${SKIP_TETHYS_INSTALL}" ] then echo "Tethys installation complete!" echo - echo "NOTE: to enable the new alias 't' which activates the tethys environment you must run '. ~/${BASH_PROFILE}'" + if [ -n "${CREATE_SHORTCUTS}" ] + then + echo "NOTE: to enable the new alias 't' which activates the tethys environment you must run '. ~/${BASH_PROFILE}'" + fi fi on_exit(){ diff --git a/setup.py b/setup.py index 0c5480e22..b4695529c 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ requires = [] -version = '2.0.4' +version = '2.1.0' setup( name='tethys_platform', @@ -44,16 +44,12 @@ 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], entry_points={ - 'console_scripts': ['tethys=tethys_apps.cli:tethys_command',], + 'console_scripts': ['tethys=tethys_apps.cli:tethys_command', ], }, install_requires=requires, extras_require={ - 'tests': [], - 'docs': [ - 'sphinx', - 'sphinx_rtd_theme', - 'sphinxcontrib-napoleon', - 'pbr', - ] + 'tests': [ + 'requests_mock', + ], }, ) diff --git a/templates/base.html b/templates/base.html index c9383ec8c..c4064d11f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -57,7 +57,7 @@ {% endcomment %} {% block links %} - + {% endblock %} {% comment "import_gizmos explanation" %} diff --git a/templates/tethys_portal/accounts/login.html b/templates/tethys_portal/accounts/login.html index 1f01e54d6..386715385 100644 --- a/templates/tethys_portal/accounts/login.html +++ b/templates/tethys_portal/accounts/login.html @@ -65,7 +65,7 @@

Log In

{% if signup_enabled %} Don't have an account? Sign Up {% endif %} - Forget your password? + Forgot your password? diff --git a/tests/apps/tethysapp-test_app/.gitignore b/tests/apps/tethysapp-test_app/.gitignore new file mode 100644 index 000000000..b0419c5d9 --- /dev/null +++ b/tests/apps/tethysapp-test_app/.gitignore @@ -0,0 +1,9 @@ +*.pydevproject +*.project +*.egg-info +*.class +*.pyo +*.pyc +*.db +*.sqlite +*.DS_Store \ No newline at end of file diff --git a/tests/apps/tethysapp-test_app/setup.py b/tests/apps/tethysapp-test_app/setup.py new file mode 100644 index 000000000..5aeafad8d --- /dev/null +++ b/tests/apps/tethysapp-test_app/setup.py @@ -0,0 +1,34 @@ +import os +from setuptools import setup, find_packages +from tethys_apps.app_installation import custom_develop_command, custom_install_command + +# -- Apps Definition -- # +app_package = 'test_app' +release_package = 'tethysapp-' + app_package +app_class = 'test_app.app:TestApp' +app_package_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tethysapp', app_package) + +# -- Python Dependencies -- # +dependencies = [] + +setup( + name=release_package, + version='0.0.1', + tags='', + description='', + long_description='', + keywords='', + author='', + author_email='', + url='', + license='', + packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), + namespace_packages=['tethysapp', 'tethysapp.' + app_package], + include_package_data=True, + zip_safe=False, + install_requires=dependencies, + cmdclass={ + 'install': custom_install_command(app_package, app_package_dir, dependencies), + 'develop': custom_develop_command(app_package, app_package_dir, dependencies) + } +) diff --git a/tests/apps/tethysapp-test_app/tethysapp/__init__.py b/tests/apps/tethysapp-test_app/tethysapp/__init__.py new file mode 100644 index 000000000..2e2033b3c --- /dev/null +++ b/tests/apps/tethysapp-test_app/tethysapp/__init__.py @@ -0,0 +1,7 @@ +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/__init__.py b/tests/apps/tethysapp-test_app/tethysapp/test_app/__init__.py new file mode 100644 index 000000000..2e2033b3c --- /dev/null +++ b/tests/apps/tethysapp-test_app/tethysapp/test_app/__init__.py @@ -0,0 +1,7 @@ +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/api.py b/tests/apps/tethysapp-test_app/tethysapp/test_app/api.py new file mode 100644 index 000000000..252434b11 --- /dev/null +++ b/tests/apps/tethysapp-test_app/tethysapp/test_app/api.py @@ -0,0 +1,19 @@ +# Define your REST API endpoints here. +# In the comments below is an example. +# For more information, see: +# http://docs.tethysplatform.org/en/dev/tethys_sdk/rest_api.html +""" +from django.http import JsonResponse +from rest_framework.authentication import TokenAuthentication +from rest_framework.decorators import api_view, authentication_classes + +@api_view(['GET']) +@authentication_classes((TokenAuthentication,)) +def get_data(request): + ''' + API Controller for getting data + ''' + name = request.GET.get('name') + data = {"name": name} + return JsonResponse(data) +""" diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/app.py b/tests/apps/tethysapp-test_app/tethysapp/test_app/app.py new file mode 100644 index 000000000..d13dbbb9c --- /dev/null +++ b/tests/apps/tethysapp-test_app/tethysapp/test_app/app.py @@ -0,0 +1,156 @@ +from tethys_sdk.base import TethysAppBase, url_map_maker +from tethys_sdk.app_settings import CustomSetting, PersistentStoreDatabaseSetting, PersistentStoreConnectionSetting, \ + DatasetServiceSetting, SpatialDatasetServiceSetting, WebProcessingServiceSetting + + +class TestApp(TethysAppBase): + """ + Tethys app class for Test App. + """ + + name = 'Test App' + index = 'test_app:home' + icon = 'test_app/images/icon.gif' + package = 'test_app' + root_url = 'test-app' + color = '#2c3e50' + description = 'Place a brief description of your app here.' + tags = '' + enable_feedback = False + feedback_emails = [] + + def url_maps(self): + """ + Add controllers + """ + UrlMap = url_map_maker(self.root_url) + + url_maps = ( + UrlMap( + name='home', + url='test-app/{var1}/{var2}', + controller='test_app.controllers.home' + ), + ) + + return url_maps + + def custom_settings(self): + """ + Example custom_settings method. + """ + custom_settings = ( + CustomSetting( + name='default_name', + type=CustomSetting.TYPE_STRING, + description='Default model name.', + required=True, + ), + CustomSetting( + name='max_count', + type=CustomSetting.TYPE_INTEGER, + description='Maximum allowed count in a method.', + required=False + ), + CustomSetting( + name='change_factor', + type=CustomSetting.TYPE_FLOAT, + description='Change factor that is applied to some process.', + required=False + ), + CustomSetting( + name='enable_feature', + type=CustomSetting.TYPE_BOOLEAN, + description='Enable this feature when True.', + required=False + ) + ) + + return custom_settings + + def persistent_store_settings(self): + """ + Example persistent_store_settings method. + """ + ps_settings = ( + # Connection only, no database + PersistentStoreConnectionSetting( + name='primary', + description='Connection with superuser role needed.', + required=True + ), + # Connection only, no database + PersistentStoreConnectionSetting( + name='creator', + description='Create database role only.', + required=False + ), + # Spatial database + PersistentStoreDatabaseSetting( + name='spatial_db', + description='for storing important spatial stuff', + required=True, + initializer='appsettings.model.init_spatial_db', + spatial=True, + ), + # Non-spatial database + PersistentStoreDatabaseSetting( + name='temp_db', + description='for storing temporary stuff', + required=False, + initializer='appsettings.model.init_temp_db', + spatial=False, + ) + ) + + return ps_settings + + def dataset_service_settings(self): + """ + Example dataset_service_settings method. + """ + ds_settings = ( + DatasetServiceSetting( + name='primary_ckan', + description='Primary CKAN service for app to use.', + engine=DatasetServiceSetting.CKAN, + required=True, + ), + DatasetServiceSetting( + name='hydroshare', + description='HydroShare service for app to use.', + engine=DatasetServiceSetting.HYDROSHARE, + required=False + ) + ) + + return ds_settings + + def spatial_dataset_service_settings(self): + """ + Example spatial_dataset_service_settings method. + """ + sds_settings = ( + SpatialDatasetServiceSetting( + name='primary_geoserver', + description='spatial dataset service for app to use', + engine=SpatialDatasetServiceSetting.GEOSERVER, + required=True, + ), + ) + + return sds_settings + + def web_processing_service_settings(self): + """ + Example wps_services method. + """ + wps_services = ( + WebProcessingServiceSetting( + name='primary_52n', + description='WPS service for app to use', + required=True, + ), + ) + + return wps_services diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/controllers.py b/tests/apps/tethysapp-test_app/tethysapp/test_app/controllers.py new file mode 100644 index 000000000..a06fb6d9d --- /dev/null +++ b/tests/apps/tethysapp-test_app/tethysapp/test_app/controllers.py @@ -0,0 +1,75 @@ +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from tethys_sdk.gizmos import Button + + +@login_required() +def home(request, var1, var2): + """ + Controller for the app home page. + """ + save_button = Button( + display_text='', + name='save-button', + icon='glyphicon glyphicon-floppy-disk', + style='success', + attributes={ + 'data-toggle': 'tooltip', + 'data-placement': 'top', + 'title': 'Save' + } + ) + + edit_button = Button( + display_text='', + name='edit-button', + icon='glyphicon glyphicon-edit', + style='warning', + attributes={ + 'data-toggle': 'tooltip', + 'data-placement': 'top', + 'title': 'Edit' + } + ) + + remove_button = Button( + display_text='', + name='remove-button', + icon='glyphicon glyphicon-remove', + style='danger', + attributes={ + 'data-toggle': 'tooltip', + 'data-placement': 'top', + 'title': 'Remove' + } + ) + + previous_button = Button( + display_text='Previous', + name='previous-button', + attributes={ + 'data-toggle': 'tooltip', + 'data-placement': 'top', + 'title': 'Previous' + } + ) + + next_button = Button( + display_text='Next', + name='next-button', + attributes={ + 'data-toggle': 'tooltip', + 'data-placement': 'top', + 'title': 'Next' + } + ) + + context = { + 'save_button': save_button, + 'edit_button': edit_button, + 'remove_button': remove_button, + 'previous_button': previous_button, + 'next_button': next_button + } + + return render(request, 'test_app/home.html', context) diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/handoff.py b/tests/apps/tethysapp-test_app/tethysapp/test_app/handoff.py new file mode 100644 index 000000000..e39dd3ef4 --- /dev/null +++ b/tests/apps/tethysapp-test_app/tethysapp/test_app/handoff.py @@ -0,0 +1,26 @@ +# Define your handoff handlers here +# for more information, see: +# http://docs.tethysplatform.org/en/dev/tethys_sdk/handoff.html + +import os +import requests + + +def csv(request, csv_url): + """ + Handoff handler for csv files. + """ + # Get a filename in the current user's workspace + user_workspace = request.workspace + filename = os.path.join(user_workspace, 'hydrograph.csv') + + # Initiate a GET request on the CSV URL + response = requests.get(csv_url, stream=True) + + # Stream content into a file + with open(filename, 'w') as f: + for chunk in response.iter_content(chunk_size=512): + if chunk: + f.write(chunk) + + return 'hydrograph_plotter:plot_csv' diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/model.py b/tests/apps/tethysapp-test_app/tethysapp/test_app/model.py new file mode 100644 index 000000000..711fcfe86 --- /dev/null +++ b/tests/apps/tethysapp-test_app/tethysapp/test_app/model.py @@ -0,0 +1,3 @@ +# Put your persistent store models in this file +def test_initializer(): + pass diff --git a/tethys_apps/cli/scaffold_templates/app_templates/default/__init__.py b/tests/apps/tethysapp-test_app/tethysapp/test_app/public/css/main.css similarity index 100% rename from tethys_apps/cli/scaffold_templates/app_templates/default/__init__.py rename to tests/apps/tethysapp-test_app/tethysapp/test_app/public/css/main.css diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/public/images/icon.gif b/tests/apps/tethysapp-test_app/tethysapp/test_app/public/images/icon.gif new file mode 100644 index 000000000..5c8236e97 Binary files /dev/null and b/tests/apps/tethysapp-test_app/tethysapp/test_app/public/images/icon.gif differ diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/.gitadd b/tests/apps/tethysapp-test_app/tethysapp/test_app/public/js/main.js similarity index 100% rename from tethys_apps/cli/scaffold_templates/extension_templates/.gitadd rename to tests/apps/tethysapp-test_app/tethysapp/test_app/public/js/main.js diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/templates/test_app/base.html b/tests/apps/tethysapp-test_app/tethysapp/test_app/templates/test_app/base.html new file mode 100644 index 000000000..fa4c0d586 --- /dev/null +++ b/tests/apps/tethysapp-test_app/tethysapp/test_app/templates/test_app/base.html @@ -0,0 +1,42 @@ +{% extends "tethys_apps/app_base.html" %} + +{% load staticfiles %} + +{% block title %}{{ tethys_app.name }}{% endblock %} + +{% block app_icon %} + {# The path you provided in your app.py is accessible through the tethys_app.icon context variable #} + +{% endblock %} + +{# The name you provided in your app.py is accessible through the tethys_app.name context variable #} +{% block app_title %}{{ tethys_app.name }}{% endblock %} + +{% block app_navigation_items %} +
  • App Navigation
  • +
  • Home
  • +
  • Jobs
  • +
  • Results
  • +
  • Steps
  • +
  • 1. The First Step
  • +
  • 2. The Second Step
  • +
  • 3. The Third Step
  • +
  • +
  • Get Started
  • +{% endblock %} + +{% block app_content %} +{% endblock %} + +{% block app_actions %} +{% endblock %} + +{% block content_dependent_styles %} + {{ block.super }} + +{% endblock %} + +{% block scripts %} + {{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/templates/test_app/home.html b/tests/apps/tethysapp-test_app/tethysapp/test_app/templates/test_app/home.html new file mode 100644 index 000000000..c8c4a06fd --- /dev/null +++ b/tests/apps/tethysapp-test_app/tethysapp/test_app/templates/test_app/home.html @@ -0,0 +1,51 @@ +{% extends "test_app/base.html" %} +{% load tethys_gizmos %} + +{% block header_buttons %} +
    + +
    +{% endblock %} + +{% block app_content %} +

    Welcome to your Tethys App!

    +

    Take advantage of beautiful typography to organize the content of your app:

    +

    Heading 1

    +

    Heading 2

    +

    Heading 3

    +

    Heading 4

    +
    Heading 5
    +
    Heading 6
    +{% endblock %} + +{# Use the after_app_content block for modals #} +{% block after_app_content %} + + +{% endblock %} + +{% block app_actions %} + {% gizmo save_button %} + {% gizmo edit_button %} + {% gizmo remove_button %} + {% gizmo previous_button %} + {% gizmo next_button %} +{% endblock %} \ No newline at end of file diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/tests/__init__.py b/tests/apps/tethysapp-test_app/tethysapp/test_app/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/tests/tests.py b/tests/apps/tethysapp-test_app/tethysapp/test_app/tests/tests.py new file mode 100644 index 000000000..e0e8bda8d --- /dev/null +++ b/tests/apps/tethysapp-test_app/tethysapp/test_app/tests/tests.py @@ -0,0 +1,166 @@ +# Most of your test classes should inherit from TethysTestCase +from tethys_sdk.testing import TethysTestCase + +# Use if your app has persistent stores that will be tested against. +# Your app class from app.py must be passed as an argument to the TethysTestCase functions to both +# create and destroy the temporary persistent stores for your app used during testing +# from ..app import TestApp + +# Use if you'd like a simplified way to test rendered HTML templates. +# You likely need to install BeautifulSoup, as it is not included by default in Tethys Platform +# 1. Open a terminal +# 2. Enter command ". /usr/lib/tethys/bin/activate" to activate the Tethys python environment +# 3. Enter command "pip install beautifulsoup4" +# For help, see https://www.crummy.com/software/BeautifulSoup/bs4/doc/ +# from bs4 import BeautifulSoup + +""" +To run any tests: + 1. Open a terminal + 2. Enter command ". /usr/lib/tethys/bin/activate" to activate the Tethys python environment + 3. In settings.py make sure that the tethys_default database user is set to tethys_super + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'tethys_default', + 'USER': 'tethys_super', + 'PASSWORD': 'pass', + 'HOST': '127.0.0.1', + 'PORT': '5435' + } + } + 4. Enter tethys test command. + The general form is: "tethys test -f tethys_apps.tethysapp....." + See below for specific examples + + To run all tests across this app: + Test command: "tethys test -f tethys_apps.tethysapp.test_app" + + To run all tests in this file: + Test command: "tethys test -f tethys_apps.tethysapp.test_app.tests.tests" + + To run tests in the TestAppTestCase class: + Test command: "tethys test -f tethys_apps.tethysapp.test_app.tests.tests.TestAppTestCase" + + To run only the test_if_tethys_platform_is_great function in the TestAppTestCase class: + Test command: "tethys test -f tethys_apps.tethysapp.test_app.tests.tests.TestAppTestCase.test_if_tethys_platform_is_great" + +To learn more about writing tests, see: + https://docs.djangoproject.com/en/1.9/topics/testing/overview/#writing-tests + https://docs.python.org/2.7/library/unittest.html#module-unittest +""" # noqa:E501 + + +class TestAppTestCase(TethysTestCase): + """ + In this class you may define as many functions as you'd like to test different aspects of your app. + Each function must start with the word "test" for it to be recognized and executed during testing. + You could also create multiple TethysTestCase classes within this or other python files to organize your tests. + """ + + def set_up(self): + """ + This function is not required, but can be used if any environmental setup needs to take place before + execution of each test function. Thus, if you have multiple test that require the same setup to run, + place that code here. For example, if you are testing against any persistent stores, you should call the + test database creation function here, like so: + + self.create_test_persistent_stores_for_app(TestApp) + + If you are testing against a controller that check for certain user info, you can create a fake test user and + get a test client, like so: + + #The test client simulates a browser that can navigate your app's url endpoints + self.c = self.get_test_client() + self.user = self.create_test_user(username="joe", password="secret", email="joe@some_site.com") + # To create a super_user, use "self.create_test_superuser(*params)" with the same params + + # To force a login for the test user + self.c.force_login(self.user) + + # If for some reason you do not want to force a login, you can use the following: + login_success = self.c.login(username="joe", password="secret") + + NOTE: You do not have place these functions here, but if they are not placed here and are needed + then they must be placed at the beginning of your individual test functions. Also, if a certain + setup does not apply to all of your functions, you should either place it directly in each + function it applies to, or maybe consider creating a new test file or test class to group similar + tests. + """ + pass + + def tear_down(self): + """ + This function is not required, but should be used if you need to tear down any environmental setup + that took place before execution of the test functions. If you are testing against any persistent + stores, you should call the test database destruction function from here, like so: + + self.destroy_test_persistent_stores_for_app(TestApp) + + NOTE: You do not have to set these functions up here, but if they are not placed here and are needed + then they must be placed at the very end of your individual test functions. Also, if certain + tearDown code does not apply to all of your functions, you should either place it directly in each + function it applies to, or maybe consider creating a new test file or test class to group similar + tests. + """ + pass + + def is_tethys_platform_great(self): + return True + + def test_if_tethys_platform_is_great(self): + """ + This is an example test function that can be modified to test a specific aspect of your app. + It is required that the function name begins with the word "test" or it will not be executed. + Generally, the code written here will consist of many assert methods. + A list of assert methods is included here for reference or to get you started: + assertEqual(a, b) a == b + assertNotEqual(a, b) a != b + assertTrue(x) bool(x) is True + assertFalse(x) bool(x) is False + assertIs(a, b) a is b + assertIsNot(a, b) a is not b + assertIsNone(x) x is None + assertIsNotNone(x) x is not None + assertIn(a, b) a in b + assertNotIn(a, b) a not in b + assertIsInstance(a, b) isinstance(a, b) + assertNotIsInstance(a, b) !isinstance(a, b) + Learn more about assert methods here: + https://docs.python.org/2.7/library/unittest.html#assert-methods + """ + + self.assertEqual(self.is_tethys_platform_great(), True) + self.assertNotEqual(self.is_tethys_platform_great(), False) + self.assertTrue(self.is_tethys_platform_great()) + self.assertFalse(not self.is_tethys_platform_great()) + self.assertIs(self.is_tethys_platform_great(), True) + self.assertIsNot(self.is_tethys_platform_great(), False) + + def test_home_controller(self): + """ + This is an example test function of how you might test a controller that returns an HTML template rendered + with context variables. + """ + + # If all test functions were testing controllers or required a test client for another reason, the following + # 3 lines of code could be placed once in the set_up function. Note that in that case, each variable should be + # prepended with "self." (i.e. self.c = ...) to make those variables "global" to this test class and able to be + # used in each separate test function. + c = self.get_test_client() + user = self.create_test_user(username="joe", password="secret", email="joe@some_site.com") + c.force_login(user) + + # Have the test client "browse" to your home page + response = c.get('/apps/test-app/') # The final '/' is essential for all pages/controllers + + # Test that the request processed correctly (with a 200 status code) + self.assertEqual(response.status_code, 200) + + ''' + NOTE: Next, you would likely test that your context variables returned as expected. That would look + something like the following: + + context = response.context + self.assertEqual(context['my_integer'], 10) + ''' diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/workspaces/app_workspace/.gitkeep b/tests/apps/tethysapp-test_app/tethysapp/test_app/workspaces/app_workspace/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/apps/tethysapp-test_app/tethysapp/test_app/workspaces/user_workspaces/.gitkeep b/tests/apps/tethysapp-test_app/tethysapp/test_app/workspaces/user_workspaces/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/coverage.cfg b/tests/coverage.cfg index 5856970aa..68d61a745 100644 --- a/tests/coverage.cfg +++ b/tests/coverage.cfg @@ -6,12 +6,11 @@ source = $TETHYS_TEST_DIR/../tethys_apps $TETHYS_TEST_DIR/../tethys_config $TETHYS_TEST_DIR/../tethys_gizmos $TETHYS_TEST_DIR/../tethys_portal - $TETHYS_TEST_DIR/../tethys_sdk $TETHYS_TEST_DIR/../tethys_services -omit = *.egg-info +omit = *.egg-info, */migrations/*, $TETHYS_TEST_DIR/../tethys_sdk*, $TETHYS_TEST_DIR/../tethys_portal/wsgi.py, */settings* -branch = True +# branch = True [report] diff --git a/tests/extensions/tethysext-test_extension/.gitignore b/tests/extensions/tethysext-test_extension/.gitignore new file mode 100644 index 000000000..b0419c5d9 --- /dev/null +++ b/tests/extensions/tethysext-test_extension/.gitignore @@ -0,0 +1,9 @@ +*.pydevproject +*.project +*.egg-info +*.class +*.pyo +*.pyc +*.db +*.sqlite +*.DS_Store \ No newline at end of file diff --git a/tests/extensions/tethysext-test_extension/setup.py b/tests/extensions/tethysext-test_extension/setup.py new file mode 100644 index 000000000..4b2fe8662 --- /dev/null +++ b/tests/extensions/tethysext-test_extension/setup.py @@ -0,0 +1,36 @@ +import os +from setuptools import setup, find_packages +from tethys_apps.app_installation import find_resource_files + +# -- Extension Definition -- # +ext_package = 'test_extension' +release_package = 'tethysext-' + ext_package +ext_class = 'test_extension.ext:TestExtension' +ext_package_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tethysext', ext_package) + +# -- Python Dependencies -- # +dependencies = [] + +# -- Get Resource File -- # +resource_files = find_resource_files('tethysext/' + ext_package + '/templates') +resource_files += find_resource_files('tethysext/' + ext_package + '/public') + +setup( + name=release_package, + version='0.0.0', + description='', + long_description='', + keywords='', + author='', + author_email='', + url='', + license='', + packages=find_packages( + exclude=['ez_setup', 'examples', 'tethysext/' + ext_package + '/tests', 'tethysext/' + ext_package + '/tests.*'] + ), + package_data={'': resource_files}, + namespace_packages=['tethysext', 'tethysext.' + ext_package], + include_package_data=True, + zip_safe=False, + install_requires=dependencies, +) diff --git a/tests/extensions/tethysext-test_extension/tethysext/__init__.py b/tests/extensions/tethysext-test_extension/tethysext/__init__.py new file mode 100644 index 000000000..2e2033b3c --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/__init__.py @@ -0,0 +1,7 @@ +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/__init__.py b/tests/extensions/tethysext-test_extension/tethysext/test_extension/__init__.py new file mode 100644 index 000000000..2e2033b3c --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/__init__.py @@ -0,0 +1,7 @@ +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/controllers.py b/tests/extensions/tethysext-test_extension/tethysext/test_extension/controllers.py new file mode 100644 index 000000000..d23012d8c --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/controllers.py @@ -0,0 +1,15 @@ +from django.shortcuts import render +from django.contrib.auth.decorators import login_required + + +@login_required() +def home(request, var1, var2): + """ + Controller for the app home page. + """ + context = { + 'var1': var1, + 'var2': var2, + } + + return render(request, 'test_extension/home.html', context) diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/ext.py b/tests/extensions/tethysext-test_extension/tethysext/test_extension/ext.py new file mode 100644 index 000000000..f0aed3442 --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/ext.py @@ -0,0 +1,28 @@ +from tethys_sdk.base import TethysExtensionBase, url_map_maker + + +class TestExtension(TethysExtensionBase): + """ + Tethys extension class for Test Extension. + """ + + name = 'Test Extension' + package = 'test_extension' + root_url = 'test-extension' + description = 'Place a brief description of your extension here.' + + def url_maps(self): + """ + Add controllers + """ + UrlMap = url_map_maker(self.root_url) + + url_maps = ( + UrlMap( + name='home', + url='test-extension/{var1}/{var2}', + controller='test_extension.controllers.home' + ), + ) + + return url_maps diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/gizmos/__init__.py b/tests/extensions/tethysext-test_extension/tethysext/test_extension/gizmos/__init__.py new file mode 100644 index 000000000..e29ba2f84 --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/gizmos/__init__.py @@ -0,0 +1 @@ +from tethysext.test_extension.gizmos.custom_select_input import CustomSelectInput # noqa: F401 diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/gizmos/custom_select_input.py b/tests/extensions/tethysext-test_extension/tethysext/test_extension/gizmos/custom_select_input.py new file mode 100644 index 000000000..2d4c02207 --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/gizmos/custom_select_input.py @@ -0,0 +1,53 @@ +from tethys_sdk.gizmos import TethysGizmoOptions + + +class CustomSelectInput(TethysGizmoOptions): + """ + Custom select input gizmo. + """ + gizmo_name = 'custom_select_input' + + def __init__(self, name, display_text='', options=(), initial=(), multiselect=False, + disabled=False, error='', **kwargs): + """ + constructor + """ + # Initialize parent + super(CustomSelectInput, self).__init__(**kwargs) + + # Initialize Attributes + self.name = name + self.display_text = display_text + self.options = options + self.initial = initial + self.multiselect = multiselect + self.disabled = disabled + self.error = error + + @staticmethod + def get_vendor_js(): + """ + JavaScript vendor libraries. + """ + return ('https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js',) + + @staticmethod + def get_vendor_css(): + """ + CSS vendor libraries. + """ + return ('https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css',) + + @staticmethod + def get_gizmo_js(): + """ + JavaScript specific to gizmo. + """ + return ('test_extension/gizmos/custom_select_input/custom_select_input.js',) + + @staticmethod + def get_gizmo_css(): + """ + CSS specific to gizmo . + """ + return ('test_extension/gizmos/custom_select_input/custom_select_input.css',) diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/model.py b/tests/extensions/tethysext-test_extension/tethysext/test_extension/model.py new file mode 100644 index 000000000..770e9b4cb --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/model.py @@ -0,0 +1 @@ +# Put your persistent store models in this file diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/public/css/main.css b/tests/extensions/tethysext-test_extension/tethysext/test_extension/public/css/main.css new file mode 100644 index 000000000..e69de29bb diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/public/gizmos/custom_select_input/custom_select_input.css b/tests/extensions/tethysext-test_extension/tethysext/test_extension/public/gizmos/custom_select_input/custom_select_input.css new file mode 100644 index 000000000..3a95956e5 --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/public/gizmos/custom_select_input/custom_select_input.css @@ -0,0 +1,3 @@ +.select2 { + width: 100%; +} diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/public/gizmos/custom_select_input/custom_select_input.js b/tests/extensions/tethysext-test_extension/tethysext/test_extension/public/gizmos/custom_select_input/custom_select_input.js new file mode 100644 index 000000000..7cc1bba57 --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/public/gizmos/custom_select_input/custom_select_input.js @@ -0,0 +1,3 @@ +$(document).ready(function() { + $('.select2').select2(); +}); \ No newline at end of file diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/public/js/main.js b/tests/extensions/tethysext-test_extension/tethysext/test_extension/public/js/main.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/templates/gizmos/.gitkeep b/tests/extensions/tethysext-test_extension/tethysext/test_extension/templates/gizmos/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/templates/gizmos/custom_select_input.html b/tests/extensions/tethysext-test_extension/tethysext/test_extension/templates/gizmos/custom_select_input.html new file mode 100644 index 000000000..1fa36f9e2 --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/templates/gizmos/custom_select_input.html @@ -0,0 +1,28 @@ +{% load staticfiles %} + +
    + {% if display_text %} + + {% endif %} + + {% if error %} +

    {{ error }}

    + {% endif %} +
    diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/templates/test_extension/.gitkeep b/tests/extensions/tethysext-test_extension/tethysext/test_extension/templates/test_extension/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/templates/test_extension/home.html b/tests/extensions/tethysext-test_extension/tethysext/test_extension/templates/test_extension/home.html new file mode 100644 index 000000000..d4216dca9 --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/templates/test_extension/home.html @@ -0,0 +1,5 @@ +

    Hello, World!

    +
      +
    • {{ var1 }}
    • +
    • {{ var2 }}
    • +
    \ No newline at end of file diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/tests/__init__.py b/tests/extensions/tethysext-test_extension/tethysext/test_extension/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/extensions/tethysext-test_extension/tethysext/test_extension/tests/tests.py b/tests/extensions/tethysext-test_extension/tethysext/test_extension/tests/tests.py new file mode 100644 index 000000000..be2b5e263 --- /dev/null +++ b/tests/extensions/tethysext-test_extension/tethysext/test_extension/tests/tests.py @@ -0,0 +1,155 @@ +# Most of your test classes should inherit from TethysTestCase +from tethys_sdk.testing import TethysTestCase + +# Use if you'd like a simplified way to test rendered HTML templates. +# You likely need to install BeautifulSoup, as it is not included by default in Tethys Platform +# 1. Open a terminal +# 2. Enter command ". /usr/lib/tethys/bin/activate" to activate the Tethys python environment +# 3. Enter command "pip install beautifulsoup4" +# For help, see https://www.crummy.com/software/BeautifulSoup/bs4/doc/ +# from bs4 import BeautifulSoup + +""" +To run any tests: + 1. Open a terminal + 2. Enter command "t" to activate the Tethys python environment + 3. In settings.py make sure that the tethys_default database user is set to tethys_super or is a super user of the database + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'tethys_default', + 'USER': 'tethys_super', + 'PASSWORD': 'pass', + 'HOST': '127.0.0.1', + 'PORT': '5435' + } + } + 4. Enter tethys test command. + The general form is: "tethys test -f tethysext....." + See below for specific examples + + To run all tests across this extension: + Test command: "tethys test -f tethysext.test_extension" + + To run all tests in this file: + Test command: "tethys test -f tethysext.test_extension.tests.tests" + + To run tests in the TestExtensionTestCase class: + Test command: "tethys test -f tethysext.test_extension.tests.tests.TestExtensionTestCase" + + To run only the test_if_tethys_platform_is_great function in the TestExtensionTestCase class: + Test command: "tethys test -f tethysext.test_extension.tests.tests.TestExtensionTestCase.test_if_tethys_platform_is_great" + +To learn more about writing tests, see: + https://docs.djangoproject.com/en/1.9/topics/testing/overview/#writing-tests + https://docs.python.org/2.7/library/unittest.html#module-unittest +""" # noqa: E501 + + +class TestExtensionTestCase(TethysTestCase): + """ + In this class you may define as many functions as you'd like to test different aspects of your extension. + Each function must start with the word "test" for it to be recognized and executed during testing. + You could also create multiple TethysTestCase classes within this or other python files to organize your tests. + """ + + def set_up(self): + """ + This function is not required, but can be used if any environmental setup needs to take place before + execution of each test function. Thus, if you have multiple test that require the same setup to run, + place that code here. + + If you are testing against a controller that check for certain user info, you can create a fake test user and + get a test client, like so: + + #The test client simulates a browser that can navigate your extension's url endpoints + self.c = self.get_test_client() + self.user = self.create_test_user(username="joe", password="secret", email="joe@some_site.com") + # To create a super_user, use "self.create_test_superuser(*params)" with the same params + + # To force a login for the test user + self.c.force_login(self.user) + + # If for some reason you do not want to force a login, you can use the following: + login_success = self.c.login(username="joe", password="secret") + + NOTE: You do not have place these functions here, but if they are not placed here and are needed + then they must be placed at the beginning of your individual test functions. Also, if a certain + setup does not apply to all of your functions, you should either place it directly in each + function it applies to, or maybe consider creating a new test file or test class to group similar + tests. + """ + pass + + def tear_down(self): + """ + This function is not required, but should be used if you need to tear down any environmental setup + that took place before execution of the test functions. + + NOTE: You do not have to set these functions up here, but if they are not placed here and are needed + then they must be placed at the very end of your individual test functions. Also, if certain + tearDown code does not apply to all of your functions, you should either place it directly in each + function it applies to, or maybe consider creating a new test file or test class to group similar + tests. + """ + pass + + def is_tethys_platform_great(self): + return True + + def test_if_tethys_platform_is_great(self): + """ + This is an example test function that can be modified to test a specific aspect of your extension. + It is required that the function name begins with the word "test" or it will not be executed. + Generally, the code written here will consist of many assert methods. + A list of assert methods is included here for reference or to get you started: + assertEqual(a, b) a == b + assertNotEqual(a, b) a != b + assertTrue(x) bool(x) is True + assertFalse(x) bool(x) is False + assertIs(a, b) a is b + assertIsNot(a, b) a is not b + assertIsNone(x) x is None + assertIsNotNone(x) x is not None + assertIn(a, b) a in b + assertNotIn(a, b) a not in b + assertIsInstance(a, b) isinstance(a, b) + assertNotIsInstance(a, b) !isinstance(a, b) + Learn more about assert methods here: + https://docs.python.org/2.7/library/unittest.html#assert-methods + """ + + self.assertEqual(self.is_tethys_platform_great(), True) + self.assertNotEqual(self.is_tethys_platform_great(), False) + self.assertTrue(self.is_tethys_platform_great()) + self.assertFalse(not self.is_tethys_platform_great()) + self.assertIs(self.is_tethys_platform_great(), True) + self.assertIsNot(self.is_tethys_platform_great(), False) + + def test_a_controller(self): + """ + This is an example test function of how you might test a controller that returns an HTML template rendered + with context variables. + """ + + # If all test functions were testing controllers or required a test client for another reason, the following + # 3 lines of code could be placed once in the set_up function. Note that in that case, each variable should be + # prepended with "self." (i.e. self.c = ...) to make those variables "global" to this test class and able to be + # used in each separate test function. + c = self.get_test_client() + user = self.create_test_user(username="joe", password="secret", email="joe@some_site.com") + c.force_login(user) + + # Have the test client "browse" to your page + response = c.get('/extensions/test-extension/foo/') # The final '/' is essential for all pages/controllers + + # Test that the request processed correctly (with a 200 status code) + self.assertEqual(response.status_code, 200) + + ''' + NOTE: Next, you would likely test that your context variables returned as expected. That would look + something like the following: + ''' + + context = response.context + self.assertEqual(context['my_integer'], 10) diff --git a/tests/factories/__init__.py b/tests/factories/__init__.py new file mode 100644 index 000000000..504f3f581 --- /dev/null +++ b/tests/factories/__init__.py @@ -0,0 +1,8 @@ +""" +******************************************************************************** +* Name: __init__.py +* Author: nswain +* Created On: May 15, 2018 +* Copyright: (c) Aquaveo 2018 +******************************************************************************** +""" diff --git a/tests/factories/django_user.py b/tests/factories/django_user.py new file mode 100644 index 000000000..862223e14 --- /dev/null +++ b/tests/factories/django_user.py @@ -0,0 +1,40 @@ +""" +******************************************************************************** +* Name: django_user +* Author: nswain +* Created On: May 15, 2018 +* Copyright: (c) Aquaveo 2018 +******************************************************************************** +""" +import datetime +from hashlib import md5 +import factory +from django.contrib.auth.models import User + + +class UserFactory(factory.Factory): + """ + Creates a new ``User`` object. + Username will be a random 30 character md5 value. + Email will be ``userN@example.com`` with ``N`` being a counter. + Password will be ``test123`` by default. + """ + class Meta: + model = User + abstract = False + + username = factory.LazyAttribute( + lambda x: md5(datetime.datetime.now().strftime('%Y%,%d%H%M%S').encode('utf-8')).hexdigest()[0:30] + ) + email = factory.Sequence(lambda n: 'user{0}@example.com'.format(n)) + + @classmethod + def _prepare(cls, create, **kwargs): + password = 'test123' + if 'password' in kwargs: + password = kwargs.pop('password') + user = super(UserFactory, cls).prepare(create, **kwargs) + user.set_password(password) + if create: + user.save() + return user diff --git a/tests/gui_tests/test_tethys_portal/test_authentication.py b/tests/gui_tests/test_tethys_portal/test_authentication.py index 0dfe60720..fd593966d 100644 --- a/tests/gui_tests/test_tethys_portal/test_authentication.py +++ b/tests/gui_tests/test_tethys_portal/test_authentication.py @@ -2,6 +2,7 @@ from selenium.webdriver.firefox.webdriver import WebDriver from django.contrib.auth.models import User + class AuthenticationTests(StaticLiveServerTestCase): @classmethod @@ -25,4 +26,4 @@ def test_login(self): username_input.send_keys(self.user.username) password_input = self.selenium.find_element_by_name("password") password_input.send_keys(self.user_pass) - self.selenium.find_element_by_name('login-submit').click() \ No newline at end of file + self.selenium.find_element_by_name('login-submit').click() diff --git a/tests/intermediate_tests/__init__.py b/tests/intermediate_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/intermediate_tests/test_tethys_services/__init__.py b/tests/intermediate_tests/test_tethys_services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/intermediate_tests/test_tethys_services/test_backends/__init__.py b/tests/intermediate_tests/test_tethys_services/test_backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_services/test_hydroshare_backend.py b/tests/intermediate_tests/test_tethys_services/test_backends/test_hydroshare.py similarity index 94% rename from tests/unit_tests/test_tethys_services/test_hydroshare_backend.py rename to tests/intermediate_tests/test_tethys_services/test_backends/test_hydroshare.py index a41a18a3b..5e38d25c7 100644 --- a/tests/unit_tests/test_tethys_services/test_hydroshare_backend.py +++ b/tests/intermediate_tests/test_tethys_services/test_backends/test_hydroshare.py @@ -42,12 +42,12 @@ def setUp(self): self.access_token = str(uuid.uuid4()) self.refresh_token = str(uuid.uuid4()) - self.expires_in = random.randint(1, 30*60*60) # 1 sec to 30 days + self.expires_in = random.randint(1, 30 * 60 * 60) # 1 sec to 30 days self.token_type = "bearer" self.scope = "read write" - self.social_username="drew" - self.social_email="drew@byu.edu" + self.social_username = "drew" + self.social_email = "drew@byu.edu" def tearDown(self): pass @@ -58,7 +58,7 @@ def test_oauth_create_new_user(self, m): # expect for only 1 user: anonymous user self.assertEqual(User.objects.all().count(), 1) - username_new, social, backend=self.run_oauth(m) + username_new, social, backend = self.run_oauth(m) # expect for 2 users: anonymous and newly created social user self.assertEqual(User.objects.all().count(), 2) @@ -113,9 +113,11 @@ def test_oauth_connection_to_user(self, m): # expect for only 1 user: anonymous user self.assertEqual(User.objects.all().count(), 1) # manually create a new user named self.social_username - user_sherry = User.objects.create_user(username="sherry", - email="sherry@byu.edu", - password='top_secret') + user_sherry = User.objects.create_user( + username="sherry", + email="sherry@byu.edu", + password='top_secret' + ) logger.debug(user_sherry.is_authenticated()) logger.debug(user_sherry.is_active) diff --git a/tests/unit_tests/test_tethys_apps/test_admin.py b/tests/unit_tests/test_tethys_apps/test_admin.py new file mode 100644 index 000000000..094d19418 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_admin.py @@ -0,0 +1,166 @@ +import unittest +import mock + +from tethys_apps.admin import TethysAppSettingInline, CustomSettingInline, DatasetServiceSettingInline, \ + SpatialDatasetServiceSettingInline, WebProcessingServiceSettingInline, PersistentStoreConnectionSettingInline, \ + PersistentStoreDatabaseSettingInline, TethysAppAdmin, TethysExtensionAdmin + +from tethys_apps.models import (TethysApp, + TethysExtension, + CustomSetting, + DatasetServiceSetting, + SpatialDatasetServiceSetting, + WebProcessingServiceSetting, + PersistentStoreConnectionSetting, + PersistentStoreDatabaseSetting) + + +class TestTethysAppAdmin(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_TethysAppSettingInline(self): + expected_template = 'tethys_portal/admin/edit_inline/tabular.html' + TethysAppSettingInline.model = mock.MagicMock() + ret = TethysAppSettingInline(mock.MagicMock(), mock.MagicMock()) + self.assertEquals(expected_template, ret.template) + + def test_has_delete_permission(self): + TethysAppSettingInline.model = mock.MagicMock() + ret = TethysAppSettingInline(mock.MagicMock(), mock.MagicMock()) + self.assertFalse(ret.has_delete_permission(mock.MagicMock())) + + def test_has_add_permission(self): + TethysAppSettingInline.model = mock.MagicMock() + ret = TethysAppSettingInline(mock.MagicMock(), mock.MagicMock()) + self.assertFalse(ret.has_add_permission(mock.MagicMock())) + + def test_CustomSettingInline(self): + expected_readonly_fields = ('name', 'description', 'type', 'required') + expected_fields = ('name', 'description', 'type', 'value', 'required') + expected_model = CustomSetting + + ret = CustomSettingInline(mock.MagicMock(), mock.MagicMock()) + + self.assertEquals(expected_readonly_fields, ret.readonly_fields) + self.assertEquals(expected_fields, ret.fields) + self.assertEquals(expected_model, ret.model) + + def test_DatasetServiceSettingInline(self): + expected_readonly_fields = ('name', 'description', 'required', 'engine') + expected_fields = ('name', 'description', 'dataset_service', 'engine', 'required') + expected_model = DatasetServiceSetting + + ret = DatasetServiceSettingInline(mock.MagicMock(), mock.MagicMock()) + + self.assertEquals(expected_readonly_fields, ret.readonly_fields) + self.assertEquals(expected_fields, ret.fields) + self.assertEquals(expected_model, ret.model) + + def test_SpatialDatasetServiceSettingInline(self): + expected_readonly_fields = ('name', 'description', 'required', 'engine') + expected_fields = ('name', 'description', 'spatial_dataset_service', 'engine', 'required') + expected_model = SpatialDatasetServiceSetting + + ret = SpatialDatasetServiceSettingInline(mock.MagicMock(), mock.MagicMock()) + + self.assertEquals(expected_readonly_fields, ret.readonly_fields) + self.assertEquals(expected_fields, ret.fields) + self.assertEquals(expected_model, ret.model) + + def test_WebProcessingServiceSettingInline(self): + expected_readonly_fields = ('name', 'description', 'required') + expected_fields = ('name', 'description', 'web_processing_service', 'required') + expected_model = WebProcessingServiceSetting + + ret = WebProcessingServiceSettingInline(mock.MagicMock(), mock.MagicMock()) + + self.assertEquals(expected_readonly_fields, ret.readonly_fields) + self.assertEquals(expected_fields, ret.fields) + self.assertEquals(expected_model, ret.model) + + def test_PersistentStoreConnectionSettingInline(self): + expected_readonly_fields = ('name', 'description', 'required') + expected_fields = ('name', 'description', 'persistent_store_service', 'required') + expected_model = PersistentStoreConnectionSetting + + ret = PersistentStoreConnectionSettingInline(mock.MagicMock(), mock.MagicMock()) + + self.assertEquals(expected_readonly_fields, ret.readonly_fields) + self.assertEquals(expected_fields, ret.fields) + self.assertEquals(expected_model, ret.model) + + def test_PersistentStoreDatabaseSettingInline(self): + expected_readonly_fields = ('name', 'description', 'required', 'spatial', 'initialized') + expected_fields = ('name', 'description', 'spatial', 'initialized', 'persistent_store_service', 'required') + expected_model = PersistentStoreDatabaseSetting + + ret = PersistentStoreDatabaseSettingInline(mock.MagicMock(), mock.MagicMock()) + + self.assertEquals(expected_readonly_fields, ret.readonly_fields) + self.assertEquals(expected_fields, ret.fields) + self.assertEquals(expected_model, ret.model) + + # Need to check + def test_PersistentStoreDatabaseSettingInline_get_queryset(self): + obj = PersistentStoreDatabaseSettingInline(mock.MagicMock(), mock.MagicMock()) + mock_request = mock.MagicMock() + obj.get_queryset(mock_request) + + def test_TethysAppAdmin(self): + expected_readonly_fields = ('package',) + expected_fields = ('package', 'name', 'description', 'tags', 'enabled', 'show_in_apps_library', + 'enable_feedback') + expected_inlines = [CustomSettingInline, + PersistentStoreConnectionSettingInline, + PersistentStoreDatabaseSettingInline, + DatasetServiceSettingInline, + SpatialDatasetServiceSettingInline, + WebProcessingServiceSettingInline] + + ret = TethysAppAdmin(mock.MagicMock(), mock.MagicMock()) + + self.assertEquals(expected_readonly_fields, ret.readonly_fields) + self.assertEquals(expected_fields, ret.fields) + self.assertEquals(expected_inlines, ret.inlines) + + def test_TethysAppAdmin_has_delete_permission(self): + ret = TethysAppAdmin(mock.MagicMock(), mock.MagicMock()) + self.assertFalse(ret.has_delete_permission(mock.MagicMock())) + + def test_TethysAppAdmin_has_add_permission(self): + ret = TethysAppAdmin(mock.MagicMock(), mock.MagicMock()) + self.assertFalse(ret.has_add_permission(mock.MagicMock())) + + def test_TethysExtensionAdmin(self): + expected_readonly_fields = ('package', 'name', 'description') + expected_fields = ('package', 'name', 'description', 'enabled') + + ret = TethysExtensionAdmin(mock.MagicMock(), mock.MagicMock()) + + self.assertEquals(expected_readonly_fields, ret.readonly_fields) + self.assertEquals(expected_fields, ret.fields) + + def test_TethysExtensionAdmin_has_delete_permission(self): + ret = TethysExtensionAdmin(mock.MagicMock(), mock.MagicMock()) + self.assertFalse(ret.has_delete_permission(mock.MagicMock())) + + def test_TethysExtensionAdmin_has_add_permission(self): + ret = TethysExtensionAdmin(mock.MagicMock(), mock.MagicMock()) + self.assertFalse(ret.has_add_permission(mock.MagicMock())) + + def test_admin_site_register_tethys_app_admin(self): + from django.contrib import admin + registry = admin.site._registry + self.assertIn(TethysApp, registry) + self.assertIsInstance(registry[TethysApp], TethysAppAdmin) + + def test_admin_site_register_tethys_app_extension(self): + from django.contrib import admin + registry = admin.site._registry + self.assertIn(TethysExtension, registry) + self.assertIsInstance(registry[TethysExtension], TethysExtensionAdmin) diff --git a/tests/unit_tests/test_tethys_apps/test_app_installation.py b/tests/unit_tests/test_tethys_apps/test_app_installation.py new file mode 100644 index 000000000..75467cfd2 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_app_installation.py @@ -0,0 +1,290 @@ +import unittest +import mock +import os +import sys +import tethys_apps.app_installation as tethys_app_installation + +if sys.version_info[0] < 3: + callable_mock_path = '__builtin__.callable' +else: + callable_mock_path = 'builtins.callable' + + +class TestAppInstallation(unittest.TestCase): + def setUp(self): + self.src_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + self.root = os.path.join(self.src_dir, 'tethys_apps', 'tethysapp', 'test_app', 'public') + + def tearDown(self): + pass + + def test_find_resource_files(self): + ret = tethys_app_installation.find_resource_files(self.root) + main_js = False + icon_gif = False + main_css = False + if any('/js/main.js' in s for s in ret): + main_js = True + if any('/images/icon.gif' in s for s in ret): + icon_gif = True + if any('/css/main.css' in s for s in ret): + main_css = True + + self.assertTrue(main_js) + self.assertTrue(icon_gif) + self.assertTrue(main_css) + + def test_get_tethysapp_directory(self): + ret = tethys_app_installation.get_tethysapp_directory() + self.assertIn('tethys_apps/tethysapp', ret) + + @mock.patch('tethys_apps.app_installation.install') + @mock.patch('tethys_apps.app_installation.subprocess') + @mock.patch('tethys_apps.app_installation.get_tethysapp_directory') + @mock.patch('tethys_apps.app_installation.shutil') + @mock.patch('tethys_apps.app_installation.pretty_output') + def test__run_install(self, mock_pretty_output, mock_shutil, mock_getdir, mock_subprocess, mock_install): + # mock the self input + mock_self = mock.MagicMock(app_package='tethys_apps', app_package_dir='/test_app/', dependencies='foo') + + # call the method for testing + tethys_app_installation._run_install(self=mock_self) + + # check the method call + mock_getdir.assert_called() + + # check the user notification + mock_pretty_output.assert_called() + + # check the input arguments for shutil.copytree method + shutil_call_args = mock_shutil.copytree.call_args_list + self.assertEquals('/test_app/', shutil_call_args[0][0][0]) + + # check the input arguments for subprocess.call method + process_call_args = mock_subprocess.call.call_args_list + self.assertEquals('pip', process_call_args[0][0][0][0]) + self.assertEquals('install', process_call_args[0][0][0][1]) + self.assertEquals('f', process_call_args[0][0][0][2]) + + # check the install call + mock_install.run.assert_called_with(mock_self) + + @mock.patch('tethys_apps.app_installation.develop') + @mock.patch('tethys_apps.app_installation.subprocess') + @mock.patch('tethys_apps.app_installation.os') + @mock.patch(callable_mock_path) + @mock.patch('tethys_apps.app_installation.pretty_output') + @mock.patch('tethys_apps.app_installation.get_tethysapp_directory') + def test__run_develop(self, mock_getdir, mock_pretty_output, mock_callable, mock_os, mock_subprocess, + mock_develop): + + # mock the self input + mock_self = mock.MagicMock(app_package='tethys_apps', app_package_dir='/test_app/', dependencies='foo') + + # call the method for testing + tethys_app_installation._run_develop(self=mock_self) + + # check the method call + mock_getdir.assert_called() + + # check the user notification + mock_pretty_output.assert_called() + + # mock callable method + mock_callable.return_value = True + + # check the input arguments for os.symlink method + symlink_call_args = mock_os.symlink.call_args_list + self.assertEquals('/test_app/', symlink_call_args[0][0][0]) + + # check the input arguments for subprocess.call method + process_call_args = mock_subprocess.call.call_args_list + self.assertEquals('pip', process_call_args[0][0][0][0]) + self.assertEquals('install', process_call_args[0][0][0][1]) + self.assertEquals('f', process_call_args[0][0][0][2]) + + # check the develop call + mock_develop.run.assert_called_with(mock_self) + + def test_custom_install_command(self): + app_package = 'tethys_apps' + app_package_dir = '/test_app/' + dependencies = 'foo' + + ret = tethys_app_installation.custom_install_command(app_package, app_package_dir, dependencies) + + self.assertEquals('tethys_apps', ret.app_package) + self.assertEquals('/test_app/', ret.app_package_dir) + self.assertEquals('foo', ret.dependencies) + self.assertEquals('tethys_apps.app_installation', ret.__module__) + + def test_custom_develop_command(self): + app_package = 'tethys_apps1' + app_package_dir = '/test_app/' + dependencies = 'foo' + + ret = tethys_app_installation.custom_develop_command(app_package, app_package_dir, dependencies) + + self.assertEquals('tethys_apps1', ret.app_package) + self.assertEquals('/test_app/', ret.app_package_dir) + self.assertEquals('foo', ret.dependencies) + self.assertEquals('tethys_apps.app_installation', ret.__module__) + + @mock.patch('tethys_apps.app_installation.shutil.copytree') + @mock.patch('tethys_apps.app_installation.install') + @mock.patch('tethys_apps.app_installation.subprocess') + @mock.patch('tethys_apps.app_installation.get_tethysapp_directory') + @mock.patch('tethys_apps.app_installation.shutil') + @mock.patch('tethys_apps.app_installation.pretty_output') + def test__run_install_exception(self, mock_pretty_output, mock_shutil, mock_getdir, mock_subprocess, mock_install, + mock_copy_tree): + # mock the self input + mock_self = mock.MagicMock(app_package='tethys_apps', app_package_dir='/test_app/', dependencies='foo') + + mock_copy_tree.side_effect = Exception, True + + # call the method for testing + tethys_app_installation._run_install(self=mock_self) + + # check the method call + mock_getdir.assert_called() + + # check the user notification + mock_pretty_output.assert_called() + + # check the input arguments for shutil.copytree method + shutil_call_args = mock_shutil.copytree.call_args_list + self.assertEquals('/test_app/', shutil_call_args[0][0][0]) + + mock_shutil.rmtree.assert_called() + + mock_shutil.copytree.assert_called() + + # check the input arguments for subprocess.call method + process_call_args = mock_subprocess.call.call_args_list + self.assertEquals('pip', process_call_args[0][0][0][0]) + self.assertEquals('install', process_call_args[0][0][0][1]) + self.assertEquals('f', process_call_args[0][0][0][2]) + + # check the install call + mock_install.run.assert_called_with(mock_self) + + @mock.patch('tethys_apps.app_installation.os.remove') + @mock.patch('tethys_apps.app_installation.shutil.rmtree') + @mock.patch('tethys_apps.app_installation.shutil.copytree') + @mock.patch('tethys_apps.app_installation.install') + @mock.patch('tethys_apps.app_installation.subprocess') + @mock.patch('tethys_apps.app_installation.get_tethysapp_directory') + @mock.patch('tethys_apps.app_installation.shutil') + @mock.patch('tethys_apps.app_installation.pretty_output') + def test__run_install_exception_rm_tree_exception(self, mock_pretty_output, mock_shutil, mock_getdir, + mock_subprocess, mock_install, mock_copy_tree, mock_remove_tree, + mock_os_remove_tree): + # mock the self input + mock_self = mock.MagicMock(app_package='tethys_apps', app_package_dir='/test_app/', dependencies='foo') + + mock_copy_tree.side_effect = Exception, True + + mock_remove_tree.side_effect = Exception + + # call the method for testing + tethys_app_installation._run_install(self=mock_self) + + # check the method call + mock_getdir.assert_called() + + # check the user notification + mock_pretty_output.assert_called() + + # check the input arguments for shutil.copytree method + shutil_call_args = mock_shutil.copytree.call_args_list + self.assertEquals('/test_app/', shutil_call_args[0][0][0]) + + mock_os_remove_tree.assert_called() + + mock_shutil.copytree.assert_called() + + # check the input arguments for subprocess.call method + process_call_args = mock_subprocess.call.call_args_list + self.assertEquals('pip', process_call_args[0][0][0][0]) + self.assertEquals('install', process_call_args[0][0][0][1]) + self.assertEquals('f', process_call_args[0][0][0][2]) + + # check the install call + mock_install.run.assert_called_with(mock_self) + + @mock.patch('tethys_apps.app_installation.ctypes') + @mock.patch('tethys_apps.app_installation.os.path.isdir') + @mock.patch(callable_mock_path) + @mock.patch('tethys_apps.app_installation.pretty_output') + @mock.patch('tethys_apps.app_installation.get_tethysapp_directory') + def test_run_develop_windows(self, mock_getdir, mock_pretty_output, mock_callable, mock_os_path_isdir, mock_ctypes): + # mock the self input + mock_self = mock.MagicMock(app_package='tethys_apps', app_package_dir='/test_app/', dependencies='foo') + + # mock callable method + mock_callable.return_value = False + + mock_csl = mock.MagicMock(argtypes=mock.MagicMock(), restype=mock.MagicMock()) + + mock_ctypes.windll.kernel32.CreateSymbolicLinkW = mock_csl + + mock_os_path_isdir.return_value = True + + mock_csl.return_value = 0 + mock_ctypes.WinError = Exception + + # call the method for testing + self.assertRaises(Exception, tethys_app_installation._run_develop, mock_self) + + # check the method call + mock_getdir.assert_called() + + # check the user notification + mock_pretty_output.assert_called() + + @mock.patch('tethys_apps.app_installation.shutil.rmtree') + @mock.patch('tethys_apps.app_installation.getattr') + @mock.patch('tethys_apps.app_installation.develop') + @mock.patch('tethys_apps.app_installation.subprocess') + @mock.patch('tethys_apps.app_installation.os') + @mock.patch('tethys_apps.app_installation.pretty_output') + @mock.patch('tethys_apps.app_installation.get_tethysapp_directory') + def test__run_develop_exception(self, mock_getdir, mock_pretty_output, mock_os, mock_subprocess, + mock_develop, mock_getattr, mock_rm_tree): + + mock_destination = mock.MagicMock() + mock_os.path.join.return_value = mock_destination + + # mock the self input + mock_self = mock.MagicMock(app_package='tethys_apps', app_package_dir='/test_app/', dependencies='foo') + + mock_getattr.side_effect = Exception + + mock_rm_tree.side_effect = Exception + + # call the method for testing + tethys_app_installation._run_develop(self=mock_self) + + # check the method call + mock_getdir.assert_called() + + # check the user notification + mock_pretty_output.assert_called() + + mock_rm_tree.assert_called_with(mock_destination) + + mock_os.remove.assert_called_with(mock_destination) + + # check the input arguments for os.symlink method + symlink_call_args = mock_os.symlink.call_args_list + self.assertEquals('/test_app/', symlink_call_args[0][0][0]) + + # check the input arguments for subprocess.call method + process_call_args = mock_subprocess.call.call_args_list + self.assertEquals('pip', process_call_args[0][0][0][0]) + self.assertEquals('install', process_call_args[0][0][0][1]) + self.assertEquals('f', process_call_args[0][0][0][2]) + + # check the develop call + mock_develop.run.assert_called_with(mock_self) diff --git a/tests/unit_tests/test_tethys_apps/test_apps.py b/tests/unit_tests/test_tethys_apps/test_apps.py new file mode 100644 index 000000000..8b94da330 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_apps.py @@ -0,0 +1,22 @@ +import unittest +import mock +import tethys_apps +from tethys_apps.apps import TethysAppsConfig + + +class TestApps(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_TethysAppsConfig(self): + self.assertEqual('tethys_apps', TethysAppsConfig.name) + self.assertEqual('Tethys Apps', TethysAppsConfig.verbose_name) + + @mock.patch('tethys_apps.apps.SingletonHarvester') + def test_ready(self, mock_singleton_harvester): + tethys_app_config_obj = TethysAppsConfig('tethys_apps', tethys_apps) + tethys_app_config_obj.ready() + mock_singleton_harvester().harvest.assert_called() diff --git a/tests/unit_tests/test_tethys_apps/test_base/__init__.py b/tests/unit_tests/test_tethys_apps/test_base/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py b/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py new file mode 100644 index 000000000..6a51abb3a --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_base/test_app_base.py @@ -0,0 +1,903 @@ +import unittest +import tethys_apps.base.app_base as tethys_app_base +import mock + +from django.test import RequestFactory +from tests.factories.django_user import UserFactory +from django.core.exceptions import ObjectDoesNotExist +from tethys_apps.exceptions import TethysAppSettingDoesNotExist, TethysAppSettingNotAssigned +from types import FunctionType +from tethys_apps.base.permissions import Permission, PermissionGroup + + +class TethysAppChild(tethys_app_base.TethysAppBase): + """ + Tethys app class for Test App. + """ + + name = 'Test App' + index = 'test_app:home' + icon = 'test_app/images/icon.gif' + package = 'test_app' + root_url = 'test-app' + color = '#2c3e50' + description = 'Place a brief description of your app here.' + + +class TestTethysBase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_url_maps(self): + result = tethys_app_base.TethysBase().url_maps() + self.assertEqual([], result) + + @mock.patch('tethys_apps.base.app_base.url') + @mock.patch('tethys_apps.base.app_base.TethysBaseMixin') + def test_url_patterns(self, mock_tbm, mock_url): + app = tethys_app_base.TethysBase() + app._namespace = 'foo' + url_map = mock.MagicMock(controller='test_app.controllers.home', url='test-url') + url_map.name = 'home' + app.url_maps = mock.MagicMock(return_value=[url_map]) + mock_tbm.return_value = mock.MagicMock(url_maps='test-app') + + # Execute + result = app.url_patterns + # Check url call at django_url = url... + rts_call_args = mock_url.call_args_list + self.assertEqual('test-url', rts_call_args[0][0][0]) + self.assertIn('name', rts_call_args[0][1]) + self.assertEqual('home', rts_call_args[0][1]['name']) + self.assertIn('foo', result) + self.assertIsInstance(rts_call_args[0][0][1], FunctionType) + + @mock.patch('tethys_apps.base.app_base.url') + @mock.patch('tethys_apps.base.app_base.TethysBaseMixin') + def test_url_patterns_no_basestring(self, mock_tbm, mock_url): + app = tethys_app_base.TethysBase() + # controller_mock = mock.MagicMock() + + def test_func(): + return '' + + url_map = mock.MagicMock(controller=test_func, url='test-app') + url_map.name = 'home' + app.url_maps = mock.MagicMock(return_value=[url_map]) + mock_tbm.return_value = mock.MagicMock(url_maps='test-app') + + # Execute + app.url_patterns + + # Check url call at django_url = url... + rts_call_args = mock_url.call_args_list + self.assertEqual('test-app', rts_call_args[0][0][0]) + self.assertIn('name', rts_call_args[0][1]) + self.assertEqual('home', rts_call_args[0][1]['name']) + self.assertIs(rts_call_args[0][0][1], test_func) + + @mock.patch('tethys_apps.base.app_base.tethys_log') + @mock.patch('tethys_apps.base.app_base.TethysBaseMixin') + def test_url_patterns_import_error(self, mock_tbm, mock_log): + mock_error = mock_log.error + app = tethys_app_base.TethysBase() + url_map = mock.MagicMock(controller='1module.1function', url='test-app') + url_map.name = 'home' + app.url_maps = mock.MagicMock(return_value=[url_map]) + mock_tbm.return_value = mock.MagicMock(url_maps='test-app') + + # assertRaises needs a callable, not a property + def test_url_patterns(): + return app.url_patterns + + # Check Error Message + self.assertRaises(ImportError, test_url_patterns) + rts_call_args = mock_error.call_args_list + error_message = 'The following error occurred while trying to import' \ + ' the controller function "1module.1function"' + self.assertIn(error_message, rts_call_args[0][0][0]) + + @mock.patch('tethys_apps.base.app_base.tethys_log') + @mock.patch('tethys_apps.base.app_base.TethysBaseMixin') + def test_url_patterns_attribute_error(self, mock_tbm, mock_log): + mock_error = mock_log.error + app = tethys_app_base.TethysBase() + url_map = mock.MagicMock(controller='test_app.controllers.home1', url='test-app') + url_map.name = 'home' + app.url_maps = mock.MagicMock(return_value=[url_map]) + mock_tbm.return_value = mock.MagicMock(url_maps='test-app') + + # assertRaises needs a callable, not a property + def test_url_patterns(): + return app.url_patterns + + # Check Error Message + self.assertRaises(AttributeError, test_url_patterns) + rts_call_args = mock_error.call_args_list + error_message = 'The following error occurred while trying to access' \ + ' the controller function "test_app.controllers.home1"' + self.assertIn(error_message, rts_call_args[0][0][0]) + + def test_sync_with_tethys_db(self): + self.assertRaises(NotImplementedError, tethys_app_base.TethysBase().sync_with_tethys_db) + + def test_remove_from_db(self): + self.assertRaises(NotImplementedError, tethys_app_base.TethysBase().remove_from_db) + + +class TestTethysExtensionBase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test__unicode__(self): + result = tethys_app_base.TethysExtensionBase().__unicode__() + self.assertEqual('', result) + + def test__repr__(self): + result = tethys_app_base.TethysExtensionBase().__repr__() + self.assertEqual('', result) + + def test_url_maps(self): + result = tethys_app_base.TethysExtensionBase().url_maps() + self.assertEqual([], result) + + @mock.patch('tethys_apps.models.TethysExtension') + def test_sync_with_tethys_db(self, mock_te): + mock_te.objects.filter().all.return_value = [] + + tethys_app_base.TethysExtensionBase().sync_with_tethys_db() + + mock_te.assert_called_with(description='', name='', package='', root_url='') + mock_te().save.assert_called() + + @mock.patch('django.conf.settings') + @mock.patch('tethys_apps.models.TethysExtension') + def test_sync_with_tethys_db_exists(self, mock_te, mock_ds): + mock_ds.DEBUG = True + ext = tethys_app_base.TethysExtensionBase() + ext.root_url = 'test_url' + mock_te2 = mock.MagicMock() + mock_te.objects.filter().all.return_value = [mock_te2] + ext.sync_with_tethys_db() + + # Check_result + self.assertTrue(mock_te2.save.call_count == 2) + + @mock.patch('tethys_apps.base.app_base.tethys_log') + @mock.patch('tethys_apps.models.TethysExtension') + def test_sync_with_tethys_db_exists_log_error(self, mock_te, mock_log): + mock_error = mock_log.error + ext = tethys_app_base.TethysExtensionBase() + ext.root_url = 'test_url' + mock_te.objects.filter().all.side_effect = Exception('test_error') + ext.sync_with_tethys_db() + + # Check_result + rts_call_args = mock_error.call_args_list + self.assertEqual('test_error', rts_call_args[0][0][0].args[0]) + + +class TestTethysAppBase(unittest.TestCase): + def setUp(self): + self.app = tethys_app_base.TethysAppBase() + self.user = UserFactory() + self.request_factory = RequestFactory() + self.fake_name = 'fake_name' + + def tearDown(self): + pass + + def test__unicode__(self): + result = tethys_app_base.TethysAppBase().__unicode__() + self.assertEqual('', result) + + def test__repr__(self): + result = tethys_app_base.TethysAppBase().__repr__() + self.assertEqual('', result) + + def test_custom_settings(self): + self.assertIsNone(tethys_app_base.TethysAppBase().custom_settings()) + + def test_persistent_store_settings(self): + self.assertIsNone(tethys_app_base.TethysAppBase().persistent_store_settings()) + + def test_dataset_service_settings(self): + self.assertIsNone(tethys_app_base.TethysAppBase().dataset_service_settings()) + + def test_spatial_dataset_service_settings(self): + self.assertIsNone(tethys_app_base.TethysAppBase().spatial_dataset_service_settings()) + + def test_web_processing_service_settings(self): + self.assertIsNone(tethys_app_base.TethysAppBase().web_processing_service_settings()) + + def test_handoff_handlers(self): + self.assertIsNone(tethys_app_base.TethysAppBase().handoff_handlers()) + + def test_permissions(self): + self.assertIsNone(tethys_app_base.TethysAppBase().permissions()) + + @mock.patch('guardian.shortcuts.get_perms') + @mock.patch('guardian.shortcuts.remove_perm') + @mock.patch('guardian.shortcuts.assign_perm') + @mock.patch('tethys_apps.models.TethysApp') + @mock.patch('django.contrib.auth.models.Group') + @mock.patch('django.contrib.auth.models.Permission') + def test_register_app_permissions(self, mock_dp, mock_dg, mock_ta, mock_asg, mock_rem, mock_get): + group_name = 'test_group' + create_test_perm = Permission(name='create_test', description='test_create') + delete_test_perm = Permission(name='delete_test', description='test_delete') + group_perm = PermissionGroup(name=group_name, permissions=[create_test_perm, delete_test_perm]) + self.app.permissions = mock.MagicMock(return_value=[create_test_perm, group_perm]) + + # Mock db_app_permissions + db_app_permission = mock.MagicMock(codename='test_code') + mock_perm_query = mock_dp.objects.filter().filter().all + mock_perm_query.return_value = [db_app_permission] + + # Mock Group.objects.filter + db_group = mock.MagicMock() + db_group.name = 'test_app_name:group' + + mock_group = mock_dg.objects.filter().all + mock_group.return_value = [db_group] + + # Mock TethysApp.objects.all() + db_app = mock.MagicMock(package='test_app_name') + + mock_toa = mock_ta.objects.all + mock_toa.return_value = [db_app] + + # Mock TethysApp.objects.get() + mock_ta_get = mock_ta.objects.get + mock_ta_get.return_value = 'test_get' + + # Mock Group.objects.get() + mock_group_get = mock_dg.objects.get + mock_group_get.return_value = group_name + + # Mock get permission get_perms(g, db_app) + mock_get.return_value = ['create_test'] + + # Execute + self.app.register_app_permissions() + + # Check if db_app_permission.delete() is called + db_app_permission.delete.assert_called_with() + + # Check if p.saved is called in perm + mock_dp.objects.get().save.assert_called_with() + + # Check if db_group.delete() is called + db_group.delete.assert_called_with() + + # Check if remove_perm(p, g, db_app) is called + mock_rem.assert_called_with('create_test', group_name, 'test_get') + + # Check if assign_perm(p, g, db_app) is called + mock_asg.assert_called_with(':delete_test', group_name, 'test_get') + + @mock.patch('guardian.shortcuts.get_perms') + @mock.patch('guardian.shortcuts.remove_perm') + @mock.patch('guardian.shortcuts.assign_perm') + @mock.patch('tethys_apps.models.TethysApp') + @mock.patch('django.contrib.auth.models.Group') + @mock.patch('django.contrib.auth.models.Permission') + def test_register_app_permissions_except_permission(self, mock_dp, mock_dg, mock_ta, mock_asg, mock_rem, mock_get): + group_name = 'test_group' + create_test_perm = Permission(name='create_test', description='test_create') + delete_test_perm = Permission(name='delete_test', description='test_delete') + group_perm = PermissionGroup(name=group_name, permissions=[create_test_perm, delete_test_perm]) + self.app.permissions = mock.MagicMock(return_value=[create_test_perm, group_perm]) + + # Mock Permission.objects.filter + db_app_permission = mock.MagicMock(codename='test_code') + mock_perm_query = mock_dp.objects.filter().filter().all + mock_perm_query.return_value = [db_app_permission] + + # Mock Permission.DoesNotExist + mock_dp.DoesNotExist = Exception + # Mock Permission.objects.get + mock_perm_get = mock_dp.objects.get + mock_perm_get.side_effect = Exception + + # Mock Group.objects.filter + db_group = mock.MagicMock() + db_group.name = 'test_app_name:group' + + mock_group = mock_dg.objects.filter().all + mock_group.return_value = [db_group] + + # Mock TethysApp.objects.all() + db_app = mock.MagicMock(package='test_app_name') + + mock_toa = mock_ta.objects.all + mock_toa.return_value = [db_app] + + # Mock TethysApp.objects.get() + mock_ta_get = mock_ta.objects.get + mock_ta_get.return_value = 'test_get' + + # Mock Permission.DoesNotExist + mock_dg.DoesNotExist = Exception + + # Mock Permission.objects.get + mock_group_get = mock_dg.objects.get + mock_group_get.side_effect = Exception + + # Execute + self.app.register_app_permissions() + + # Check if Permission in Permission.DoesNotExist is called + rts_call_args = mock_dp.call_args_list + + codename_check = [] + name_check = [] + for i in range(len(rts_call_args)): + codename_check.append(rts_call_args[i][1]['codename']) + name_check.append(rts_call_args[i][1]['name']) + + self.assertIn(':create_test', codename_check) + self.assertIn(' | test_create', name_check) + + # Check if db_group.delete() is called + db_group.delete.assert_called_with() + + # Check if Permission is called inside DoesNotExist + # Get the TethysApp content type + from django.contrib.contenttypes.models import ContentType + tethys_content_type = ContentType.objects.get( + app_label='tethys_apps', + model='tethysapp' + ) + mock_dp.assert_any_call(codename=':create_test', content_type=tethys_content_type, + name=' | test_create') + + # Check if p.save() is called inside DoesNotExist + mock_dp().save.assert_called() + + # Check if Group in Group.DoesNotExist is called + rts_call_args = mock_dg.call_args_list + self.assertEqual(':test_group', rts_call_args[0][1]['name']) + + # Check if Group(name=group) is called + mock_dg.assert_called_with(name=':test_group') + + # Check if g.save() is called + mock_dg().save.assert_called() + + # Check if assign_perm(p, g, db_app) is called + rts_call_args = mock_asg.call_args_list + check_list = [] + for i in range(len(rts_call_args)): + for j in [0, 2]: # only get first and last element to check + check_list.append(rts_call_args[i][0][j]) + + self.assertIn(':create_test', check_list) + self.assertIn('test_get', check_list) + self.assertIn(':delete_test', check_list) + self.assertIn('test_get', check_list) + + def test_job_templates(self): + self.assertIsNone(tethys_app_base.TethysAppBase().job_templates()) + + @mock.patch('tethys_apps.base.app_base.HandoffManager') + def test_get_handoff_manager(self, mock_hom): + mock_hom.return_value = 'test_handoff' + self.assertEqual('test_handoff', self.app.get_handoff_manager()) + + @mock.patch('tethys_sdk.jobs.JobManager') + def test_get_job_manager(self, mock_jm): + mock_jm.return_value = 'test_job_manager' + self.assertEqual('test_job_manager', self.app.get_job_manager()) + + @mock.patch('tethys_apps.base.app_base.TethysWorkspace') + def test_get_user_workspace(self, mock_tws): + user = self.user + self.app.get_user_workspace(user) + + # Check result + rts_call_args = mock_tws.call_args_list + self.assertIn('workspaces', rts_call_args[0][0][0]) + self.assertIn('user_workspaces', rts_call_args[0][0][0]) + self.assertIn(user.username, rts_call_args[0][0][0]) + + @mock.patch('tethys_apps.base.app_base.TethysWorkspace') + def test_get_user_workspace_http(self, mock_tws): + from django.http import HttpRequest + request = HttpRequest() + request.user = self.user + + self.app.get_user_workspace(request) + + # Check result + rts_call_args = mock_tws.call_args_list + self.assertIn('workspaces', rts_call_args[0][0][0]) + self.assertIn('user_workspaces', rts_call_args[0][0][0]) + self.assertIn(self.user.username, rts_call_args[0][0][0]) + + @mock.patch('tethys_apps.base.app_base.TethysWorkspace') + def test_get_user_workspace_none(self, mock_tws): + self.app.get_user_workspace(None) + + # Check result + rts_call_args = mock_tws.call_args_list + self.assertIn('workspaces', rts_call_args[0][0][0]) + self.assertIn('user_workspaces', rts_call_args[0][0][0]) + self.assertIn('anonymous_user', rts_call_args[0][0][0]) + + def test_get_user_workspace_error(self): + self.assertRaises(ValueError, self.app.get_user_workspace, user=['test']) + + @mock.patch('tethys_apps.base.app_base.TethysWorkspace') + def test_get_app_workspace(self, mock_tws): + self.app.get_app_workspace() + + # Check result + rts_call_args = mock_tws.call_args_list + self.assertIn('workspaces', rts_call_args[0][0][0]) + self.assertIn('app_workspace', rts_call_args[0][0][0]) + self.assertNotIn('user_workspaces', rts_call_args[0][0][0]) + + @mock.patch('tethys_apps.models.TethysApp') + def test_get_custom_setting(self, mock_ta): + setting_name = 'fake_setting' + result = TethysAppChild.get_custom_setting(name=setting_name) + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + mock_ta.objects.get().custom_settings.get.assert_called_with(name=setting_name) + mock_ta.objects.get().custom_settings.get().get_value.assert_called() + self.assertEqual(mock_ta.objects.get().custom_settings.get().get_value(), result) + + @mock.patch('tethys_apps.base.app_base.TethysAppSettingDoesNotExist') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_custom_setting_object_not_exist(self, mock_ta, mock_tas_dne): + mock_db_app = mock_ta.objects.get + mock_db_app.return_value = mock.MagicMock() + + mock_custom_settings = mock_ta.objects.get().custom_settings.get + mock_custom_settings.side_effect = ObjectDoesNotExist + + mock_tas_dne.return_value = TypeError + self.assertRaises(TypeError, self.app.get_custom_setting, name='test') + + mock_tas_dne.assert_called_with('CustomTethysAppSetting', 'test', '') + + @mock.patch('tethys_apps.models.TethysApp') + def test_get_dataset_service(self, mock_ta): + TethysAppChild.get_dataset_service(name=self.fake_name) + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + mock_ta.objects.get().dataset_services_settings.get.assert_called_with(name=self.fake_name) + mock_ta.objects.get().dataset_services_settings.get().\ + get_value.assert_called_with(as_endpoint=False, as_engine=False, as_public_endpoint=False) + + @mock.patch('tethys_apps.base.app_base.TethysAppSettingDoesNotExist') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_dataset_service_object_not_exist(self, mock_ta, mock_tas_dne): + mock_dss = mock_ta.objects.get().dataset_services_settings.get + mock_dss.side_effect = ObjectDoesNotExist + + mock_tas_dne.return_value = TypeError + self.assertRaises(TypeError, TethysAppChild.get_dataset_service, name=self.fake_name) + + mock_tas_dne.assert_called_with('DatasetServiceSetting', self.fake_name, TethysAppChild.name) + + @mock.patch('tethys_apps.models.TethysApp') + def test_get_spatial_dataset_service(self, mock_ta): + TethysAppChild.get_spatial_dataset_service(name=self.fake_name) + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + mock_ta.objects.get().spatial_dataset_service_settings.get.assert_called_with(name=self.fake_name) + mock_ta.objects.get().spatial_dataset_service_settings.get().\ + get_value.assert_called_with(as_endpoint=False, as_engine=False, as_public_endpoint=False, + as_wfs=False, as_wms=False) + + @mock.patch('tethys_apps.base.app_base.TethysAppSettingDoesNotExist') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_spatial_dataset_service_object_not_exist(self, mock_ta, mock_tas_dne): + mock_sdss = mock_ta.objects.get().spatial_dataset_service_settings.get + mock_sdss.side_effect = ObjectDoesNotExist + + mock_tas_dne.return_value = TypeError + self.assertRaises(TypeError, TethysAppChild.get_spatial_dataset_service, name=self.fake_name) + + mock_tas_dne.assert_called_with('SpatialDatasetServiceSetting', self.fake_name, TethysAppChild.name) + + @mock.patch('tethys_apps.models.TethysApp') + def test_get_web_processing_service(self, mock_ta): + TethysAppChild.get_web_processing_service(name=self.fake_name) + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + mock_ta.objects.get().wps_services_settings.objects.get.assert_called_with(name=self.fake_name) + mock_ta.objects.get().wps_services_settings.objects.get().get_value.\ + assert_called_with(as_public_endpoint=False, as_endpoint=False, as_engine=False) + + @mock.patch('tethys_apps.base.app_base.TethysAppSettingDoesNotExist') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_web_processing_service_object_not_exist(self, mock_ta, mock_tas_dne): + mock_wss = mock_ta.objects.get().wps_services_settings.objects.get + mock_wss.side_effect = ObjectDoesNotExist + + mock_tas_dne.return_value = TypeError + self.assertRaises(TypeError, TethysAppChild.get_web_processing_service, name=self.fake_name) + + mock_tas_dne.assert_called_with('WebProcessingServiceSetting', self.fake_name, TethysAppChild.name) + + @mock.patch('tethys_apps.models.TethysApp') + def test_get_persistent_store_connection(self, mock_ta): + TethysAppChild.get_persistent_store_connection(name=self.fake_name) + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + mock_ta.objects.get().persistent_store_connection_settings.get.assert_called_with(name=self.fake_name) + mock_ta.objects.get().persistent_store_connection_settings.get().get_value.\ + assert_called_with(as_engine=True, as_sessionmaker=False, as_url=False) + + @mock.patch('tethys_apps.base.app_base.TethysAppSettingDoesNotExist') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_persistent_store_connection_object_not_exist(self, mock_ta, mock_tas_dne): + mock_sdss = mock_ta.objects.get().persistent_store_connection_settings.get + mock_sdss.side_effect = ObjectDoesNotExist + + mock_tas_dne.return_value = TypeError + self.assertRaises(TypeError, TethysAppChild.get_persistent_store_connection, name=self.fake_name) + + mock_tas_dne.assert_called_with('PersistentStoreConnectionSetting', self.fake_name, TethysAppChild.name) + + @mock.patch('tethys_apps.base.app_base.tethys_log') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_persistent_store_connection_not_assign(self, mock_ta, mock_log): + mock_sdss = mock_ta.objects.get().persistent_store_connection_settings.get + mock_sdss.side_effect = TethysAppSettingNotAssigned + + # Execute + TethysAppChild.get_persistent_store_connection(name=self.fake_name) + + # Check log + rts_call_args = mock_log.warn.call_args_list + self.assertIn('Tethys app setting is not assigned.', rts_call_args[0][0][0]) + check_string = 'PersistentStoreConnectionSetting named "{}" has not been assigned'. format(self.fake_name) + self.assertIn(check_string, rts_call_args[0][0][0]) + + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_persistent_store_database(self, mock_ta, mock_ite): + mock_ite.return_value = False + TethysAppChild.get_persistent_store_database(name=self.fake_name) + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + + # Check ps_database_settings.get(name=verified_name) is called + mock_ta.objects.get().persistent_store_database_settings.get.assert_called_with(name=self.fake_name) + mock_ta.objects.get().persistent_store_database_settings.get().get_value.\ + assert_called_with(as_engine=True, as_sessionmaker=False, as_url=False, with_db=True) + + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_persistent_store_database_object_does_not_exist(self, mock_ta, mock_ite): + mock_ite.return_value = False + mock_ta.objects.get().persistent_store_database_settings.get.side_effect = ObjectDoesNotExist + + # Check Raise + self.assertRaises(TethysAppSettingDoesNotExist, TethysAppChild.get_persistent_store_database, + name=self.fake_name) + + @mock.patch('tethys_apps.base.app_base.tethys_log') + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_persistent_store_database_not_assigned(self, mock_ta, mock_ite, mock_log): + mock_ite.return_value = False + mock_ta.objects.get().persistent_store_database_settings.get.side_effect = TethysAppSettingNotAssigned + + TethysAppChild.get_persistent_store_database(name=self.fake_name) + + # Check log + rts_call_args = mock_log.warn.call_args_list + self.assertIn('Tethys app setting is not assigned.', rts_call_args[0][0][0]) + check_string = 'PersistentStoreDatabaseSetting named "{}" has not been assigned'. format(self.fake_name) + self.assertIn(check_string, rts_call_args[0][0][0]) + + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_create_persistent_store(self, mock_ta, mock_ite): + mock_ite.return_value = False + result = TethysAppChild.create_persistent_store(db_name='example_db', connection_name='primary') + + # Check ps_connection_settings.get(name=connection_name) is called + mock_ta.objects.get().persistent_store_connection_settings.get.assert_called_with(name='primary') + + # Check db_app.persistent_store_database_settings.get(name=verified_db_name) is called + mock_ta.objects.get().persistent_store_database_settings.get.assert_called_with(name='example_db') + + # Check db_setting.save() is called + mock_ta.objects.get().persistent_store_database_settings.get().save.assert_called() + + # Check Create the new database is called + mock_ta.objects.get().persistent_store_database_settings.get().create_persistent_store_database.\ + assert_called_with(force_first_time=False, refresh=False) + + # Check result is true + self.assertTrue(result) + + @mock.patch('tethys_apps.base.app_base.get_test_db_name') + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_create_persistent_store_testing_env(self, mock_ta, mock_ite, mock_tdn): + mock_ite.return_value = True + mock_tdn.return_value = 'verified_db_name' + TethysAppChild.create_persistent_store(db_name='example_db', connection_name=None) + + # Check get_test_db_name(db_name) is called + mock_tdn.assert_called_with('example_db') + + rts_call_args = mock_ta.objects.get().persistent_store_database_settings.get.call_args_list + # Check ps_connection_settings.get(name=connection_name) is called + self.assertEqual({'name': 'example_db'}, rts_call_args[0][1]) + + # Check db_app.persistent_store_database_settings.get(name=verified_db_name) is called + self.assertEqual({'name': 'verified_db_name'}, rts_call_args[1][1]) + + # Check db_setting.save() is called + mock_ta.objects.get().persistent_store_database_settings.get().save.assert_called() + + # Check Create the new database is called + mock_ta.objects.get().persistent_store_database_settings.get().create_persistent_store_database.\ + assert_called_with(force_first_time=False, refresh=False) + + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_create_persistent_store_no_connection_name(self, _, mock_ite): + mock_ite.return_value = False + self.assertRaises(ValueError, TethysAppChild.create_persistent_store, db_name='example_db', + connection_name=None) + + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_create_persistent_store_no_connection_object_not_exist_testing_env(self, mock_ta, mock_ite): + # Need to test in testing env to test the connection_name is None case + mock_ite.return_value = True + mock_ta.objects.get().persistent_store_database_settings.get.side_effect = ObjectDoesNotExist + + self.assertRaises(TethysAppSettingDoesNotExist, TethysAppChild.create_persistent_store, db_name='example_db', + connection_name=None) + + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_create_persistent_store_connection_object_not_exist_testing_env(self, mock_ta, mock_ite): + # Need to test in testing env to test the connection_name is None case + mock_ite.return_value = True + mock_ta.objects.get().persistent_store_connection_settings.get.side_effect = ObjectDoesNotExist + + self.assertRaises(TethysAppSettingDoesNotExist, TethysAppChild.create_persistent_store, db_name='example_db', + connection_name='test_con') + + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting') + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_create_persistent_store_object_not_exist(self, mock_ta, mock_ite, mock_psd): + # Need to test in testing env to test the connection_name is None case + mock_ite.return_value = False + mock_ta.objects.get().persistent_store_database_settings.get.side_effect = ObjectDoesNotExist + + # Execute + TethysAppChild.create_persistent_store(db_name='example_db', connection_name='test_con') + + # Check if PersistentStoreDatabaseSetting is called + mock_psd.assert_called_with(description='', dynamic=True, initializer='', name='example_db', + required=False, spatial=False) + + # Check if db_setting is called + db_setting = mock_psd() + mock_ta.objects.get().add_settings.assert_called_with((db_setting,)) + + # Check if save is called + mock_ta.objects.get().save.assert_called() + + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_drop_persistent_store(self, mock_ta, mock_ite): + mock_ite.return_value = False + result = TethysAppChild.drop_persistent_store(name='example_store') + + # Check if TethysApp.objects.get(package=cls.package) is called + mock_ta.objects.get.assert_called_with(package='test_app') + + # Check if ps_database_settings.get(name=verified_name) is called + mock_ta.objects.get().persistent_store_database_settings.get.assert_called_with(name='example_store') + + # Check if drop the persistent store is called + mock_ta.objects.get().persistent_store_database_settings.get().drop_persistent_store_database.assert_called() + + # Check if remove the database setting is called + mock_ta.objects.get().persistent_store_database_settings.get().delete.assert_called() + + # Check result return True + self.assertTrue(result) + + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_drop_persistent_store_object_does_not_exist(self, mock_ta, mock_ite): + mock_ite.return_value = False + mock_ta.objects.get().persistent_store_database_settings.get.side_effect = ObjectDoesNotExist + result = TethysAppChild.drop_persistent_store(name='example_store') + + # Check result return True + self.assertTrue(result) + + @mock.patch('tethys_apps.models.TethysApp') + def test_list_persistent_store_databases_dynamic(self, mock_ta): + mock_settings = mock_ta.objects.get().persistent_store_database_settings.filter + setting1 = mock.MagicMock() + setting1.name = 'test1' + setting2 = mock.MagicMock() + setting2.name = 'test2' + mock_settings.return_value = [setting1, setting2] + + result = TethysAppChild.list_persistent_store_databases(dynamic_only=True) + + # Check TethysApp.objects.get is called + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + + # Check filter is called + mock_ta.objects.get().persistent_store_database_settings.filter.\ + assert_called_with(persistentstoredatabasesetting__dynamic=True) + + # Check result + self.assertEqual(['test1', 'test2'], result) + + @mock.patch('tethys_apps.models.TethysApp') + def test_list_persistent_store_databases_static(self, mock_ta): + mock_settings = mock_ta.objects.get().persistent_store_database_settings.filter + setting1 = mock.MagicMock() + setting1.name = 'test1' + setting2 = mock.MagicMock() + setting2.name = 'test2' + mock_settings.return_value = [setting1, setting2] + + result = TethysAppChild.list_persistent_store_databases(static_only=True) + + # Check TethysApp.objects.get is called + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + + # Check filter is called + mock_ta.objects.get().persistent_store_database_settings.filter.\ + assert_called_with(persistentstoredatabasesetting__dynamic=False) + + # Check result + self.assertEqual(['test1', 'test2'], result) + + @mock.patch('tethys_apps.models.TethysApp') + def test_list_persistent_store_connections(self, mock_ta): + setting1 = mock.MagicMock() + setting1.name = 'test1' + setting2 = mock.MagicMock() + setting2.name = 'test2' + mock_ta.objects.get().persistent_store_connection_settings = [setting1, setting2] + + result = TethysAppChild.list_persistent_store_connections() + + # Check TethysApp.objects.get is called + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + + # Check result + self.assertEqual(['test1', 'test2'], result) + + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_persistent_store_exists(self, mock_ta, mock_ite): + mock_ite.return_value = False + result = TethysAppChild.persistent_store_exists(name='test_store') + + # Check TethysApp.objects.get is called + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + + # Check if ps_database_settings.get is called + mock_ta.objects.get().persistent_store_database_settings.get.assert_called_with(name='test_store') + + # Check if database exists is called + mock_ta.objects.get().persistent_store_database_settings.get().persistent_store_database_exists.assert_called() + + # Check if result True + self.assertTrue(result) + + @mock.patch('tethys_apps.base.app_base.is_testing_environment') + @mock.patch('tethys_apps.models.TethysApp') + def test_persistent_store_exists_object_does_not_exist(self, mock_ta, mock_ite): + mock_ite.return_value = False + mock_ta.objects.get().persistent_store_database_settings.get.side_effect = ObjectDoesNotExist + result = TethysAppChild.persistent_store_exists(name='test_store') + + # Check TethysApp.objects.get is called + mock_ta.objects.get.assert_called_with(package=TethysAppChild.package) + + # Check if ps_database_settings.get is called + mock_ta.objects.get().persistent_store_database_settings.get.assert_called_with(name='test_store') + + # Check if result False + self.assertFalse(result) + + @mock.patch('django.conf.settings') + @mock.patch('tethys_apps.models.TethysApp') + def test_sync_with_tethys_db(self, mock_ta, _): + mock_ta.objects.filter().all.return_value = [] + self.app.name = 'n' + self.app.package = 'p' + self.app.description = 'd' + self.app.enable_feedback = 'e' + self.app.feedback_emails = 'f' + self.app.index = 'in' + self.app.icon = 'ic' + self.app.root_url = 'r' + self.app.color = 'c' + self.app.tags = 't' + self.app.sync_with_tethys_db() + + # Check if TethysApp.objects.filter is called + mock_ta.objects.filter().all.assert_called() + + # Check if TethysApp is called + mock_ta.assert_called_with(color='c', description='d', enable_feedback='e', feedback_emails='f', + icon='ic', index='in', name='n', package='p', root_url='r', tags='t') + + # Check if save is called 2 times + self.assertTrue(mock_ta().save.call_count == 2) + + # Check if add_settings is called 5 times + self.assertTrue(mock_ta().add_settings.call_count == 5) + + @mock.patch('django.conf.settings') + @mock.patch('tethys_apps.models.TethysApp') + def test_sync_with_tethys_db_in_db(self, mock_ta, mock_ds): + mock_ds.DEBUG = True + mock_app = mock.MagicMock() + mock_ta.objects.filter().all.return_value = [mock_app] + self.app.sync_with_tethys_db() + + # Check if TethysApp.objects.filter is called + mock_ta.objects.filter().all.assert_called() + + # Check if save is called 2 times + self.assertTrue(mock_app.save.call_count == 2) + + # Check if add_settings is called 5 times + self.assertTrue(mock_app.add_settings.call_count == 5) + + @mock.patch('django.conf.settings') + @mock.patch('tethys_apps.models.TethysApp') + def test_sync_with_tethys_db_more_than_one(self, mock_ta, mock_ds): + mock_ds.DEBUG = True + mock_app = mock.MagicMock() + mock_ta.objects.filter().all.return_value = [mock_app, mock_app] + self.app.sync_with_tethys_db() + + # Check if TethysApp.objects.filter is called + mock_ta.objects.filter().all.assert_called() + + # Check if is not called + mock_app.save.assert_not_called() + + # Check if is not called + mock_app.add_settings.assert_not_called() + + @mock.patch('tethys_apps.base.app_base.tethys_log') + @mock.patch('tethys_apps.models.TethysApp') + def test_sync_with_tethys_db_exception(self, mock_ta, mock_log): + mock_ta.objects.filter().all.side_effect = Exception + self.app.sync_with_tethys_db() + + mock_log.error.assert_called() + + @mock.patch('tethys_apps.models.TethysApp') + def test_remove_from_db(self, mock_ta): + self.app.remove_from_db() + + # Check if delete is called + mock_ta.objects.filter().delete.assert_called() + + @mock.patch('tethys_apps.base.app_base.tethys_log') + @mock.patch('tethys_apps.models.TethysApp') + def test_remove_from_db_2(self, mock_ta, mock_log): + mock_ta.objects.filter().delete.side_effect = Exception + self.app.remove_from_db() + + # Check tethys log error + mock_log.error.assert_called() diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_controller.py b/tests/unit_tests/test_tethys_apps/test_base/test_controller.py new file mode 100644 index 000000000..5f6051bef --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_base/test_controller.py @@ -0,0 +1,22 @@ +import unittest +import mock +import tethys_apps.base.controller as tethys_controller + + +class TestController(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_app_controller_maker(self): + root_url = 'test_root_url' + result = tethys_controller.app_controller_maker(root_url) + self.assertEqual(result.root_url, root_url) + + @mock.patch('django.views.generic.View.as_view') + def test_TethysController(self, mock_as_view): + kwargs = {'foo': 'bar'} + tethys_controller.TethysController.as_controller(**kwargs) + mock_as_view.assert_called_with(**kwargs) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_function_extractor.py b/tests/unit_tests/test_tethys_apps/test_base/test_function_extractor.py new file mode 100644 index 000000000..4c31db0b6 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_base/test_function_extractor.py @@ -0,0 +1,53 @@ +import unittest +import types +import tethys_apps.base.function_extractor as tethys_function_extractor + + +def test_func(): + pass + + +class TestTethysFunctionExtractor(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_init(self): + path = 'tethysapp-test_app.tethysapp.test_app.controller.home' + result = tethys_function_extractor.TethysFunctionExtractor(path=path) + + # Check Result + self.assertEqual(path, result.path) + self.assertEqual('tethys_apps.tethysapp', result.prefix) + + def test_init_func(self): + result = tethys_function_extractor.TethysFunctionExtractor(path=test_func) + + # Check Result + self.assertIs(test_func, result.function) + self.assertTrue(result.valid) + + def test_valid(self): + path = 'test_app.model.test_initializer' + result = tethys_function_extractor.TethysFunctionExtractor(path=path).valid + + # Check Result + self.assertTrue(result) + + def test_function(self): + path = 'test_app.model.test_initializer' + result = tethys_function_extractor.TethysFunctionExtractor(path=path).function + + # Check Result + self.assertIsInstance(result, types.FunctionType) + + def test_function_error(self): + path = 'test_app1.foo' + app = tethys_function_extractor.TethysFunctionExtractor(path=path, throw=True) + + def test_function_import(): + return app.function + + self.assertRaises(ImportError, test_function_import) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_handoff.py b/tests/unit_tests/test_tethys_apps/test_base/test_handoff.py new file mode 100644 index 000000000..298dc1cbe --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_base/test_handoff.py @@ -0,0 +1,277 @@ +from __future__ import print_function +import unittest +import tethys_apps.base.handoff as tethys_handoff +from types import FunctionType +import mock + + +def test_function(*args): + + if args is not None: + arg_list = [] + for arg in args: + arg_list.append(arg) + return arg_list + else: + return '' + + +class TestHandoffManager(unittest.TestCase): + def setUp(self): + self.hm = tethys_handoff.HandoffManager + + def tearDown(self): + pass + + def test_init(self): + # Mock app + app = mock.MagicMock() + + # Mock handoff_handlers + handlers = mock.MagicMock(name='handler_name') + app.handoff_handlers.return_value = handlers + + # mock _get_valid_handlers + self.hm._get_valid_handlers = mock.MagicMock(return_value=['valid_handler']) + result = tethys_handoff.HandoffManager(app=app) + + # Check result + self.assertEqual(app, result.app) + self.assertEqual(handlers, result.handlers) + self.assertEqual(['valid_handler'], result.valid_handlers) + + def test_repr(self): + # Mock app + app = mock.MagicMock() + + # Mock handoff_handlers + handlers = mock.MagicMock() + handlers.name = 'test_handler' + app.handoff_handlers.return_value = [handlers] + + # mock _get_valid_handlers + self.hm._get_valid_handlers = mock.MagicMock(return_value=['valid_handler']) + result = tethys_handoff.HandoffManager(app=app).__repr__() + check_string = "".format(app, handlers.name) + + self.assertEqual(check_string, result) + + def test_get_capabilities(self): + # Mock app + app = mock.MagicMock() + + # Mock _get_handoff_manager_for_app + manager = mock.MagicMock(valid_handlers='test_handlers') + self.hm._get_handoff_manager_for_app = mock.MagicMock(return_value=manager) + + result = tethys_handoff.HandoffManager(app=app).get_capabilities(app_name='test_app') + + # Check Result + self.assertEqual('test_handlers', result) + + def test_get_capabilities_external(self): + # Mock app + app = mock.MagicMock() + + # Mock _get_handoff_manager_for_app + handler1 = mock.MagicMock() + handler1.internal = False + handler2 = mock.MagicMock() + # Do not write out handler2 + handler2.internal = True + manager = mock.MagicMock(valid_handlers=[handler1, handler2]) + self.hm._get_handoff_manager_for_app = mock.MagicMock(return_value=manager) + + result = tethys_handoff.HandoffManager(app=app).get_capabilities(app_name='test_app', external_only=True) + + # Check Result + self.assertEqual([handler1], result) + + @mock.patch('tethys_apps.base.handoff.json') + def test_get_capabilities_json(self, mock_json): + # Mock app + app = mock.MagicMock() + + # Mock HandoffHandler.__json + + handler1 = mock.MagicMock(name='test_name') + manager = mock.MagicMock(valid_handlers=[handler1]) + self.hm._get_handoff_manager_for_app = mock.MagicMock(return_value=manager) + + tethys_handoff.HandoffManager(app=app).get_capabilities(app_name='test_app', jsonify=True) + + # Check Result + rts_call_args = mock_json.dumps.call_args_list + self.assertEqual('test_name', rts_call_args[0][0][0][0]['_mock_name']) + + def test_get_handler(self): + app = mock.MagicMock() + + # Mock _get_handoff_manager_for_app + handler1 = mock.MagicMock() + handler1.name = 'handler1' + manager = mock.MagicMock(valid_handlers=[handler1]) + self.hm._get_handoff_manager_for_app = mock.MagicMock(return_value=manager) + + result = tethys_handoff.HandoffManager(app=app).get_handler(handler_name='handler1') + + self.assertEqual('handler1', result.name) + + def test_handoff(self): + from django.http import HttpRequest + request = HttpRequest() + + # Mock app + app = mock.MagicMock() + app.name = 'test_app_name' + + # Mock _get_handoff_manager_for_app + handler1 = mock.MagicMock() + handler1().internal = False + manager = mock.MagicMock(get_handler=handler1) + self.hm._get_handoff_manager_for_app = mock.MagicMock(return_value=manager) + + result = tethys_handoff.HandoffManager(app=app).handoff(request=request, handler_name='test_handler') + + # 302 code is for redirect + self.assertEqual(302, result.status_code) + + @mock.patch('tethys_apps.base.handoff.HttpResponseBadRequest') + def test_handoff_type_error(self, mock_hrbr): + from django.http import HttpRequest + request = HttpRequest() + + # Mock app + app = mock.MagicMock() + app.name = 'test_app_name' + + # Mock _get_handoff_manager_for_app + handler1 = mock.MagicMock() + handler1().internal = False + handler1().side_effect = TypeError('test message') + manager = mock.MagicMock(get_handler=handler1) + self.hm._get_handoff_manager_for_app = mock.MagicMock(return_value=manager) + + tethys_handoff.HandoffManager(app=app).handoff(request=request, handler_name='test_handler') + rts_call_args = mock_hrbr.call_args_list + + # Check result + self.assertIn('HTTP 400 Bad Request: test message.', rts_call_args[0][0][0]) + + @mock.patch('tethys_apps.base.handoff.HttpResponseBadRequest') + def test_handoff_error(self, mock_hrbr): + from django.http import HttpRequest + request = HttpRequest() + # + # # Mock app + app = mock.MagicMock() + app.name = 'test_app_name' + + # Mock _get_handoff_manager_for_app + handler1 = mock.MagicMock() + # Ask Nathan is this how the test should be. because internal = True has + # nothing to do with the error message. + handler1().internal = True + handler1().side_effect = TypeError('test message') + mapp = mock.MagicMock() + mapp.name = 'test manager name' + manager = mock.MagicMock(get_handler=handler1, app=mapp) + self.hm._get_handoff_manager_for_app = mock.MagicMock(return_value=manager) + + tethys_handoff.HandoffManager(app=app).handoff(request=request, handler_name='test_handler') + rts_call_args = mock_hrbr.call_args_list + + # Check result + check_message = "HTTP 400 Bad Request: No handoff handler '{0}' for app '{1}' found".\ + format('test manager name', 'test_handler') + self.assertIn(check_message, rts_call_args[0][0][0]) + + @mock.patch('tethys_apps.base.handoff.print') + def test_get_valid_handlers(self, mock_print): + app = mock.MagicMock(package='test_app') + + # Mock handoff_handlers + # Mock handoff_handlers + handler1 = mock.MagicMock(handler='my_first_app.controllers.my_handler', valid=True) + handler2 = mock.MagicMock(handler='controllers:home', valid=False) + + # Cover Import Error Case + handler3 = mock.MagicMock(handler='controllers1:home1', valid=False) + + app.handoff_handlers.return_value = [handler1, handler2, handler3] + # mock _get_valid_handlers + + result = tethys_handoff.HandoffManager(app=app)._get_valid_handlers() + + # Check result + self.assertEqual('my_first_app.controllers.my_handler', result[0].handler) + self.assertEqual('controllers:home', result[1].handler) + + check_message = 'DEPRECATION WARNING: The handler attribute of a HandoffHandler should now be in the form:' \ + ' "my_first_app.controllers.my_handler". The form "handoff:my_handler" is now deprecated.' + mock_print.assert_called_with(check_message) + + +class TestHandoffHandler(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_init(self): + result = tethys_handoff.HandoffHandler(name='test_name', handler='test_app.handoff.csv', internal=True) + + # Check Result + self.assertEqual('test_name', result.name) + self.assertEqual('test_app.handoff.csv', result.handler) + self.assertTrue(result.internal) + self.assertIs(type(result.function), FunctionType) + + def test_repr(self): + result = tethys_handoff.HandoffHandler(name='test_name', handler='test_app.handoff.csv', + internal=True).__repr__() + + # Check Result + check_string = '' + self.assertEqual(check_string, result) + + def test_dict_json_arguments(self): + tethys_handoff.HandoffHandler.arguments = ['test_json', 'request'] + result = tethys_handoff.HandoffHandler(name='test_name', handler='test_app.handoff.csv', + internal=True).__dict__() + + # Check Result + check_dict = {'name': 'test_name', 'arguments': ['test_json']} + self.assertIsInstance(result, dict) + self.assertEqual(check_dict, result) + + def test_arguments(self): + result = tethys_handoff.HandoffHandler(name='test_name', handler='test_app.handoff.csv', internal=True)\ + .arguments + + self.assertEqual(['request', 'csv_url'], result) + + +class TestGetHandoffManagerFroApp(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_not_app_name(self): + app = mock.MagicMock() + result = tethys_handoff.HandoffManager(app=app)._get_handoff_manager_for_app(app_name=None) + + self.assertEqual(app, result.app) + + @mock.patch('tethys_apps.base.handoff.tethys_apps') + def test_with_app(self, mock_ta): + app = mock.MagicMock(package='test_app') + app.get_handoff_manager.return_value = 'test_manager' + mock_ta.harvester.SingletonAppHarvester().apps = [app] + result = tethys_handoff.HandoffManager(app=app)._get_handoff_manager_for_app(app_name='test_app') + + # Check result + self.assertEqual('test_manager', result) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_mixins.py b/tests/unit_tests/test_tethys_apps/test_base/test_mixins.py new file mode 100644 index 000000000..b3f7b0875 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_base/test_mixins.py @@ -0,0 +1,15 @@ +import unittest +import tethys_apps.base.mixins as tethys_mixins + + +class TestTethysBaseMixin(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_TethysBaseMixin(self): + result = tethys_mixins.TethysBaseMixin() + result.root_url = 'test-url' + self.assertEqual('test_url', result.namespace) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py b/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py new file mode 100644 index 000000000..eb459213e --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_base/test_permissions.py @@ -0,0 +1,94 @@ +import unittest +import tethys_apps.base.permissions as tethys_permission +import mock + +from django.test import RequestFactory +from tests.factories.django_user import UserFactory + + +class TestPermission(unittest.TestCase): + def setUp(self): + self.name = 'test_name' + self.description = 'test_description' + self.check_string = ''.\ + format(self.name, self.description) + + def tearDown(self): + pass + + def test_init(self): + result = tethys_permission.Permission(name=self.name, description=self.description) + self.assertEqual(self.name, result.name) + self.assertEqual(self.description, result.description) + + def test_repr(self): + result = tethys_permission.Permission(name=self.name, description=self.description)._repr() + + # Check Result + self.assertEqual(self.check_string, result) + + def test_str(self): + result = tethys_permission.Permission(name=self.name, description=self.description).__str__() + + # Check Result + self.assertEqual(self.check_string, result) + + def test_repr_(self): + result = tethys_permission.Permission(name=self.name, description=self.description).__repr__() + + # Check Result + self.assertEqual(self.check_string, result) + + +class TestPermissionGroup(unittest.TestCase): + def setUp(self): + self.user = UserFactory() + self.request_factory = RequestFactory() + self.name = 'test_name' + self.permissions = ['foo', 'bar'] + self.check_string = ''.format(self.name) + + def tearDown(self): + pass + + def test_init(self): + result = tethys_permission.PermissionGroup(name=self.name, permissions=['foo', 'bar']) + + self.assertEqual(self.name, result.name) + self.assertEqual(self.permissions, result.permissions) + + def test_repr(self): + result = tethys_permission.PermissionGroup(name=self.name)._repr() + + # Check Result + self.assertEqual(self.check_string, result) + + def test_str(self): + result = tethys_permission.PermissionGroup(name=self.name).__str__() + + # Check Result + self.assertEqual(self.check_string, result) + + def test_repr_(self): + result = tethys_permission.PermissionGroup(name=self.name).__repr__() + + # Check Result + self.assertEqual(self.check_string, result) + + @mock.patch('tethys_apps.utilities.get_active_app') + def test_has_permission(self, mock_app): + request = self.request_factory + self.user.has_perm = mock.MagicMock() + request.user = self.user + mock_app.return_value = mock.MagicMock(package='test_package') + result = tethys_permission.has_permission(request=request, perm='test_perm') + self.assertTrue(result) + + @mock.patch('tethys_apps.utilities.get_active_app') + def test_has_permission_no(self, mock_app): + request = self.request_factory + self.user.has_perm = mock.MagicMock(return_value=False) + request.user = self.user + mock_app.return_value = mock.MagicMock(package='test_package') + result = tethys_permission.has_permission(request=request, perm='test_perm') + self.assertFalse(result) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_testing/__init__.py b/tests/unit_tests/test_tethys_apps/test_base/test_testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_testing/test_environment.py b/tests/unit_tests/test_tethys_apps/test_base/test_testing/test_environment.py new file mode 100644 index 000000000..d83c19c93 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_base/test_testing/test_environment.py @@ -0,0 +1,32 @@ +import unittest +import tethys_apps.base.testing.environment as base_environment +from os import environ + + +class TestEnvironment(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_set_testing_environment(self): + base_environment.set_testing_environment(True) + self.assertEqual('true', environ['TETHYS_TESTING_IN_PROGRESS']) + + result = base_environment.is_testing_environment() + self.assertEqual('true', result) + + base_environment.set_testing_environment(False) + self.assertIsNone(environ.get('TETHYS_TESTING_IN_PROGRESS')) + + result = base_environment.is_testing_environment() + self.assertIsNone(result) + + def test_get_test_db_name(self): + expected_result = 'tethys-testing_test' + result = base_environment.get_test_db_name('test') + self.assertEqual(expected_result, result) + + result = base_environment.get_test_db_name('tethys-testing_test') + self.assertEqual(expected_result, result) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_testing/test_testing.py b/tests/unit_tests/test_tethys_apps/test_base/test_testing/test_testing.py new file mode 100644 index 000000000..8f9dffcb0 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_base/test_testing/test_testing.py @@ -0,0 +1,163 @@ +import unittest +import tethys_apps.base.testing.testing as base_testing +import mock +from tethys_apps.base.app_base import TethysAppBase + + +class TestClass(): + pass + + +def bypass_init(self): + pass + + +class TestTethysTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.harvester.SingletonHarvester') + def test_setup(self, mock_harvest): + base_testing.TethysTestCase.__init__ = bypass_init + t = base_testing.TethysTestCase() + t.set_up = mock.MagicMock() + t.setUp() + + t.set_up.assert_called() + mock_harvest().harvest.assert_called() + + def test_set_up(self): + base_testing.TethysTestCase.__init__ = bypass_init + t = base_testing.TethysTestCase() + t.set_up() + + def test_teardown(self): + base_testing.TethysTestCase.__init__ = bypass_init + t = base_testing.TethysTestCase() + t.tear_down = mock.MagicMock() + t.tearDown() + + t.tear_down.assert_called() + + def test_tear_down(self): + base_testing.TethysTestCase.__init__ = bypass_init + t = base_testing.TethysTestCase() + t.tear_down() + + @mock.patch('tethys_apps.base.testing.testing.TethysAppBase.create_persistent_store') + @mock.patch('tethys_apps.models.TethysApp') + @mock.patch('tethys_apps.base.testing.testing.is_testing_environment') + def test_create_test_persistent_stores_for_app(self, mock_ite, mock_ta, mock_cps): + mock_ite.return_value = True + app_class = TethysAppBase + + # mock_db_setting + db_setting = mock.MagicMock(spatial='test_spatial', initializer='test_init') + db_setting.name = 'test_name' + db_app = mock.MagicMock(persistent_store_database_settings=[db_setting]) + mock_ta.objects.get.return_value = db_app + + # Execute + base_testing.TethysTestCase.create_test_persistent_stores_for_app(app_class) + + # Check Result + mock_ta.objects.get.assert_called_with(package='') + mock_cps.assert_called_with(connection_name=None, db_name='test_name', force_first_time=True, + initializer='test_init', refresh=True, spatial='test_spatial') + + @mock.patch('tethys_apps.base.testing.testing.is_testing_environment') + def test_create_test_persistent_stores_for_app_not_testing_env(self, mock_ite): + mock_ite.return_value = False + app_class = TethysAppBase + self.assertRaises(EnvironmentError, base_testing.TethysTestCase.create_test_persistent_stores_for_app, + app_class) + + @mock.patch('tethys_apps.base.testing.testing.is_testing_environment') + def test_create_test_persistent_stores_for_app_not_subclass(self, mock_ite): + mock_ite.return_value = True + app_class = TestClass + self.assertRaises(TypeError, base_testing.TethysTestCase.create_test_persistent_stores_for_app, app_class) + + @mock.patch('tethys_apps.base.testing.testing.TethysAppBase.create_persistent_store') + @mock.patch('tethys_apps.models.TethysApp') + @mock.patch('tethys_apps.base.testing.testing.is_testing_environment') + def test_create_test_persistent_stores_for_app_not_success(self, mock_ite, mock_ta, mock_cps): + mock_ite.return_value = True + app_class = TethysAppBase + + # mock_db_setting + db_setting = mock.MagicMock() + db_app = mock.MagicMock(persistent_store_database_settings=[db_setting]) + mock_ta.objects.get.return_value = db_app + + # mock create_peristent_store + mock_cps.return_value = False + + # Execute + self.assertRaises(SystemError, base_testing.TethysTestCase.create_test_persistent_stores_for_app, app_class) + + @mock.patch('tethys_apps.base.testing.testing.TethysAppBase.drop_persistent_store') + @mock.patch('tethys_apps.base.testing.testing.get_test_db_name') + @mock.patch('tethys_apps.base.testing.testing.TethysAppBase.list_persistent_store_databases') + @mock.patch('tethys_apps.base.testing.testing.is_testing_environment') + def test_destroy_test_persistent_stores_for_app(self, mock_ite, mock_lps, mock_get, mock_dps): + mock_ite.return_value = True + app_class = TethysAppBase + + mock_lps.return_value = ['db_name'] + mock_get.return_value = 'test_db_name' + # Execute + base_testing.TethysTestCase.destroy_test_persistent_stores_for_app(app_class) + + # Check mock called + mock_get.assert_called_with('db_name') + mock_dps.assert_called_with('test_db_name') + + @mock.patch('tethys_apps.base.testing.testing.is_testing_environment') + def test_destroy_test_persistent_stores_for_app_not_testing_env(self, mock_ite): + mock_ite.return_value = False + app_class = TethysAppBase + self.assertRaises(EnvironmentError, base_testing.TethysTestCase.destroy_test_persistent_stores_for_app, + app_class) + + @mock.patch('tethys_apps.base.testing.testing.is_testing_environment') + def test_destroy_test_persistent_stores_for_app_not_subclass(self, mock_ite): + mock_ite.return_value = True + app_class = TestClass + # Execute + self.assertRaises(TypeError, base_testing.TethysTestCase.destroy_test_persistent_stores_for_app, app_class) + + @mock.patch('django.contrib.auth.models.User') + def test_create_test_user(self, mock_user): + mock_user.objects.create_user = mock.MagicMock(return_value='test_create_user') + + result = base_testing.TethysTestCase.create_test_user(username='test_user', password='test_pass', + email='test_e') + + # Check result + self.assertEqual('test_create_user', result) + mock_user.objects.create_user.assert_called_with(username='test_user', password='test_pass', email='test_e') + + @mock.patch('django.contrib.auth.models.User') + def test_create_test_super_user(self, mock_user): + mock_user.objects.create_superuser = mock.MagicMock(return_value='test_create_super_user') + + result = base_testing.TethysTestCase.create_test_superuser(username='test_user', password='test_pass', + email='test_e') + + # Check result + self.assertEqual('test_create_super_user', result) + mock_user.objects.create_superuser.assert_called_with(username='test_user', password='test_pass', + email='test_e') + + @mock.patch('tethys_apps.base.testing.testing.Client') + def test_get_test_client(self, mock_client): + mock_client.return_value = 'test_get_client' + + result = base_testing.TethysTestCase.get_test_client() + + # Check result + self.assertEqual('test_get_client', result) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_url_map.py b/tests/unit_tests/test_tethys_apps/test_base/test_url_map.py new file mode 100644 index 000000000..61ad9798a --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_base/test_url_map.py @@ -0,0 +1,64 @@ +import unittest +import tethys_apps.base.url_map as base_url_map + + +class TestUrlMap(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_UrlMapBase(self): + name = 'test_name' + url = '/example/resource/{variable_name}/' + expected_url = r'^example/resource/(?P[0-9A-Za-z-_.]+)//$' + controller = 'test_controller' + + result = base_url_map.UrlMapBase(name=name, url=url, controller=controller) + + # Check Result + self.assertEqual(name, result.name) + self.assertEqual(expected_url, result.url) + self.assertEqual(controller, result.controller) + + # TEST regex-case1 + regex = '[0-9A-Z]+' + expected_url = '^example/resource/(?P[0-9A-Z]+)//$' + + result = base_url_map.UrlMapBase(name=name, url=url, controller=controller, regex=regex) + self.assertEqual(expected_url, result.url) + + # TEST regex-case2 + regex = ['[0-9A-Z]+', '[0-8A-W]+'] + url = '/example/resource/{variable_name}/{variable_name2}/' + expected_url = '^example/resource/(?P[0-9A-Z]+)/(?P[0-8A-W]+)//$' + + result = base_url_map.UrlMapBase(name=name, url=url, controller=controller, regex=regex) + self.assertEqual(expected_url, result.url) + + # TEST regex-case3 + regex = ['[0-9A-Z]+'] + url = '/example/resource/{variable_name}/{variable_name2}/' + expected_url = '^example/resource/(?P[0-9A-Z]+)/(?P[0-9A-Z]+)//$' + + result = base_url_map.UrlMapBase(name=name, url=url, controller=controller, regex=regex) + self.assertEqual(expected_url, result.url) + + # TEST __repre__ + expected_result = '[0-9A-Za-z-_.]+)/' \ + '(?P[0-9A-Za-z-_.]+)//$, controller=test_controller>' + result = base_url_map.UrlMapBase(name=name, url=url, controller=controller).__repr__() + self.assertEqual(expected_result, result) + + # TEST empty url + regex = ['[0-9A-Z]+'] + url = '' + expected_url = '^$' + + result = base_url_map.UrlMapBase(name=name, url=url, controller=controller, regex=regex) + self.assertEqual(expected_url, result.url) + + def test_UrlMapBase_value_error(self): + self.assertRaises(ValueError, base_url_map.UrlMapBase, name='1', url='2', + controller='3', regex={'1': '2'}) diff --git a/tests/unit_tests/test_tethys_apps/test_base/test_workspace.py b/tests/unit_tests/test_tethys_apps/test_base/test_workspace.py new file mode 100644 index 000000000..f0a73cd00 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_base/test_workspace.py @@ -0,0 +1,86 @@ +import unittest +import tethys_apps.base.workspace as base_workspace +import os +import shutil + + +class TestUrlMap(unittest.TestCase): + def setUp(self): + self.root = os.path.abspath(os.path.dirname(__file__)) + self.test_root = os.path.join(self.root, 'test_workspace') + self.test_root_a = os.path.join(self.test_root, 'test_workspace_a') + self.test_root2 = os.path.join(self.root, 'test_workspace2') + + def tearDown(self): + if os.path.isdir(self.test_root): + shutil.rmtree(self.test_root) + if os.path.isdir(self.test_root2): + shutil.rmtree(self.test_root2) + + def test_TethysWorkspace(self): + # Test Create new workspace folder test_workspace + result = base_workspace.TethysWorkspace(path=self.test_root) + workspace = ''.format(self.test_root) + + # Create new folder inside test_workspace + base_workspace.TethysWorkspace(path=self.test_root_a) + + # Create new folder test_workspace2 + base_workspace.TethysWorkspace(path=self.test_root2) + + self.assertEqual(result.__repr__(), workspace) + self.assertEqual(result.path, self.test_root) + + # Create Files + file_list = ['test1.txt', 'test2.txt'] + for file_name in file_list: + # Create file + open(os.path.join(self.test_root, file_name), 'a').close() + + # Test files with full path + result = base_workspace.TethysWorkspace(path=self.test_root).files(full_path=True) + for file_name in file_list: + self.assertIn(os.path.join(self.test_root, file_name), result) + + # Test files without full path + result = base_workspace.TethysWorkspace(path=self.test_root).files() + for file_name in file_list: + self.assertIn(file_name, result) + + # Test Directories with full path + result = base_workspace.TethysWorkspace(path=self.root).directories(full_path=True) + self.assertIn(self.test_root, result) + self.assertIn(self.test_root2, result) + + # Test Directories without full path + result = base_workspace.TethysWorkspace(path=self.root).directories() + self.assertIn('test_workspace', result) + self.assertIn('test_workspace2', result) + self.assertNotIn(self.test_root, result) + self.assertNotIn(self.test_root2, result) + + # Test Remove file + base_workspace.TethysWorkspace(path=self.test_root).remove('test2.txt') + + # Verify that the file has been remove + self.assertFalse(os.path.isfile(os.path.join(self.test_root, 'test2.txt'))) + + # Test Remove Directory + base_workspace.TethysWorkspace(path=self.root).remove(self.test_root2) + + # Verify that the Directory has been remove + self.assertFalse(os.path.isdir(self.test_root2)) + + # Test Clear + base_workspace.TethysWorkspace(path=self.test_root).clear() + + # Verify that the Directory has been remove + self.assertFalse(os.path.isdir(self.test_root_a)) + + # Verify that the File has been remove + self.assertFalse(os.path.isfile(os.path.join(self.test_root, 'test1.txt'))) + + # Test don't allow overwriting the path property + workspace = base_workspace.TethysWorkspace(path=self.test_root) + workspace.path = 'foo' + self.assertEqual(self.test_root, workspace.path) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/__init__.py b/tests/unit_tests/test_tethys_apps/test_cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test__init__.py b/tests/unit_tests/test_tethys_apps/test_cli/test__init__.py new file mode 100644 index 000000000..9cf95c3fd --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test__init__.py @@ -0,0 +1,1220 @@ +import sys +import unittest +import mock +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +from tethys_apps.cli import tethys_command + + +class TethysCommandTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def assert_returns_help(self, stdout): + self.assertIn('usage: tethys', stdout) + self.assertIn('scaffold', stdout) + self.assertIn('gen', stdout) + self.assertIn('manage', stdout) + self.assertIn('schedulers', stdout) + self.assertIn('services', stdout) + self.assertIn('app_settings', stdout) + self.assertIn('link', stdout) + self.assertIn('test', stdout) + self.assertIn('uninstall', stdout) + self.assertIn('list', stdout) + self.assertIn('syncstores', stdout) + self.assertIn('docker', stdout) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('sys.stderr', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + def test_tethys_with_no_subcommand(self, mock_exit, mock_stderr, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + if mock_stdout.getvalue(): + # Python 3 + self.assert_returns_help(mock_stdout.getvalue()) + else: + # Python 2 + self.assert_returns_help(mock_stderr.getvalue()) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + def test_tethys_help(self, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_exit.assert_called_with(0) + self.assert_returns_help(mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.scaffold_command') + def test_scaffold_subcommand(self, mock_scaffold_command): + testargs = ['tethys', 'scaffold', 'foo'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_scaffold_command.assert_called() + call_args = mock_scaffold_command.call_args_list + self.assertEqual('foo', call_args[0][0][0].name) + self.assertEqual('default', call_args[0][0][0].template) + self.assertFalse(call_args[0][0][0].overwrite) + self.assertFalse(call_args[0][0][0].extension) + self.assertFalse(call_args[0][0][0].use_defaults) + + @mock.patch('tethys_apps.cli.scaffold_command') + def test_scaffold_subcommand_with_options(self, mock_scaffold_command): + testargs = ['tethys', 'scaffold', 'foo', '-e', '-t', 'my_template', '-o', '-d'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_scaffold_command.assert_called() + call_args = mock_scaffold_command.call_args_list + self.assertEqual('foo', call_args[0][0][0].name) + self.assertEqual('my_template', call_args[0][0][0].template) + self.assertTrue(call_args[0][0][0].overwrite) + self.assertTrue(call_args[0][0][0].extension) + self.assertTrue(call_args[0][0][0].use_defaults) + + @mock.patch('tethys_apps.cli.scaffold_command') + def test_scaffold_subcommand_with_verbose_options(self, mock_scaffold_command): + testargs = ['tethys', 'scaffold', 'foo', '--extension', '--template', 'my_template', '--overwrite', + '--defaults'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_scaffold_command.assert_called() + call_args = mock_scaffold_command.call_args_list + self.assertEqual('foo', call_args[0][0][0].name) + self.assertEqual('my_template', call_args[0][0][0].template) + self.assertTrue(call_args[0][0][0].overwrite) + self.assertTrue(call_args[0][0][0].extension) + self.assertTrue(call_args[0][0][0].use_defaults) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.scaffold_command') + def test_scaffold_subcommand_help(self, mock_scaffold_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'scaffold', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_scaffold_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--template', mock_stdout.getvalue()) + self.assertIn('--extension', mock_stdout.getvalue()) + self.assertIn('--defaults', mock_stdout.getvalue()) + self.assertIn('--overwrite', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.generate_command') + def test_generate_subcommand_settings_defaults(self, mock_gen_command): + testargs = ['tethys', 'gen', 'settings'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_gen_command.assert_called() + call_args = mock_gen_command.call_args_list + self.assertEqual(None, call_args[0][0][0].allowed_host) + self.assertEqual(None, call_args[0][0][0].allowed_hosts) + self.assertEqual('75M', call_args[0][0][0].client_max_body_size) + self.assertEqual('pass', call_args[0][0][0].db_password) + self.assertEqual(5436, call_args[0][0][0].db_port) + self.assertEqual('tethys_default', call_args[0][0][0].db_username) + self.assertEqual(None, call_args[0][0][0].directory) + self.assertFalse(call_args[0][0][0].overwrite) + self.assertFalse(call_args[0][0][0].production) + self.assertEqual('settings', call_args[0][0][0].type) + self.assertEqual(10, call_args[0][0][0].uwsgi_processes) + + @mock.patch('tethys_apps.cli.generate_command') + def test_generate_subcommand_settings_directory(self, mock_gen_command): + testargs = ['tethys', 'gen', 'settings', '--directory', '/tmp/foo/bar'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_gen_command.assert_called() + call_args = mock_gen_command.call_args_list + self.assertEqual(None, call_args[0][0][0].allowed_host) + self.assertEqual(None, call_args[0][0][0].allowed_hosts) + self.assertEqual('75M', call_args[0][0][0].client_max_body_size) + self.assertEqual('pass', call_args[0][0][0].db_password) + self.assertEqual(5436, call_args[0][0][0].db_port) + self.assertEqual('tethys_default', call_args[0][0][0].db_username) + self.assertEqual('/tmp/foo/bar', call_args[0][0][0].directory) + self.assertFalse(call_args[0][0][0].overwrite) + self.assertFalse(call_args[0][0][0].production) + self.assertEqual('settings', call_args[0][0][0].type) + self.assertEqual(10, call_args[0][0][0].uwsgi_processes) + + @mock.patch('tethys_apps.cli.generate_command') + def test_generate_subcommand_apache_settings_verbose_options(self, mock_gen_command): + testargs = ['tethys', 'gen', 'apache', '-d', '/tmp/foo/bar', '--allowed-host', '127.0.0.1', + '--allowed-hosts', 'localhost', '--client-max-body-size', '123M', '--uwsgi-processes', '9', + '--db-username', 'foo_user', '--db-password', 'foo_pass', '--db-port', '5555', + '--production', '--overwrite'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_gen_command.assert_called() + call_args = mock_gen_command.call_args_list + self.assertEqual('127.0.0.1', call_args[0][0][0].allowed_host) + self.assertEqual('localhost', call_args[0][0][0].allowed_hosts) + self.assertEqual('123M', call_args[0][0][0].client_max_body_size) + self.assertEqual('foo_pass', call_args[0][0][0].db_password) + self.assertEqual('5555', call_args[0][0][0].db_port) + self.assertEqual('foo_user', call_args[0][0][0].db_username) + self.assertEqual('/tmp/foo/bar', call_args[0][0][0].directory) + self.assertTrue(call_args[0][0][0].overwrite) + self.assertTrue(call_args[0][0][0].production) + self.assertEqual('apache', call_args[0][0][0].type) + self.assertEqual('9', call_args[0][0][0].uwsgi_processes) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.generate_command') + def test_generate_subcommand_help(self, mock_gen_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'gen', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_gen_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--directory', mock_stdout.getvalue()) + self.assertIn('--allowed-host', mock_stdout.getvalue()) + self.assertIn('--client-max-body-size', mock_stdout.getvalue()) + self.assertIn('--uwsgi-processes', mock_stdout.getvalue()) + self.assertIn('--db-username', mock_stdout.getvalue()) + self.assertIn('--db-password', mock_stdout.getvalue()) + self.assertIn('--db-port', mock_stdout.getvalue()) + self.assertIn('--production', mock_stdout.getvalue()) + self.assertIn('--overwrite', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.manage_command') + def test_manage_subcommand_start(self, mock_manage_command): + testargs = ['tethys', 'manage', 'start'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_manage_command.assert_called() + call_args = mock_manage_command.call_args_list + self.assertEqual('start', call_args[0][0][0].command) + self.assertEqual(False, call_args[0][0][0].force) + self.assertEqual(None, call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].noinput) + self.assertEqual(None, call_args[0][0][0].port) + + @mock.patch('tethys_apps.cli.manage_command') + def test_manage_subcommand_start_options(self, mock_manage_command): + testargs = ['tethys', 'manage', 'start', '-m', '/foo/bar/manage.py', '-p', '5555', '-f'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_manage_command.assert_called() + call_args = mock_manage_command.call_args_list + self.assertEqual('start', call_args[0][0][0].command) + self.assertEqual(True, call_args[0][0][0].force) + self.assertEqual('/foo/bar/manage.py', call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].noinput) + self.assertEqual('5555', call_args[0][0][0].port) + + @mock.patch('tethys_apps.cli.manage_command') + def test_manage_subcommand_start_verbose_options(self, mock_manage_command): + testargs = ['tethys', 'manage', 'start', '--manage', '/foo/bar/manage.py', '--port', '5555', '--force', + '--noinput'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_manage_command.assert_called() + call_args = mock_manage_command.call_args_list + self.assertEqual('start', call_args[0][0][0].command) + self.assertEqual(True, call_args[0][0][0].force) + self.assertEqual('/foo/bar/manage.py', call_args[0][0][0].manage) + self.assertEqual(True, call_args[0][0][0].noinput) + self.assertEqual('5555', call_args[0][0][0].port) + + @mock.patch('tethys_apps.cli.manage_command') + def test_manage_subcommand_syncdb(self, mock_manage_command): + testargs = ['tethys', 'manage', 'syncdb'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_manage_command.assert_called() + call_args = mock_manage_command.call_args_list + self.assertEqual('syncdb', call_args[0][0][0].command) + self.assertEqual(False, call_args[0][0][0].force) + self.assertEqual(None, call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].noinput) + self.assertEqual(None, call_args[0][0][0].port) + + @mock.patch('tethys_apps.cli.manage_command') + def test_manage_subcommand_collectstatic(self, mock_manage_command): + testargs = ['tethys', 'manage', 'collectstatic'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_manage_command.assert_called() + call_args = mock_manage_command.call_args_list + self.assertEqual('collectstatic', call_args[0][0][0].command) + self.assertEqual(False, call_args[0][0][0].force) + self.assertEqual(None, call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].noinput) + self.assertEqual(None, call_args[0][0][0].port) + + @mock.patch('tethys_apps.cli.manage_command') + def test_manage_subcommand_collectworkspaces(self, mock_manage_command): + testargs = ['tethys', 'manage', 'collectworkspaces'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_manage_command.assert_called() + call_args = mock_manage_command.call_args_list + self.assertEqual('collectworkspaces', call_args[0][0][0].command) + self.assertEqual(False, call_args[0][0][0].force) + self.assertEqual(None, call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].noinput) + self.assertEqual(None, call_args[0][0][0].port) + + @mock.patch('tethys_apps.cli.manage_command') + def test_manage_subcommand_collectall(self, mock_manage_command): + testargs = ['tethys', 'manage', 'collectall'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_manage_command.assert_called() + call_args = mock_manage_command.call_args_list + self.assertEqual('collectall', call_args[0][0][0].command) + self.assertEqual(False, call_args[0][0][0].force) + self.assertEqual(None, call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].noinput) + self.assertEqual(None, call_args[0][0][0].port) + + @mock.patch('tethys_apps.cli.manage_command') + def test_manage_subcommand_createsuperuser(self, mock_manage_command): + testargs = ['tethys', 'manage', 'createsuperuser'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_manage_command.assert_called() + call_args = mock_manage_command.call_args_list + self.assertEqual('createsuperuser', call_args[0][0][0].command) + self.assertEqual(False, call_args[0][0][0].force) + self.assertEqual(None, call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].noinput) + self.assertEqual(None, call_args[0][0][0].port) + + @mock.patch('tethys_apps.cli.manage_command') + def test_manage_subcommand_sync(self, mock_manage_command): + testargs = ['tethys', 'manage', 'sync'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_manage_command.assert_called() + call_args = mock_manage_command.call_args_list + self.assertEqual('sync', call_args[0][0][0].command) + self.assertEqual(False, call_args[0][0][0].force) + self.assertEqual(None, call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].noinput) + self.assertEqual(None, call_args[0][0][0].port) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.manage_command') + def test_manage_subcommand_help(self, mock_manage_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'manage', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_manage_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--manage', mock_stdout.getvalue()) + self.assertIn('--port', mock_stdout.getvalue()) + self.assertIn('--noinput', mock_stdout.getvalue()) + self.assertIn('--force', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.scheduler_create_command') + def test_scheduler_create_command_options(self, mock_scheduler_create_command): + testargs = ['tethys', 'schedulers', 'create', '-n', 'foo_name', '-e', 'http://foo.foo_endpoint', + '-u', 'foo_user', '-p', 'foo_pass', '-k', 'private_foo_pass'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_scheduler_create_command.assert_called() + call_args = mock_scheduler_create_command.call_args_list + self.assertEqual('http://foo.foo_endpoint', call_args[0][0][0].endpoint) + self.assertEqual('foo_name', call_args[0][0][0].name) + self.assertEqual('foo_pass', call_args[0][0][0].password) + self.assertEqual('private_foo_pass', call_args[0][0][0].private_key_pass) + self.assertEqual(None, call_args[0][0][0].private_key_path) + self.assertEqual('foo_user', call_args[0][0][0].username) + + @mock.patch('tethys_apps.cli.scheduler_create_command') + def test_scheduler_create_command_verbose_options(self, mock_scheduler_create_command): + testargs = ['tethys', 'schedulers', 'create', '--name', 'foo_name', '--endpoint', 'http://foo.foo_endpoint', + '--username', 'foo_user', '--private-key-path', 'private_foo_path'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_scheduler_create_command.assert_called() + call_args = mock_scheduler_create_command.call_args_list + self.assertEqual('http://foo.foo_endpoint', call_args[0][0][0].endpoint) + self.assertEqual('foo_name', call_args[0][0][0].name) + self.assertEqual(None, call_args[0][0][0].password) + self.assertEqual(None, call_args[0][0][0].private_key_pass) + self.assertEqual('private_foo_path', call_args[0][0][0].private_key_path) + self.assertEqual('foo_user', call_args[0][0][0].username) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.scheduler_create_command') + def test_scheduler_create_command_help(self, mock_scheduler_create_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'schedulers', 'create', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_scheduler_create_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--name', mock_stdout.getvalue()) + self.assertIn('--endpoint', mock_stdout.getvalue()) + self.assertIn('--username', mock_stdout.getvalue()) + self.assertIn('--password', mock_stdout.getvalue()) + self.assertIn('--private-key-path', mock_stdout.getvalue()) + self.assertIn('--private-key-pass', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.schedulers_list_command') + def test_scheduler_list_command(self, mock_scheduler_list_command): + testargs = ['tethys', 'schedulers', 'list'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_scheduler_list_command.assert_called() + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.schedulers_list_command') + def test_scheduler_list_command_help(self, mock_scheduler_list_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'schedulers', 'list', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_scheduler_list_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.schedulers_remove_command') + def test_scheduler_remove_command(self, mock_scheduler_remove_command): + testargs = ['tethys', 'schedulers', 'remove', 'foo_name'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_scheduler_remove_command.assert_called() + call_args = mock_scheduler_remove_command.call_args_list + self.assertEqual(False, call_args[0][0][0].force) + self.assertEqual('foo_name', call_args[0][0][0].scheduler_name) + + @mock.patch('tethys_apps.cli.schedulers_remove_command') + def test_scheduler_remove_command_options(self, mock_scheduler_remove_command): + testargs = ['tethys', 'schedulers', 'remove', 'foo_name', '-f'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_scheduler_remove_command.assert_called() + call_args = mock_scheduler_remove_command.call_args_list + self.assertEqual(True, call_args[0][0][0].force) + self.assertEqual('foo_name', call_args[0][0][0].scheduler_name) + + @mock.patch('tethys_apps.cli.schedulers_remove_command') + def test_scheduler_remove_command_verbose_options(self, mock_scheduler_remove_command): + testargs = ['tethys', 'schedulers', 'remove', 'foo_name', '--force'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_scheduler_remove_command.assert_called() + call_args = mock_scheduler_remove_command.call_args_list + self.assertEqual(True, call_args[0][0][0].force) + self.assertEqual('foo_name', call_args[0][0][0].scheduler_name) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.schedulers_remove_command') + def test_scheduler_list_command_help_2(self, mock_scheduler_remove_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'schedulers', 'remove', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_scheduler_remove_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--force', mock_stdout.getvalue()) + self.assertIn('scheduler_name', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.services_remove_persistent_command') + def test_services_remove_persistent_command(self, mock_services_remove_persistent_command): + testargs = ['tethys', 'services', 'remove', 'persistent', 'foo_service_uid'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_remove_persistent_command.assert_called() + call_args = mock_services_remove_persistent_command.call_args_list + self.assertEqual(False, call_args[0][0][0].force) + self.assertEqual('foo_service_uid', call_args[0][0][0].service_uid) + + @mock.patch('tethys_apps.cli.services_remove_persistent_command') + def test_services_remove_persistent_command_options(self, mock_services_remove_persistent_command): + testargs = ['tethys', 'services', 'remove', 'persistent', '-f', 'foo_service_uid'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_remove_persistent_command.assert_called() + call_args = mock_services_remove_persistent_command.call_args_list + self.assertEqual(True, call_args[0][0][0].force) + self.assertEqual('foo_service_uid', call_args[0][0][0].service_uid) + + @mock.patch('tethys_apps.cli.services_remove_persistent_command') + def test_services_remove_persistent_command_verbose_options(self, mock_services_remove_persistent_command): + testargs = ['tethys', 'services', 'remove', 'persistent', '--force', 'foo_service_uid'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_remove_persistent_command.assert_called() + call_args = mock_services_remove_persistent_command.call_args_list + self.assertEqual(True, call_args[0][0][0].force) + self.assertEqual('foo_service_uid', call_args[0][0][0].service_uid) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.services_remove_persistent_command') + def test_services_remove_persistent_command_help(self, mock_services_remove_persistent_command, mock_exit, + mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'services', 'remove', 'persistent', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_services_remove_persistent_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--force', mock_stdout.getvalue()) + self.assertIn('service_uid', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.services_remove_spatial_command') + def test_services_remove_spatial_command(self, mock_services_remove_spatial_command): + testargs = ['tethys', 'services', 'remove', 'spatial', 'foo_service_uid'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_remove_spatial_command.assert_called() + call_args = mock_services_remove_spatial_command.call_args_list + self.assertEqual(False, call_args[0][0][0].force) + self.assertEqual('foo_service_uid', call_args[0][0][0].service_uid) + + @mock.patch('tethys_apps.cli.services_remove_spatial_command') + def test_services_remove_spatial_command_options(self, mock_services_remove_spatial_command): + testargs = ['tethys', 'services', 'remove', 'spatial', '-f', 'foo_service_uid'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_remove_spatial_command.assert_called() + call_args = mock_services_remove_spatial_command.call_args_list + self.assertEqual(True, call_args[0][0][0].force) + self.assertEqual('foo_service_uid', call_args[0][0][0].service_uid) + + @mock.patch('tethys_apps.cli.services_remove_spatial_command') + def test_services_remove_spatial_command_verbose_options(self, mock_services_remove_spatial_command): + testargs = ['tethys', 'services', 'remove', 'spatial', '--force', 'foo_service_uid'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_remove_spatial_command.assert_called() + call_args = mock_services_remove_spatial_command.call_args_list + self.assertEqual(True, call_args[0][0][0].force) + self.assertEqual('foo_service_uid', call_args[0][0][0].service_uid) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.services_remove_spatial_command') + def test_services_remove_spatial_command_help(self, mock_services_remove_spatial_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'services', 'remove', 'spatial', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_services_remove_spatial_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--force', mock_stdout.getvalue()) + self.assertIn('service_uid', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.services_create_persistent_command') + def test_services_create_persistent_command_options(self, mock_services_create_persistent_command): + testargs = ['tethys', 'services', 'create', 'persistent', '-n', 'foo_name', '-c', 'foo:pass@foo.bar:5555'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_create_persistent_command.assert_called() + call_args = mock_services_create_persistent_command.call_args_list + self.assertEqual('foo:pass@foo.bar:5555', call_args[0][0][0].connection) + self.assertEqual('foo_name', call_args[0][0][0].name) + + @mock.patch('tethys_apps.cli.services_create_persistent_command') + def test_services_create_persistent_command_verbose_options(self, mock_services_create_persistent_command): + testargs = ['tethys', 'services', 'create', 'persistent', '--name', 'foo_name', + '--connection', 'foo:pass@foo.bar:5555'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_create_persistent_command.assert_called() + call_args = mock_services_create_persistent_command.call_args_list + self.assertEqual('foo:pass@foo.bar:5555', call_args[0][0][0].connection) + self.assertEqual('foo_name', call_args[0][0][0].name) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.services_create_persistent_command') + def test_services_create_persistent_command_help(self, mock_services_create_persistent_command, mock_exit, + mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'services', 'create', 'persistent', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_services_create_persistent_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--name', mock_stdout.getvalue()) + self.assertIn('--connection', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.services_create_spatial_command') + def test_services_create_spatial_command_options(self, mock_services_create_spatial_command): + testargs = ['tethys', 'services', 'create', 'spatial', '-n', 'foo_name', '-c', 'foo:pass@http://foo.bar:5555'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_create_spatial_command.assert_called() + call_args = mock_services_create_spatial_command.call_args_list + self.assertEqual(None, call_args[0][0][0].apikey) + self.assertEqual('foo:pass@http://foo.bar:5555', call_args[0][0][0].connection) + self.assertEqual('foo_name', call_args[0][0][0].name) + self.assertEqual(None, call_args[0][0][0].public_endpoint) + + @mock.patch('tethys_apps.cli.services_create_spatial_command') + def test_services_create_spatial_command_verbose_options(self, mock_services_create_spatial_command): + testargs = ['tethys', 'services', 'create', 'spatial', '--name', 'foo_name', + '--connection', 'foo:pass@http://foo.bar:5555', '--public-endpoint', 'foo.bar:1234', + '--apikey', 'foo_apikey'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_create_spatial_command.assert_called() + call_args = mock_services_create_spatial_command.call_args_list + self.assertEqual('foo_apikey', call_args[0][0][0].apikey) + self.assertEqual('foo:pass@http://foo.bar:5555', call_args[0][0][0].connection) + self.assertEqual('foo_name', call_args[0][0][0].name) + self.assertEqual('foo.bar:1234', call_args[0][0][0].public_endpoint) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.services_create_spatial_command') + def test_services_create_spatial_command_help(self, mock_services_create_spatial_command, mock_exit, + mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'services', 'create', 'spatial', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_services_create_spatial_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--name', mock_stdout.getvalue()) + self.assertIn('--connection', mock_stdout.getvalue()) + self.assertIn('--public-endpoint', mock_stdout.getvalue()) + self.assertIn('--apikey', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.services_list_command') + def test_services_list_command(self, mock_services_list_command): + testargs = ['tethys', 'services', 'list'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_list_command.assert_called() + call_args = mock_services_list_command.call_args_list + self.assertEqual(False, call_args[0][0][0].persistent) + self.assertEqual(False, call_args[0][0][0].spatial) + + @mock.patch('tethys_apps.cli.services_list_command') + def test_services_list_command_options(self, mock_services_list_command): + testargs = ['tethys', 'services', 'list', '-p'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_list_command.assert_called() + call_args = mock_services_list_command.call_args_list + self.assertEqual(True, call_args[0][0][0].persistent) + self.assertEqual(False, call_args[0][0][0].spatial) + + @mock.patch('tethys_apps.cli.services_list_command') + def test_services_list_command_verbose_options(self, mock_services_list_command): + testargs = ['tethys', 'services', 'list', '--spatial'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_services_list_command.assert_called() + call_args = mock_services_list_command.call_args_list + self.assertEqual(False, call_args[0][0][0].persistent) + self.assertEqual(True, call_args[0][0][0].spatial) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.services_list_command') + def test_services_list_command_help(self, mock_services_list_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'services', 'list', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_services_list_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--persistent', mock_stdout.getvalue()) + self.assertIn('--spatial', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.app_settings_list_command') + def test_app_settings_list_command(self, mock_app_settings_list_command): + testargs = ['tethys', 'app_settings', 'list', 'foo_app'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_app_settings_list_command.assert_called() + call_args = mock_app_settings_list_command.call_args_list + self.assertEqual('foo_app', call_args[0][0][0].app) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.app_settings_list_command') + def test_app_settings_list_command_help(self, mock_app_settings_list_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'app_settings', 'list', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_app_settings_list_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.app_settings_create_ps_database_command') + def test_app_settings_create_command_options(self, mock_app_settings_create_command): + testargs = ['tethys', 'app_settings', 'create', '-a', 'foo_app_package', '-n', 'foo', '-d', 'foo description', + 'ps_database', '-s', '-y'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_app_settings_create_command.assert_called() + call_args = mock_app_settings_create_command.call_args_list + self.assertEqual('foo_app_package', call_args[0][0][0].app) + self.assertEqual('foo description', call_args[0][0][0].description) + self.assertEqual(True, call_args[0][0][0].dynamic) + self.assertEqual(False, call_args[0][0][0].initialized) + self.assertEqual(None, call_args[0][0][0].initializer) + self.assertEqual('foo', call_args[0][0][0].name) + self.assertEqual(False, call_args[0][0][0].required) + self.assertEqual(True, call_args[0][0][0].spatial) + + @mock.patch('tethys_apps.cli.app_settings_create_ps_database_command') + def test_app_settings_create_command_verbose_options(self, mock_app_settings_create_command): + testargs = ['tethys', 'app_settings', 'create', '--app', 'foo_app_package', '--name', 'foo', '--description', + 'foo description', 'ps_database', '--spatial', '--dynamic'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_app_settings_create_command.assert_called() + call_args = mock_app_settings_create_command.call_args_list + self.assertEqual('foo_app_package', call_args[0][0][0].app) + self.assertEqual('foo description', call_args[0][0][0].description) + self.assertEqual(True, call_args[0][0][0].dynamic) + self.assertEqual(False, call_args[0][0][0].initialized) + self.assertEqual(None, call_args[0][0][0].initializer) + self.assertEqual('foo', call_args[0][0][0].name) + self.assertEqual(False, call_args[0][0][0].required) + self.assertEqual(True, call_args[0][0][0].spatial) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.app_settings_create_ps_database_command') + def test_app_settings_create_command_help(self, mock_app_settings_create_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'app_settings', 'create', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_app_settings_create_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--app', mock_stdout.getvalue()) + self.assertIn('--name', mock_stdout.getvalue()) + self.assertIn('--description', mock_stdout.getvalue()) + self.assertIn('--required', mock_stdout.getvalue()) + self.assertIn('--initializer', mock_stdout.getvalue()) + self.assertIn('--initialized', mock_stdout.getvalue()) + self.assertIn('{ps_database}', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.app_settings_remove_command') + def test_app_settings_create_command_options_2(self, mock_app_settings_remove_command): + testargs = ['tethys', 'app_settings', 'remove', '-n', 'foo', '-f', 'foo_app'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_app_settings_remove_command.assert_called() + call_args = mock_app_settings_remove_command.call_args_list + self.assertEqual('foo_app', call_args[0][0][0].app) + self.assertEqual(True, call_args[0][0][0].force) + self.assertEqual('foo', call_args[0][0][0].name) + + @mock.patch('tethys_apps.cli.app_settings_remove_command') + def test_app_settings_create_command_verbose_options_2(self, mock_app_settings_remove_command): + testargs = ['tethys', 'app_settings', 'remove', '--name', 'foo', '--force', 'foo_app'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_app_settings_remove_command.assert_called() + call_args = mock_app_settings_remove_command.call_args_list + self.assertEqual('foo_app', call_args[0][0][0].app) + self.assertEqual(True, call_args[0][0][0].force) + self.assertEqual('foo', call_args[0][0][0].name) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.app_settings_remove_command') + def test_app_settings_create_command_help_2(self, mock_app_settings_remove_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'app_settings', 'remove', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_app_settings_remove_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('', mock_stdout.getvalue()) + self.assertIn('--name', mock_stdout.getvalue()) + self.assertIn('--force', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.link_command') + def test_link_command(self, mock_link_command): + testargs = ['tethys', 'link', 'spatial:foo_service', 'foo_package:database:foo_2'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_link_command.assert_called() + call_args = mock_link_command.call_args_list + self.assertEqual('spatial:foo_service', call_args[0][0][0].service) + self.assertEqual('foo_package:database:foo_2', call_args[0][0][0].setting) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.link_command') + def test_link_command_help(self, mock_link_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'link', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_link_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('service', mock_stdout.getvalue()) + self.assertIn('setting', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.tstc') + def test_test_command(self, mock_test_command): + testargs = ['tethys', 'test'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_test_command.assert_called() + call_args = mock_test_command.call_args_list + self.assertEqual(False, call_args[0][0][0].coverage) + self.assertEqual(False, call_args[0][0][0].coverage_html) + self.assertEqual(None, call_args[0][0][0].file) + self.assertEqual(False, call_args[0][0][0].gui) + self.assertEqual(False, call_args[0][0][0].unit) + + @mock.patch('tethys_apps.cli.tstc') + def test_test_command_options(self, mock_test_command): + testargs = ['tethys', 'test', '-c', '-C', '-u', '-g', '-f', 'foo.bar'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_test_command.assert_called() + call_args = mock_test_command.call_args_list + self.assertEqual(True, call_args[0][0][0].coverage) + self.assertEqual(True, call_args[0][0][0].coverage_html) + self.assertEqual('foo.bar', call_args[0][0][0].file) + self.assertEqual(True, call_args[0][0][0].gui) + self.assertEqual(True, call_args[0][0][0].unit) + + @mock.patch('tethys_apps.cli.tstc') + def test_test_command_options_verbose(self, mock_test_command): + testargs = ['tethys', 'test', '--coverage', '--coverage-html', '--unit', '--gui', '--file', 'foo.bar'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_test_command.assert_called() + call_args = mock_test_command.call_args_list + self.assertEqual(True, call_args[0][0][0].coverage) + self.assertEqual(True, call_args[0][0][0].coverage_html) + self.assertEqual('foo.bar', call_args[0][0][0].file) + self.assertEqual(True, call_args[0][0][0].gui) + self.assertEqual(True, call_args[0][0][0].unit) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.tstc') + def test_test_command_help(self, mock_test_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'test', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_test_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--coverage', mock_stdout.getvalue()) + self.assertIn('--coverage-html', mock_stdout.getvalue()) + self.assertIn('--unit', mock_stdout.getvalue()) + self.assertIn('--gui', mock_stdout.getvalue()) + self.assertIn('--file', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.uc') + def test_uninstall_command(self, mock_uninstall_command): + testargs = ['tethys', 'uninstall', 'foo_app'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_uninstall_command.assert_called() + call_args = mock_uninstall_command.call_args_list + self.assertEqual('foo_app', call_args[0][0][0].app_or_extension) + self.assertEqual(False, call_args[0][0][0].is_extension) + + @mock.patch('tethys_apps.cli.uc') + def test_uninstall_command_options(self, mock_uninstall_command): + testargs = ['tethys', 'uninstall', '-e', 'foo_ext'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_uninstall_command.assert_called() + call_args = mock_uninstall_command.call_args_list + self.assertEqual('foo_ext', call_args[0][0][0].app_or_extension) + self.assertEqual(True, call_args[0][0][0].is_extension) + + @mock.patch('tethys_apps.cli.uc') + def test_uninstall_command_verbose_options(self, mock_uninstall_command): + testargs = ['tethys', 'uninstall', '--extension', 'foo_ext'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_uninstall_command.assert_called() + call_args = mock_uninstall_command.call_args_list + self.assertEqual('foo_ext', call_args[0][0][0].app_or_extension) + self.assertEqual(True, call_args[0][0][0].is_extension) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.uc') + def test_uninstall_command_help(self, mock_uninstall_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'uninstall', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_uninstall_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--extension', mock_stdout.getvalue()) + self.assertIn('app_or_extension', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.lc') + def test_list_command(self, mock_list_command): + testargs = ['tethys', 'list'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_list_command.assert_called() + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.lc') + def test_list_command_help(self, mock_list_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'list', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_list_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.syc') + def test_syncstores_command_single(self, mock_syncstores_command): + testargs = ['tethys', 'syncstores', 'foo_app1'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_syncstores_command.assert_called() + call_args = mock_syncstores_command.call_args_list + self.assertEqual(['foo_app1'], call_args[0][0][0].app) + self.assertEqual(None, call_args[0][0][0].database) + self.assertEqual(False, call_args[0][0][0].firstime) + self.assertEqual(False, call_args[0][0][0].firsttime) + self.assertEqual(None, call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].refresh) + + @mock.patch('tethys_apps.cli.syc') + def test_syncstores_command_multiple(self, mock_syncstores_command): + testargs = ['tethys', 'syncstores', 'foo_app1', 'foo_app2', 'foo_app3'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_syncstores_command.assert_called() + call_args = mock_syncstores_command.call_args_list + self.assertEqual(['foo_app1', 'foo_app2', 'foo_app3'], call_args[0][0][0].app) + self.assertEqual(None, call_args[0][0][0].database) + self.assertEqual(False, call_args[0][0][0].firstime) + self.assertEqual(False, call_args[0][0][0].firsttime) + self.assertEqual(None, call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].refresh) + + @mock.patch('tethys_apps.cli.syc') + def test_syncstores_command_all(self, mock_syncstores_command): + testargs = ['tethys', 'syncstores', 'all'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_syncstores_command.assert_called() + call_args = mock_syncstores_command.call_args_list + self.assertEqual(['all'], call_args[0][0][0].app) + self.assertEqual(None, call_args[0][0][0].database) + self.assertEqual(False, call_args[0][0][0].firstime) + self.assertEqual(False, call_args[0][0][0].firsttime) + self.assertEqual(None, call_args[0][0][0].manage) + self.assertEqual(False, call_args[0][0][0].refresh) + + @mock.patch('tethys_apps.cli.syc') + def test_syncstores_command_options(self, mock_syncstores_command): + testargs = ['tethys', 'syncstores', '-r', '-f', '-d', 'foo_db', '-m', '/foo/bar/manage.py', 'foo_app1'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_syncstores_command.assert_called() + call_args = mock_syncstores_command.call_args_list + self.assertEqual(['foo_app1'], call_args[0][0][0].app) + self.assertEqual('foo_db', call_args[0][0][0].database) + self.assertEqual(False, call_args[0][0][0].firstime) + self.assertEqual(True, call_args[0][0][0].firsttime) + self.assertEqual('/foo/bar/manage.py', call_args[0][0][0].manage) + self.assertEqual(True, call_args[0][0][0].refresh) + + @mock.patch('tethys_apps.cli.syc') + def test_syncstores_command_verbose_options(self, mock_syncstores_command): + testargs = ['tethys', 'syncstores', '--refresh', '--firsttime', '--database', 'foo_db', + '--manage', '/foo/bar/manage.py', 'foo_app1'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_syncstores_command.assert_called() + call_args = mock_syncstores_command.call_args_list + self.assertEqual(['foo_app1'], call_args[0][0][0].app) + self.assertEqual('foo_db', call_args[0][0][0].database) + self.assertEqual(False, call_args[0][0][0].firstime) + self.assertEqual(True, call_args[0][0][0].firsttime) + self.assertEqual('/foo/bar/manage.py', call_args[0][0][0].manage) + self.assertEqual(True, call_args[0][0][0].refresh) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.syc') + def test_syncstores_command_help(self, mock_syncstores_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'syncstores', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_syncstores_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('app', mock_stdout.getvalue()) + self.assertIn('--refresh', mock_stdout.getvalue()) + self.assertIn('--firsttime', mock_stdout.getvalue()) + self.assertIn('--database', mock_stdout.getvalue()) + self.assertIn('--manage', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.cli.docker_command') + def test_docker_command(self, mock_docker_command): + testargs = ['tethys', 'docker', 'init'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_docker_command.assert_called() + call_args = mock_docker_command.call_args_list + self.assertEqual(False, call_args[0][0][0].boot2docker) + self.assertEqual('init', call_args[0][0][0].command) + self.assertEqual(None, call_args[0][0][0].containers) + self.assertEqual(False, call_args[0][0][0].defaults) + + @mock.patch('tethys_apps.cli.docker_command') + def test_docker_command_options(self, mock_docker_command): + testargs = ['tethys', 'docker', 'start', '-c', 'postgis', 'geoserver'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_docker_command.assert_called() + call_args = mock_docker_command.call_args_list + self.assertEqual(False, call_args[0][0][0].boot2docker) + self.assertEqual('start', call_args[0][0][0].command) + self.assertEqual(['postgis', 'geoserver'], call_args[0][0][0].containers) + self.assertEqual(False, call_args[0][0][0].defaults) + + @mock.patch('tethys_apps.cli.docker_command') + def test_docker_command_verbose_options(self, mock_docker_command): + testargs = ['tethys', 'docker', 'stop', '--defaults', '--boot2docker'] + + with mock.patch.object(sys, 'argv', testargs): + tethys_command() + + mock_docker_command.assert_called() + call_args = mock_docker_command.call_args_list + self.assertEqual(True, call_args[0][0][0].boot2docker) + self.assertEqual('stop', call_args[0][0][0].command) + self.assertEqual(None, call_args[0][0][0].containers) + self.assertEqual(True, call_args[0][0][0].defaults) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.cli.argparse._sys.exit') + @mock.patch('tethys_apps.cli.docker_command') + def test_docker_command_help(self, mock_docker_command, mock_exit, mock_stdout): + mock_exit.side_effect = SystemExit + testargs = ['tethys', 'docker', '-h'] + + with mock.patch.object(sys, 'argv', testargs): + self.assertRaises(SystemExit, tethys_command) + + mock_docker_command.assert_not_called() + mock_exit.assert_called_with(0) + + self.assertIn('--help', mock_stdout.getvalue()) + self.assertIn('--defaults', mock_stdout.getvalue()) + self.assertIn('--containers', mock_stdout.getvalue()) + self.assertIn('--boot2docker', mock_stdout.getvalue()) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_app_settings_command.py b/tests/unit_tests/test_tethys_apps/test_cli/test_app_settings_command.py new file mode 100644 index 000000000..199eba86a --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_app_settings_command.py @@ -0,0 +1,246 @@ +import unittest +import mock +from django.core.exceptions import ObjectDoesNotExist +import tethys_apps.cli.app_settings_commands as cli_app_settings_command + + +class TestCliAppSettingsCommand(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.models.TethysApp') + @mock.patch('tethys_apps.models.PersistentStoreConnectionSetting') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting') + @mock.patch('tethys_apps.models.SpatialDatasetServiceSetting') + @mock.patch('tethys_apps.cli.app_settings_commands.pretty_output') + def test_app_settings_list_command(self, mock_pretty_output, MockSdss, MockPsds, MockPscs, MockTethysApp): + # mock the args + mock_arg = mock.MagicMock(app='foo') + + # mock the PersistentStoreConnectionSetting filter return value + MockPscs.objects.filter.return_value = [mock.MagicMock()] + # mock the PersistentStoreDatabaseSetting filter return value + MockPsds.objects.filter.return_value = [mock.MagicMock()] + # mock the SpatialDatasetServiceSetting filter return value + MockSdss.objects.filter.return_value = [mock.MagicMock()] + + cli_app_settings_command.app_settings_list_command(mock_arg) + + MockTethysApp.objects.get(package='foo').return_value = mock_arg.app + + # check TethysApp.object.get method is called with app + MockTethysApp.objects.get.assert_called_with(package='foo') + + # get the app name from mock_ta + app = MockTethysApp.objects.get() + + # check PersistentStoreConnectionSetting.objects.filter method is called with 'app' + MockPscs.objects.filter.assert_called_with(tethys_app=app) + # check PersistentStoreDatabaseSetting.objects.filter method is called with 'app' + MockPsds.objects.filter.assert_called_with(tethys_app=app) + # check SpatialDatasetServiceSetting.objects.filter is called with 'app' + MockSdss.objects.filter.assert_called_with(tethys_app=app) + + # get the called arguments from the mock print + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertIn('Unlinked Settings:', po_call_args[0][0][0]) + self.assertIn('None', po_call_args[1][0][0]) + self.assertIn('Linked Settings:', po_call_args[2][0][0]) + self.assertIn('Name', po_call_args[3][0][0]) + + @mock.patch('tethys_apps.models.TethysApp') + @mock.patch('tethys_apps.models.PersistentStoreConnectionSetting') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting') + @mock.patch('tethys_apps.models.SpatialDatasetServiceSetting') + @mock.patch('tethys_apps.cli.app_settings_commands.pretty_output') + @mock.patch('tethys_apps.cli.app_settings_commands.type') + def test_app_settings_list_command_unlink_settings(self, mock_type, mock_pretty_output, MockSdss, MockPsds, + MockPscs, MockTethysApp): + # mock the args + mock_arg = mock.MagicMock(app='foo') + + # mock the PersistentStoreConnectionSetting filter return value + pscs = MockPscs() + pscs.name = 'n001' + pscs.pk = 'p001' + pscs.persistent_store_service = '' + del pscs.spatial_dataset_service + MockPscs.objects.filter.return_value = [pscs] + + # mock the PersistentStoreDatabaseSetting filter return value + psds = MockPsds() + psds.name = 'n002' + psds.pk = 'p002' + psds.persistent_store_service = '' + del psds.spatial_dataset_service + MockPsds.objects.filter.return_value = [psds] + + # mock the Spatial Dataset ServiceSetting filter return value + sdss = MockSdss() + sdss.name = 'n003' + sdss.pk = 'p003' + sdss.spatial_dataset_service = '' + del sdss.persistent_store_service + MockSdss.objects.filter.return_value = [sdss] + + MockTethysApp.objects.get(package='foo').return_value = mock_arg.app + + def mock_type_func(obj): + if obj is pscs: + return MockPscs + elif obj is psds: + return MockPsds + elif obj is sdss: + return MockSdss + + mock_type.side_effect = mock_type_func + + cli_app_settings_command.app_settings_list_command(mock_arg) + + # check TethysApp.object.get method is called with app + MockTethysApp.objects.get.assert_called_with(package='foo') + + # get the app name from mock_ta + app = MockTethysApp.objects.get() + + # check PersistentStoreConnectionSetting.objects.filter method is called with 'app' + MockPscs.objects.filter.assert_called_with(tethys_app=app) + # check PersistentStoreDatabaseSetting.objects.filter method is called with 'app' + MockPsds.objects.filter.assert_called_with(tethys_app=app) + # check SpatialDatasetServiceSetting.objects.filter is called with 'app' + MockSdss.objects.filter.assert_called_with(tethys_app=app) + + # get the called arguments from the mock print + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertIn('Unlinked Settings:', po_call_args[0][0][0]) + self.assertIn('ID', po_call_args[1][0][0]) + self.assertIn('n001', po_call_args[2][0][0]) + self.assertIn('n002', po_call_args[3][0][0]) + self.assertIn('n003', po_call_args[4][0][0]) + self.assertIn('n003', po_call_args[4][0][0]) + self.assertIn('Linked Settings:', po_call_args[5][0][0]) + self.assertIn('None', po_call_args[6][0][0]) + + @mock.patch('tethys_apps.models.TethysApp') + @mock.patch('tethys_apps.cli.app_settings_commands.pretty_output') + def test_app_settings_list_command_object_does_not_exist(self, mock_pretty_output, MockTethysApp): + # mock the args + mock_arg = mock.MagicMock(app='foo') + MockTethysApp.objects.get.side_effect = ObjectDoesNotExist + + # raise ObjectDoesNotExist error + cli_app_settings_command.app_settings_list_command(mock_arg) + + # get the called arguments from the mock print + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertIn('The app you specified ("foo") does not exist. Command aborted.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.models.TethysApp') + @mock.patch('tethys_apps.cli.app_settings_commands.pretty_output') + def test_app_settings_list_command_object_exception(self, mock_pretty_output, MockTethysApp): + # mock the args + mock_arg = mock.MagicMock(app='foo') + + MockTethysApp.objects.get.side_effect = Exception + + # raise ObjectDoesNotExist error + cli_app_settings_command.app_settings_list_command(mock_arg) + + # get the called arguments from the mock print + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertIn('Something went wrong. Please try again', po_call_args[1][0][0]) + + # @mock.patch('tethys_apps.cli.app_settings_commands.create_ps_database_setting') + @mock.patch('tethys_apps.cli.app_settings_commands.exit') + @mock.patch('tethys_apps.utilities.create_ps_database_setting') + def test_app_settings_create_ps_database_command(self, mock_database_settings, mock_exit): + # mock the args + mock_arg = mock.MagicMock(app='foo') + mock_arg.name = 'arg_name' + mock_arg.description = 'mock_description' + mock_arg.required = True + mock_arg.initializer = '' + mock_arg.initialized = 'initialized' + mock_arg.spatial = 'spatial' + mock_arg.dynamic = 'dynamic' + + # mock the system exit + mock_exit.side_effect = SystemExit + + # raise the system exit call when database is created + self.assertRaises(SystemExit, cli_app_settings_command.app_settings_create_ps_database_command, mock_arg) + + # check the call arguments from mock_database + mock_database_settings.assert_called_with('foo', 'arg_name', 'mock_description', True, '', + 'initialized', 'spatial', 'dynamic') + # check the mock exit value + mock_exit.assert_called_with(0) + + @mock.patch('tethys_apps.cli.app_settings_commands.exit') + @mock.patch('tethys_apps.utilities.create_ps_database_setting') + def test_app_settings_create_ps_database_command_with_no_success(self, mock_database_settings, mock_exit): + + # mock the args + mock_arg = mock.MagicMock(app='foo') + mock_arg.name = None + mock_arg.description = 'mock_description' + mock_arg.required = True + mock_arg.initializer = '' + mock_arg.initialized = 'initialized' + mock_arg.spatial = 'spatial' + mock_arg.dynamic = 'dynamic' + + # mock the system exit + mock_exit.side_effect = SystemExit + + mock_database_settings.return_value = False + + # raise the system exit call when database is created + self.assertRaises(SystemExit, cli_app_settings_command.app_settings_create_ps_database_command, mock_arg) + + # check the mock exit value + mock_exit.assert_called_with(1) + + @mock.patch('tethys_apps.cli.app_settings_commands.exit') + @mock.patch('tethys_apps.utilities.remove_ps_database_setting') + def test_app_settings_remove_command(self, mock_database_settings, mock_exit): + # mock the args + mock_arg = mock.MagicMock(app='foo') + mock_arg.name = 'arg_name' + mock_arg.force = 'force' + + # mock the system exit + mock_exit.side_effect = SystemExit + + # raise the system exit call when database is created + self.assertRaises(SystemExit, cli_app_settings_command.app_settings_remove_command, mock_arg) + + # check the call arguments from mock_database + mock_database_settings.assert_called_with('foo', 'arg_name', 'force') + + # check the mock exit value + mock_exit.assert_called_with(0) + + @mock.patch('tethys_apps.cli.app_settings_commands.exit') + @mock.patch('tethys_apps.utilities.remove_ps_database_setting') + def test_app_settings_remove_command_with_no_success(self, mock_database_settings, mock_exit): + # mock the args + mock_arg = mock.MagicMock(app='foo') + mock_arg.name = 'arg_name' + mock_arg.force = 'force' + + # mock the system exit + mock_exit.side_effect = SystemExit + + mock_database_settings.return_value = False + + # raise the system exit call when database is created + self.assertRaises(SystemExit, cli_app_settings_command.app_settings_remove_command, mock_arg) + + # check the mock exit value + mock_exit.assert_called_with(1) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_cli_colors.py b/tests/unit_tests/test_tethys_apps/test_cli/test_cli_colors.py new file mode 100644 index 000000000..60c11f859 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_cli_colors.py @@ -0,0 +1,67 @@ +import unittest +import mock +from tethys_apps.cli.cli_colors import pretty_output, FG_RED, FG_BLUE, BOLD, FG_GREEN, BG_GREEN, END + + +class TestCliColors(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.cli.cli_colors.print') + def test_pretty_output_fg_red(self, mock_print): + act_msg = 'This is a test in RED' + expected_string = '\x1b[31mThis is a test in RED\x1b[0m' + with pretty_output(FG_RED) as p: + p.write(act_msg) + + mock_print.assert_called_with(expected_string) + + @mock.patch('tethys_apps.cli.cli_colors.print') + def test_pretty_output_fg_blue(self, mock_print): + act_msg = 'This is a test in BLUE' + expected_string = '\x1b[34mThis is a test in BLUE\x1b[0m' + with pretty_output(FG_BLUE) as p: + p.write(act_msg) + + mock_print.assert_called_with(expected_string) + + @mock.patch('tethys_apps.cli.cli_colors.print') + def test_pretty_output_bold_fg_green(self, mock_print): + act_msg = 'This is a bold text in green' + expected_string = '\x1b[1m\x1b[32mThis is a bold text in green\x1b[0m' + with pretty_output(BOLD, FG_GREEN) as p: + p.write(act_msg) + + mock_print.assert_called_with(expected_string) + + @mock.patch('tethys_apps.cli.cli_colors.print') + def test_pretty_output_bold_bg_green(self, mock_print): + act_msg = 'This is a text with green background' + expected_string = '\x1b[1m\x1b[42mThis is a text with green background\x1b[0m' + with pretty_output(BOLD, BG_GREEN) as p: + p.write(act_msg) + + mock_print.assert_called_with(expected_string) + + @mock.patch('tethys_apps.cli.cli_colors.print') + def test_pretty_output_fg_green(self, mock_print): + act_msg = 'This is a green text with ' + BOLD + 'bold' + END + ' text included' + expected_string = '\x1b[32mThis is a green text with \x1b[1mbold\x1b[0m\x1b[32m text included\x1b[0m' + with pretty_output(FG_GREEN) as p: + p.write(act_msg) + + mock_print.assert_called_with(expected_string) + + @mock.patch('tethys_apps.cli.cli_colors.print') + def test_pretty_output_empty_msg(self, mock_print): + in_msg = BOLD + 'Use this' + END + ' even with ' + BOLD + FG_RED + 'no parameters' + \ + END + ' in the with statement' + expected_string = '\x1b[1mUse this\x1b[0m even with \x1b[1m\x1b[31mno parameters\x1b' \ + '[0m in the with statement\x1b[0m' + with pretty_output() as p: + p.write(in_msg) + + mock_print.assert_called_with(expected_string) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_cli_helper.py b/tests/unit_tests/test_tethys_apps/test_cli/test_cli_helper.py new file mode 100644 index 000000000..d09ac462d --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_cli_helper.py @@ -0,0 +1,15 @@ +import unittest +import tethys_apps.cli.cli_helpers as cli_helper + + +class TestCliHelper(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_add_geoserver_rest_to_endpoint(self): + endpoint = "http://localhost:8181/geoserver/rest/" + ret = cli_helper.add_geoserver_rest_to_endpoint(endpoint) + self.assertEqual(endpoint, ret) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_docker_commands.py b/tests/unit_tests/test_tethys_apps/test_cli/test_docker_commands.py new file mode 100644 index 000000000..c27577206 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_docker_commands.py @@ -0,0 +1,1344 @@ +import unittest +import mock +import tethys_apps.cli.docker_commands as cli_docker_commands + + +class TestDockerCommands(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_add_max_to_prompt(self): + self.assertEquals(' (max Foo)', cli_docker_commands.add_max_to_prompt('', 'Foo')) + + def test_add_default_to_prompt(self): + self.assertEquals(' [Foo]', cli_docker_commands.add_default_to_prompt('', 'Foo')) + + def test_add_default_to_prompt_with_choice(self): + self.assertEquals(' [Foo/bar]', cli_docker_commands.add_default_to_prompt('', 'Foo', choices=['Foo', 'Bar'])) + + def test_close_prompt(self): + self.assertEquals('Bar: ', cli_docker_commands.close_prompt('Bar')) + + def test_validate_numeric_cli_input_with_no_value(self): + self.assertEquals('12', cli_docker_commands.validate_numeric_cli_input('', default=12, max=100)) + + def test_validate_numeric_cli_input_with_value(self): + self.assertEquals(50, cli_docker_commands.validate_numeric_cli_input(50, default=12, max=100)) + + @mock.patch('tethys_apps.cli.docker_commands.input') + def test_validate_numeric_cli_input_with_value_gt_max(self, mock_input): + mock_input.side_effect = [55] + self.assertEquals(55, cli_docker_commands.validate_numeric_cli_input(200, default=12, max=100)) + + @mock.patch('tethys_apps.cli.docker_commands.input') + def test_validate_numeric_cli_input_with_value_gt_max_no_default(self, mock_input): + mock_input.side_effect = [66] + self.assertEquals(66, cli_docker_commands.validate_numeric_cli_input(200, max=100)) + + @mock.patch('tethys_apps.cli.docker_commands.input') + def test_validate_numeric_cli_input_value_error(self, mock_input): + mock_input.side_effect = [23] + self.assertEquals(23, cli_docker_commands.validate_numeric_cli_input(value='123ABC', default=12, max=100)) + + def test_validate_choice_cli_input_no_value(self): + self.assertEquals('Bar', cli_docker_commands.validate_choice_cli_input('', '', default='Bar')) + + @mock.patch('tethys_apps.cli.docker_commands.input') + def test_validate_choice_cli_input(self, mock_input): + mock_input.side_effect = ['Foo'] + self.assertEquals('Foo', cli_docker_commands.validate_choice_cli_input('Bell', choices=['foo', 'bar'], + default='bar')) + + def test_validate_directory_cli_input_no_value(self): + self.assertEquals('/tmp', cli_docker_commands.validate_directory_cli_input('', default='/tmp')) + + @mock.patch('tethys_apps.cli.docker_commands.os.path.isdir') + def test_validate_directory_cli_input_is_dir(self, mock_os_path_isdir): + mock_os_path_isdir.return_value = True + self.assertEquals('/c://temp//foo//bar', cli_docker_commands.validate_directory_cli_input('c://temp//foo//bar', + default='c://temp//')) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.input') + @mock.patch('tethys_apps.cli.docker_commands.os.makedirs') + @mock.patch('tethys_apps.cli.docker_commands.os.path.isdir') + def test_validate_directory_cli_input_oserror(self, mock_os_path_isdir, mock_os_makedirs, mock_input, + mock_pretty_output): + mock_os_path_isdir.side_effect = [False, True] + mock_os_makedirs.side_effect = OSError + mock_input.side_effect = ['/foo/tmp'] + self.assertEquals('/foo/tmp', cli_docker_commands.validate_directory_cli_input('c://temp//foo//bar', + default='c://temp//')) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('OSError(): /c://temp//foo//bar', po_call_args[0][0][0]) + + def test_get_api_version(self): + versions = ('1.2', '1.3') + self.assertEquals(versions, cli_docker_commands.get_api_version(versions)) + + @mock.patch('tethys_apps.cli.docker_commands.get_api_version') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_get_docker_client_linux(self, mock_DockerClient, mock_get_api_version): + ret = cli_docker_commands.get_docker_client() + mock_get_api_version.assert_called() + call_args = mock_DockerClient.call_args_list + + self.assertEqual(2, len(call_args)) + # Validate first call + first_call = call_args[0] + self.assertEqual('unix://var/run/docker.sock', first_call[1]['base_url']) + self.assertEqual('1.12', first_call[1]['version']) + + # Validate second call + second_call = call_args[1] + self.assertEqual('unix://var/run/docker.sock', second_call[1]['base_url']) + self.assertEqual(mock_get_api_version(), second_call[1]['version']) + + # Validate result + self.assertEqual(mock_DockerClient(), ret) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_api_version') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + @mock.patch('tethys_apps.cli.docker_commands.kwargs_from_env') + @mock.patch('tethys_apps.cli.docker_commands.os.environ') + @mock.patch('tethys_apps.cli.docker_commands.json.loads') + @mock.patch('tethys_apps.cli.docker_commands.subprocess') + def test_get_docker_client_mac_no_os_environ(self, mock_subprocess, mock_json_loads, mock_os_environ, + mock_kwargs, mock_docker_client, mock_get_api_version, + mock_pretty_output): + mock_p = mock.MagicMock() + mock_p.communicate.return_value = ['DOCKER_HOST=bar_host:9 DOCKER_CERT_PATH=baz_path DOCKER_TLS_VERIFY=qux_tls'] + mock_subprocess.Popen.return_value = mock_p + mock_json_loads.return_value = {"State": "foo", "DOCKER_HOST": "bar", "DOCKER_CERT_PATH": "baz", + "DOCKER_TLS_VERIFY": "qux"} + mock_os_environ.return_value = {} + mock_kwargs.return_value = {} + mock_version_client = mock.MagicMock() + mock_version_client.version.return_value = {'ApiVersion': 'quux'} + mock_docker_client.return_value = mock_version_client + mock_get_api_version.return_value = 'corge' + + ret = cli_docker_commands.get_docker_client() + + self.assertEquals('9', ret.host) + self.assertEquals(mock_docker_client(), ret) + mock_subprocess.Popen.assert_any_call(['boot2docker', 'info'], stdout=-1) + mock_subprocess.call.assert_called_once_with(['boot2docker', 'start']) + mock_subprocess.Popen.assert_called_with(['boot2docker', 'shellinit'], stdout=-1) + mock_json_loads.asssert_called_once() + mock_os_environ.__setitem__.assert_any_call('DOCKER_TLS_VERIFY', 'qux_tls') + mock_os_environ.__setitem__.assert_any_call('DOCKER_HOST', 'bar_host:9') + mock_os_environ.__setitem__.assert_any_call('DOCKER_CERT_PATH', 'baz_path') + mock_kwargs.assert_called_once_with(assert_hostname=False) + mock_docker_client.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('Starting Boot2Docker VM:', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_api_version') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + @mock.patch('tethys_apps.cli.docker_commands.kwargs_from_env') + @mock.patch('tethys_apps.cli.docker_commands.json.loads') + @mock.patch('tethys_apps.cli.docker_commands.subprocess') + def test_get_docker_client_mac_docker_host_env(self, mock_subprocess, mock_json_loads, mock_kwargs, + mock_docker_client, mock_get_api_version, + mock_pretty_output): + mock_p = mock.MagicMock() + mock_p.communicate.return_value = ['DOCKER_HOST=bar_host:5555 DOCKER_CERT_PATH=baz_path ' + 'DOCKER_TLS_VERIFY=qux_tls'] + mock_subprocess.Popen.return_value = mock_p + mock_json_loads.return_value = {"State": "foo", "DOCKER_HOST": "bar", "DOCKER_CERT_PATH": "baz", + "DOCKER_TLS_VERIFY": "qux"} + mock_kwargs.return_value = {} + mock_version_client = mock.MagicMock() + mock_version_client.version.return_value = {'ApiVersion': 'quux'} + mock_docker_client.return_value = mock_version_client + mock_get_api_version.return_value = 'corge' + + with mock.patch.dict('tethys_apps.cli.docker_commands.os.environ', {'DOCKER_HOST': 'foo=888:777', + 'DOCKER_CERT_PATH': 'bar', + 'DOCKER_TLS_VERIFY': 'baz'}, clear=True): + ret = cli_docker_commands.get_docker_client() + + self.assertEquals('777', ret.host) + self.assertEquals(mock_docker_client(), ret) + mock_subprocess.Popen.assert_called_once_with(['boot2docker', 'info'], stdout=-1) + mock_subprocess.call.assert_called_once_with(['boot2docker', 'start']) + mock_json_loads.asssert_called_once() + mock_kwargs.assert_called_once_with(assert_hostname=False) + mock_docker_client.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('Starting Boot2Docker VM:', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.subprocess.Popen') + def test_get_docker_client_other(self, mock_subprocess): + mock_subprocess.side_effect = Exception + self.assertRaises(Exception, cli_docker_commands.get_docker_client) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.subprocess') + def test_stop_boot2docker(self, mock_subprocess, mock_pretty_output): + cli_docker_commands.stop_boot2docker() + mock_subprocess.call.assert_called_with(['boot2docker', 'stop']) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('Boot2Docker VM Stopped', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.subprocess.call') + def test_stop_boot2docker_os_error(self, mock_subprocess): + mock_subprocess.side_effect = OSError + cli_docker_commands.stop_boot2docker() + mock_subprocess.assert_called_once_with(['boot2docker', 'stop']) + + @mock.patch('tethys_apps.cli.docker_commands.subprocess.call') + def test_stop_boot2docker_exception(self, mock_subprocess): + mock_subprocess.side_effect = Exception + self.assertRaises(Exception, cli_docker_commands.stop_boot2docker) + + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_get_images_to_install(self, mock_dc): + mock_dc.images.return_value = [{'RepoTags': 'ciwater/postgis:2.1.2'}, + {'RepoTags': 'ciwater/geoserver:2.8.2-clustered'}] + + # mock docker client return images + all_docker_input = ('postgis', 'geoserver', 'wps') + ret = cli_docker_commands.get_images_to_install(mock_dc, all_docker_input) + + self.assertEquals(1, len(ret)) + self.assertEquals('ciwater/n52wps:3.3.1', ret[0]) + + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_get_containers_to_create(self, mock_dc): + # mock_dc.containers.return_value = [{'Names': '/tethys_postgis'}, + # {'Names': '/tethys_geoserver'}] + mock_dc.containers.return_value = [{'Names': ['/tethys_postgis'], 'Image': '/tethys_postgis'}, + {'Names': ['/tethys_geoserver'], 'Image': '/tethys_geoserver'}] + all_container_input = ('postgis', 'geoserver', 'wps') + ret = cli_docker_commands.get_containers_to_create(mock_dc, all_container_input) + + self.assertEquals(1, len(ret)) + self.assertEquals('tethys_wps', ret[0]) + + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_get_docker_container_dicts(self, mock_dc): + mock_dc.containers.return_value = [{'Names': ['/tethys_postgis'], 'Image': '/tethys_postgis'}, + {'Names': ['/tethys_geoserver'], 'Image': '/tethys_geoserver'}, + {'Names': ['/tethys_wps'], 'Image': '/tethys_wps'}] + ret = cli_docker_commands.get_docker_container_dicts(mock_dc) + + self.assertEquals(3, len(ret)) + self.assertTrue('tethys_postgis' in ret) + self.assertTrue('tethys_geoserver' in ret) + self.assertTrue('tethys_wps' in ret) + + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_get_docker_container_image(self, mock_dc): + mock_dc.containers.return_value = [{'Names': ['/tethys_postgis'], 'Image': '/tethys_postgis'}, + {'Names': ['/tethys_geoserver'], 'Image': '/tethys_geoserver'}, + {'Names': ['/tethys_wps'], 'Image': '/tethys_wps'}] + ret = cli_docker_commands.get_docker_container_image(mock_dc) + + self.assertEquals(3, len(ret)) + self.assertTrue('tethys_postgis' in ret) + self.assertTrue('tethys_geoserver' in ret) + self.assertTrue('tethys_wps' in ret) + + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_get_docker_container_status(self, mock_dc): + mock_dc.containers.return_value = [{'Names': ['/tethys_postgis'], 'Image': '/tethys_postgis'}, + {'Names': ['/tethys_geoserver'], 'Image': '/tethys_geoserver'}, + {'Names': ['/tethys_wps'], 'Image': '/tethys_wps'}] + ret = cli_docker_commands.get_docker_container_status(mock_dc) + + self.assertEquals(3, len(ret)) + self.assertTrue('tethys_postgis' in ret) + self.assertTrue('tethys_geoserver' in ret) + self.assertTrue('tethys_wps' in ret) + self.assertTrue(ret['tethys_postgis']) + self.assertTrue(ret['tethys_geoserver']) + self.assertTrue(ret['tethys_wps']) + + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_get_docker_container_status_off(self, mock_dc): + mock_dc.containers.return_value = [{'Names': ['/tethys_postgis'], 'Image': '/tethys_postgis'}, + {'Names': ['/tethys_geoserver'], 'Image': '/tethys_geoserver'}] + ret = cli_docker_commands.get_docker_container_status(mock_dc) + + self.assertEquals(2, len(ret)) + self.assertTrue('tethys_postgis' in ret) + self.assertTrue('tethys_geoserver' in ret) + self.assertFalse('tethys_wps' in ret) + self.assertTrue(ret['tethys_postgis']) + self.assertTrue(ret['tethys_geoserver']) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_containers_to_create') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_remove_docker_containers(self, mock_dc, mock_create, mock_pretty_output): + mock_create.return_value = [{'Names': ['/tethys_postgis'], 'Image': '/tethys_postgis'}, + {'Names': ['/tethys_geoserver'], 'Image': '/tethys_geoserver'}, + {'Names': ['/tethys_wps'], 'Image': '/tethys_wps'}] + cli_docker_commands.remove_docker_containers(mock_dc) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('Removing PostGIS...', po_call_args[0][0][0]) + self.assertEquals('Removing GeoServer...', po_call_args[1][0][0]) + self.assertEquals('Removing 52 North WPS...', po_call_args[2][0][0]) + mock_dc.remove_container.assert_any_call(container='tethys_postgis') + mock_dc.remove_container.assert_any_call(container='tethys_geoserver', v=True) + mock_dc.remove_container.assert_called_with(container='tethys_wps') + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.install_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.log_pull_stream') + @mock.patch('tethys_apps.cli.docker_commands.get_images_to_install') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_init(self, mock_dc, mock_images, mock_stream, mock_containers, mock_pretty_output): + mock_dc.return_value = mock.MagicMock() + mock_images.return_value = ['foo_image', 'foo2'] + mock_stream.return_value = True + mock_containers.return_value = True + + cli_docker_commands.docker_init() + + mock_dc.assert_called_once() + mock_images.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps')) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('Pulling Docker images...', po_call_args[0][0][0]) + mock_stream.assert_called_with(mock_dc().pull()) + mock_containers.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps'), defaults=False) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.install_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.log_pull_stream') + @mock.patch('tethys_apps.cli.docker_commands.get_images_to_install') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_init_no_images(self, mock_dc, mock_images, mock_stream, mock_containers, mock_pretty_output): + mock_dc.return_value = mock.MagicMock() + mock_images.return_value = [] + mock_stream.return_value = True + mock_containers.return_value = True + + cli_docker_commands.docker_init() + + mock_dc.assert_called_once() + mock_images.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps')) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('Docker images already pulled.', po_call_args[0][0][0]) + mock_stream.assert_not_called() + mock_containers.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps'), defaults=False) + + @mock.patch('tethys_apps.cli.docker_commands.start_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_start(self, mock_dc, mock_start): + cli_docker_commands.docker_start(containers=('postgis', 'geoserver', 'wps')) + + mock_dc.assert_called_once() + mock_start.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps')) + + @mock.patch('tethys_apps.cli.docker_commands.stop_boot2docker') + @mock.patch('tethys_apps.cli.docker_commands.stop_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_stop(self, mock_dc, mock_stop, mock_boot): + cli_docker_commands.docker_stop(containers=('postgis', 'geoserver', 'wps')) + + mock_dc.assert_called_once() + mock_stop.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps')) + mock_boot.assert_not_called() + + @mock.patch('tethys_apps.cli.docker_commands.stop_boot2docker') + @mock.patch('tethys_apps.cli.docker_commands.stop_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_stop_boot2docker(self, mock_dc, mock_stop, mock_boot): + cli_docker_commands.docker_stop(containers='', boot2docker=True) + + mock_dc.assert_called_once() + mock_stop.assert_called_once_with(mock_dc(), containers='') + mock_boot.assert_called_once() + + @mock.patch('tethys_apps.cli.docker_commands.start_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.stop_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_restart(self, mock_dc, mock_stop, mock_start): + cli_docker_commands.docker_restart() + + mock_dc.assert_called_once() + mock_stop.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps')) + mock_start.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps')) + + @mock.patch('tethys_apps.cli.docker_commands.start_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.stop_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_restart_containers(self, mock_dc, mock_stop, mock_start): + cli_docker_commands.docker_restart(containers=('postgis', 'geoserver')) + + mock_dc.assert_called_once() + mock_stop.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver')) + mock_start.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver')) + + @mock.patch('tethys_apps.cli.docker_commands.remove_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.stop_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_remove(self, mock_dc, mock_stop, mock_remove): + cli_docker_commands.docker_remove() + + mock_dc.assert_called_once() + mock_stop.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps')) + mock_remove.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps')) + + @mock.patch('tethys_apps.cli.docker_commands.remove_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.stop_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_remove_containers(self, mock_dc, mock_stop, mock_remove): + cli_docker_commands.docker_remove(containers=('postgis', 'geoserver')) + + mock_dc.assert_called_once() + mock_stop.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver')) + mock_remove.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver')) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_status_container_running(self, mock_dc, mock_dc_status, mock_pretty_output): + mock_dc_status.return_value = {'tethys_postgis': True, 'tethys_geoserver': True, 'tethys_wps': True} + cli_docker_commands.docker_status() + mock_dc.assert_called_once() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('PostGIS/Database: Running', po_call_args[0][0][0]) + self.assertEquals('GeoServer: Running', po_call_args[1][0][0]) + self.assertEquals('52 North WPS: Running', po_call_args[2][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_status_container_stopped(self, mock_dc, mock_dc_status, mock_pretty_output): + mock_dc_status.return_value = {'tethys_postgis': False, 'tethys_geoserver': False, 'tethys_wps': False} + cli_docker_commands.docker_status() + mock_dc.assert_called_once() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('PostGIS/Database: Stopped', po_call_args[0][0][0]) + self.assertEquals('GeoServer: Stopped', po_call_args[1][0][0]) + self.assertEquals('52 North WPS: Stopped', po_call_args[2][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_status_container_not_installed(self, mock_dc, mock_dc_status, mock_pretty_output): + mock_dc_status.return_value = {} + cli_docker_commands.docker_status() + mock_dc.assert_called_once() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('PostGIS/Database: Not Installed', po_call_args[0][0][0]) + self.assertEquals('GeoServer: Not Installed', po_call_args[1][0][0]) + self.assertEquals('52 North WPS: Not Installed', po_call_args[2][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.install_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.log_pull_stream') + @mock.patch('tethys_apps.cli.docker_commands.remove_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.stop_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_update_with_no_container(self, mock_dc, mock_stop, mock_remove, mock_lps, mock_install): + cli_docker_commands.docker_update(containers=None, defaults=False) + + mock_dc.assert_called_once() + + mock_stop.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps')) + + mock_remove.assert_called_once_with(mock_dc(), containers=('postgis', 'geoserver', 'wps')) + + mock_lps.assert_any_call(mock_dc().pull()) + + mock_install.assert_called_once_with(mock_dc(), force=True, containers=('postgis', 'geoserver', 'wps'), + defaults=False) + + @mock.patch('tethys_apps.cli.docker_commands.install_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.log_pull_stream') + @mock.patch('tethys_apps.cli.docker_commands.remove_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.stop_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_update_with_container(self, mock_dc, mock_stop, mock_remove, mock_lps, mock_install): + + cli_docker_commands.docker_update(containers=[''], defaults=False) + + mock_dc.assert_called_once() + + mock_stop.assert_called_once_with(mock_dc(), containers=['']) + + mock_remove.assert_called_once_with(mock_dc(), containers=['']) + + mock_lps.assert_any_call(mock_dc().pull()) + + mock_install.assert_called_once_with(mock_dc(), force=True, containers=[''], defaults=False) + + @mock.patch('tethys_apps.cli.docker_commands.install_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.log_pull_stream') + @mock.patch('tethys_apps.cli.docker_commands.remove_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.stop_docker_containers') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_update_with_bad_container(self, mock_dc, mock_stop, mock_remove, mock_lps, mock_install): + cli_docker_commands.docker_update(containers=['foo'], defaults=False) + + mock_dc.assert_called_once() + + mock_stop.assert_called_once_with(mock_dc(), containers=['foo']) + + mock_remove.assert_called_once_with(mock_dc(), containers=['foo']) + + mock_lps.assert_not_called() + + mock_install.assert_called_once_with(mock_dc(), force=True, containers=['foo'], defaults=False) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_dicts') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_ip(self, mock_dc, mock_containers, mock_status, mock_pretty_output): + mock_dc().host = 'host' + + mock_containers.return_value = {'tethys_postgis': {'Ports': [{'PublicPort': 123}]}, + 'tethys_geoserver': {'Ports': [{'PublicPort': 234}]}, + 'tethys_wps': {'Ports': [{'PublicPort': 456}]}} + + mock_status.return_value = {'tethys_postgis': True, 'tethys_geoserver': True, 'tethys_wps': True} + + cli_docker_commands.docker_ip() + + mock_dc.assert_called() + + mock_containers.assert_called_once_with(mock_dc()) + + mock_status.assert_called_once_with(mock_dc()) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(13, len(po_call_args)) + self.assertIn('PostGIS/Database:', po_call_args[0][0][0]) + self.assertEquals(' Host: host', po_call_args[1][0][0]) + self.assertEquals(' Port: 123', po_call_args[2][0][0]) + self.assertEquals(' Endpoint: postgresql://:@host:123/', po_call_args[3][0][0]) + + self.assertIn('GeoServer:', po_call_args[4][0][0]) + self.assertEquals(' Host: host', po_call_args[5][0][0]) + self.assertEquals(' Primary Port: 8181', po_call_args[6][0][0]) + self.assertEquals(' Node Ports: 234', po_call_args[7][0][0]) + self.assertEquals(' Endpoint: http://host:8181/geoserver/rest', po_call_args[8][0][0]) + + self.assertIn('52 North WPS:', po_call_args[9][0][0]) + self.assertEquals(' Host: host', po_call_args[10][0][0]) + self.assertEquals(' Port: 456', po_call_args[11][0][0]) + self.assertEquals(' Endpoint: http://host:456/wps/WebProcessingService\n', po_call_args[12][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_dicts') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_ip_not_running(self, mock_dc, mock_containers, mock_status, mock_pretty_output): + mock_dc().host = 'host' + + mock_containers.return_value = {'tethys_postgis': {'Ports': [{'PublicPort': 123}]}, + 'tethys_geoserver': {'Ports': [{'PublicPort': 234}]}, + 'tethys_wps': {'Ports': [{'PublicPort': 456}]}} + + mock_status.return_value = {'tethys_postgis': False, 'tethys_geoserver': False, 'tethys_wps': False} + + cli_docker_commands.docker_ip() + + mock_dc.assert_called() + + mock_containers.assert_called_once_with(mock_dc()) + + mock_status.assert_called_once_with(mock_dc()) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('\nPostGIS/Database: Not Running.', po_call_args[0][0][0]) + self.assertEquals('\nGeoServer: Not Running.', po_call_args[1][0][0]) + self.assertEquals('\n52 North WPS: Not Running.', po_call_args[2][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_dicts') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_ip_not_installed(self, mock_dc, mock_containers, mock_status, mock_pretty_output): + mock_dc().host = 'host' + + mock_containers.return_value = {'foo': {'Ports': [{'PublicPort': 123}]}, + 'bar': {'Ports': [{'PublicPort': 234}]}, + 'baz': {'Ports': [{'PublicPort': 456}]}} + + mock_status.return_value = {'foo': True, 'bar': True, 'baz': True} + + cli_docker_commands.docker_ip() + + mock_dc.assert_called() + + mock_containers.assert_called_once_with(mock_dc()) + + mock_status.assert_called_once_with(mock_dc()) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('\nPostGIS/Database: Not Installed.', po_call_args[0][0][0]) + self.assertEquals('\nGeoServer: Not Installed.', po_call_args[1][0][0]) + self.assertEquals('\n52 North WPS: Not Installed.', po_call_args[2][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output.write') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_dicts') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_ip_exception_post_gis(self, mock_dc, mock_containers, mock_status, mock_pretty_output): + mock_pretty_output.side_effect = Exception + + mock_dc().host = 'host' + + mock_containers.return_value = {'tethys_postgis': {'Ports': [{'PublicPort': 123}]}, + 'tethys_geoserver': {'Ports': [{'PublicPort': 234}]}, + 'tethys_wps': {'Ports': [{'PublicPort': 456}]}} + + mock_status.return_value = {'tethys_postgis': True, 'tethys_geoserver': False, 'tethys_wps': False} + + self.assertRaises(Exception, cli_docker_commands.docker_ip) + + mock_dc.assert_called() + + mock_containers.assert_called_once_with(mock_dc()) + + mock_status.assert_called_once_with(mock_dc()) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output.write') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_dicts') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_ip_exception_geo_server(self, mock_dc, mock_containers, mock_status, mock_pretty_output): + mock_pretty_output.side_effect = [True, Exception] + + mock_dc().host = 'host' + + mock_containers.return_value = {'tethys_postgis': {'Ports': [{'PublicPort': 123}]}, + 'tethys_geoserver': {'Ports': [{'PublicPort': 234}]}, + 'tethys_wps': {'Ports': [{'PublicPort': 456}]}} + + mock_status.return_value = {'tethys_postgis': False, 'tethys_geoserver': True, 'tethys_wps': False} + + self.assertRaises(Exception, cli_docker_commands.docker_ip) + + mock_dc.assert_called() + + mock_containers.assert_called_once_with(mock_dc()) + + mock_status.assert_called_once_with(mock_dc()) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output.write') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_dicts') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_client') + def test_docker_ip_exception_wps(self, mock_dc, mock_containers, mock_status, mock_pretty_output): + mock_pretty_output.side_effect = [True, True, Exception] + mock_dc().host = 'host' + mock_containers.return_value = {'tethys_postgis': {'Ports': [{'PublicPort': 123}]}, + 'tethys_geoserver': {'Ports': [{'PublicPort': 234}]}, + 'tethys_wps': {'Ports': [{'PublicPort': 456}]}} + mock_status.return_value = {'tethys_postgis': False, 'tethys_geoserver': False, 'tethys_wps': True} + + self.assertRaises(Exception, cli_docker_commands.docker_ip) + + mock_dc.assert_called() + mock_containers.assert_called_once_with(mock_dc()) + mock_status.assert_called_once_with(mock_dc()) + + @mock.patch('tethys_apps.cli.docker_commands.docker_init') + def test_docker_command_init(self, mock_function): + mock_args = mock.MagicMock() + mock_args.command = 'init' + mock_args.containers = 'containers' + mock_args.defaults = True + mock_args.boot2docker = True + + cli_docker_commands.docker_command(mock_args) + + mock_function.assert_called_once_with(containers='containers', defaults=True) + + @mock.patch('tethys_apps.cli.docker_commands.docker_start') + def test_docker_command_start(self, mock_function): + mock_args = mock.MagicMock() + mock_args.command = 'start' + mock_args.containers = 'containers' + mock_args.defaults = True + mock_args.boot2docker = True + + cli_docker_commands.docker_command(mock_args) + + mock_function.assert_called_once_with(containers='containers') + + @mock.patch('tethys_apps.cli.docker_commands.docker_stop') + def test_docker_command_stop(self, mock_function): + mock_args = mock.MagicMock() + mock_args.command = 'stop' + mock_args.containers = 'containers' + mock_args.defaults = True + mock_args.boot2docker = True + + cli_docker_commands.docker_command(mock_args) + + mock_function.assert_called_once_with(containers='containers', boot2docker=True) + + @mock.patch('tethys_apps.cli.docker_commands.docker_status') + def test_docker_command_status(self, mock_function): + mock_args = mock.MagicMock() + mock_args.command = 'status' + mock_args.containers = 'containers' + mock_args.defaults = True + mock_args.boot2docker = True + + cli_docker_commands.docker_command(mock_args) + + mock_function.assert_called_once_with() + + @mock.patch('tethys_apps.cli.docker_commands.docker_update') + def test_docker_command_update(self, mock_function): + mock_args = mock.MagicMock() + mock_args.command = 'update' + mock_args.containers = 'containers' + mock_args.defaults = True + mock_args.boot2docker = True + + cli_docker_commands.docker_command(mock_args) + + mock_function.assert_called_once_with(containers='containers', defaults=True) + + @mock.patch('tethys_apps.cli.docker_commands.docker_remove') + def test_docker_command_remove(self, mock_function): + mock_args = mock.MagicMock() + mock_args.command = 'remove' + mock_args.containers = 'containers' + mock_args.defaults = True + mock_args.boot2docker = True + + cli_docker_commands.docker_command(mock_args) + + mock_function.assert_called_once_with(containers='containers') + + @mock.patch('tethys_apps.cli.docker_commands.docker_ip') + def test_docker_command_ip(self, mock_function): + mock_args = mock.MagicMock() + mock_args.command = 'ip' + mock_args.containers = 'containers' + mock_args.defaults = True + mock_args.boot2docker = True + + cli_docker_commands.docker_command(mock_args) + + mock_function.assert_called_once_with() + + @mock.patch('tethys_apps.cli.docker_commands.docker_restart') + def test_docker_command_restart(self, mock_function): + mock_args = mock.MagicMock() + mock_args.command = 'restart' + mock_args.containers = 'containers' + mock_args.defaults = True + mock_args.boot2docker = True + + cli_docker_commands.docker_command(mock_args) + + mock_function.assert_called_once_with(containers='containers') + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.getpass.getpass') + @mock.patch('tethys_apps.cli.docker_commands.get_containers_to_create') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_install_docker_containers_postgis_password_given(self, mock_dc, mock_containers_to_create, + mock_getpass, mock_pretty_output): + mock_containers_to_create.return_value = ['tethys_postgis'] + mock_getpass.side_effect = ['pass', # tethys_default password + 'foo', # tethys_default password not matching + 'pass', # tethys_default password redo + 'pass', # tethys_default password redo match + 'passmgr', # tethys_db_manager password + 'foo', # tethys_db_manager password not matching + 'passmgr', # tethys_db_manager password redo + 'passmgr', # tethys_db_manager password redo matching + 'passsuper', # tethys_super password + 'foo', # tethys_super password not matching + 'passsuper', # tethys_super password redo + 'passsuper' # tethys_super password redo matching + ] + cli_docker_commands.install_docker_containers(mock_dc, force=False, defaults=False) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(6, len(po_call_args)) + self.assertEquals('\nInstalling the PostGIS Docker container...', po_call_args[0][0][0]) + self.assertIn('Provide passwords for the three Tethys database users or press enter', po_call_args[1][0][0]) + self.assertEquals('Passwords do not match, please try again: ', po_call_args[2][0][0]) + self.assertEquals('Passwords do not match, please try again: ', po_call_args[3][0][0]) + self.assertEquals('Passwords do not match, please try again: ', po_call_args[4][0][0]) + self.assertEquals('Finished installing Docker containers.', po_call_args[5][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.getpass.getpass') + @mock.patch('tethys_apps.cli.docker_commands.get_containers_to_create') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_install_docker_containers_postgis_password_default(self, mock_dc, mock_containers_to_create, + mock_getpass, mock_pretty_output): + mock_containers_to_create.return_value = ['tethys_postgis'] + mock_getpass.side_effect = ['', # tethys_default password + '', # tethys_db_manager password + '', # tethys_super password + ] + cli_docker_commands.install_docker_containers(mock_dc, force=False, defaults=False) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('\nInstalling the PostGIS Docker container...', po_call_args[0][0][0]) + self.assertIn('Provide passwords for the three Tethys database users or press enter', po_call_args[1][0][0]) + self.assertEquals('Finished installing Docker containers.', po_call_args[2][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.input') + @mock.patch('tethys_apps.cli.docker_commands.create_host_config') + @mock.patch('tethys_apps.cli.docker_commands.get_containers_to_create') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_install_docker_containers_geoserver_numprocessors_bind(self, mock_dc, mock_containers_to_create, + mock_create_host_config, mock_input, + mock_pretty_output): + mock_containers_to_create.return_value = ['tethys_geoserver'] + mock_input.side_effect = ['1', # Number of GeoServer Instances Enabled + '1', # Number of GeoServer Instances with REST API Enabled + 'c', # Would you like to specify number of Processors (c) OR set limits (e) + '2', # Number of Processors + '60', # Maximum request timeout in seconds + '1024', # Maximum memory to allocate to each GeoServer instance in MB + '0', # Minimum memory to allocate to each GeoServer instance in MB + 'y', # Bind the GeoServer data directory to the host? + '/tmp' # Specify location to bind data directory + ] + + cli_docker_commands.install_docker_containers(mock_dc, force=False, defaults=False) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(4, len(po_call_args)) + self.assertEquals('\nInstalling the GeoServer Docker container...', po_call_args[0][0][0]) + self.assertIn('The GeoServer docker can be configured to run in a clustered mode', po_call_args[1][0][0]) + self.assertIn('GeoServer can be configured with limits to certain types of requests', po_call_args[2][0][0]) + self.assertIn('Finished installing Docker containers', po_call_args[3][0][0]) + mock_create_host_config.assert_called() + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.input') + @mock.patch('tethys_apps.cli.docker_commands.create_host_config') + @mock.patch('tethys_apps.cli.docker_commands.get_containers_to_create') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_install_docker_containers_geoserver_limits_no_bind(self, mock_dc, mock_containers_to_create, + mock_create_host_config, mock_input, + mock_pretty_output): + mock_containers_to_create.return_value = ['tethys_geoserver'] + mock_input.side_effect = ['1', # Number of GeoServer Instances Enabled + '1', # Number of GeoServer Instances with REST API Enabled + 'e', # Would you like to specify number of Processors (c) OR set limits (e) + '100', # Maximum number of simultaneous OGC web service requests + '8', # Maximum number of simultaneous GetMap requests + '16', # Maximum number of simultaneous GeoWebCache tile renders + '60', # Maximum request timeout in seconds + '1024', # Maximum memory to allocate to each GeoServer instance in MB + '0', # Minimum memory to allocate to each GeoServer instance in MB + 'n', # Bind the GeoServer data directory to the host? + ] + + cli_docker_commands.install_docker_containers(mock_dc, force=False, defaults=False) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(4, len(po_call_args)) + self.assertEquals('\nInstalling the GeoServer Docker container...', po_call_args[0][0][0]) + self.assertIn('The GeoServer docker can be configured to run in a clustered mode', po_call_args[1][0][0]) + self.assertIn('GeoServer can be configured with limits to certain types of requests', po_call_args[2][0][0]) + self.assertIn('Finished installing Docker containers', po_call_args[3][0][0]) + mock_create_host_config.assert_not_called() + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.create_host_config') + @mock.patch('tethys_apps.cli.docker_commands.get_containers_to_create') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_install_docker_containers_geoserver_defaults(self, mock_dc, mock_containers_to_create, + mock_create_host_config, mock_pretty_output): + mock_containers_to_create.return_value = ['tethys_geoserver'] + + cli_docker_commands.install_docker_containers(mock_dc, force=False, defaults=True) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(2, len(po_call_args)) + self.assertEquals('\nInstalling the GeoServer Docker container...', po_call_args[0][0][0]) + self.assertIn('Finished installing Docker containers', po_call_args[1][0][0]) + mock_create_host_config.assert_called_once_with(binds=['/usr/lib/tethys/geoserver/data:/var/geoserver/data']) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.GEOSERVER_IMAGE') + @mock.patch('tethys_apps.cli.docker_commands.create_host_config') + @mock.patch('tethys_apps.cli.docker_commands.get_containers_to_create') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_install_docker_containers_geoserver_no_cluster(self, mock_dc, mock_containers_to_create, + mock_create_host_config, mock_image, mock_pretty_output): + mock_containers_to_create.return_value = ['tethys_geoserver'] + mock_image.return_value = '' + + cli_docker_commands.install_docker_containers(mock_dc, force=False, defaults=True) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(2, len(po_call_args)) + self.assertEquals('\nInstalling the GeoServer Docker container...', po_call_args[0][0][0]) + self.assertIn('Finished installing Docker containers', po_call_args[1][0][0]) + mock_create_host_config.assert_not_called() + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.getpass.getpass') + @mock.patch('tethys_apps.cli.docker_commands.input') + @mock.patch('tethys_apps.cli.docker_commands.get_containers_to_create') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_install_docker_containers_wps_no_defaults_password(self, mock_dc, mock_containers_to_create, mock_input, + mock_getpass, mock_pretty_output): + mock_containers_to_create.return_value = ['tethys_wps'] + mock_input.side_effect = ['', # Name + '', # Position + '', # Address + '', # City + '', # State + '', # Country + '', # Postal Code + '', # Email + '', # Phone + '', # Fax + ''] # Admin username + mock_getpass.side_effect = ['wps', # Admin Password + 'foo', # Admin Password no match + 'wps', # Admin Password redo + 'wps'] # Admin Password match + + cli_docker_commands.install_docker_containers(mock_dc, force=False, defaults=False) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(4, len(po_call_args)) + self.assertEquals('\nInstalling the 52 North WPS Docker container...', po_call_args[0][0][0]) + self.assertIn('Provide contact information for the 52 North Web Processing Service', po_call_args[1][0][0]) + self.assertEquals('Passwords do not match, please try again.', po_call_args[2][0][0]) + self.assertIn('Finished installing Docker containers', po_call_args[3][0][0]) + mock_dc.create_container.assert_called_once_with(name='tethys_wps', + image='ciwater/n52wps:3.3.1', + environment={'NAME': 'NONE', + 'POSITION': 'NONE', + 'ADDRESS': 'NONE', + 'CITY': 'NONE', + 'STATE': 'NONE', + 'COUNTRY': 'NONE', + 'POSTAL_CODE': 'NONE', + 'EMAIL': 'NONE', + 'PHONE': 'NONE', + 'FAX': 'NONE', + 'USERNAME': 'wps', + 'PASSWORD': 'wps'}) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.getpass.getpass') + @mock.patch('tethys_apps.cli.docker_commands.input') + @mock.patch('tethys_apps.cli.docker_commands.get_containers_to_create') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_install_docker_containers_wps_no_empty_defaults_blank_password(self, mock_dc, mock_containers_to_create, + mock_input, mock_getpass, + mock_pretty_output): + mock_containers_to_create.return_value = ['tethys_wps'] + mock_input.side_effect = ['Name', # Name + 'Pos', # Position + 'Addr', # Address + 'City', # City + 'State', # State + 'Cty', # Country + 'Code', # Postal Code + 'foo@foo.com', # Email + '123456789', # Phone + '123456788', # Fax + 'fooadmin'] # Admin username + mock_getpass.side_effect = [''] # Admin Password + + cli_docker_commands.install_docker_containers(mock_dc, force=False, defaults=False) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('\nInstalling the 52 North WPS Docker container...', po_call_args[0][0][0]) + self.assertIn('Provide contact information for the 52 North Web Processing Service', po_call_args[1][0][0]) + self.assertIn('Finished installing Docker containers', po_call_args[2][0][0]) + mock_dc.create_container.assert_called_once_with(name='tethys_wps', + image='ciwater/n52wps:3.3.1', + environment={'NAME': 'Name', + 'POSITION': 'Pos', + 'ADDRESS': 'Addr', + 'CITY': 'City', + 'STATE': 'State', + 'COUNTRY': 'Cty', + 'POSTAL_CODE': 'Code', + 'EMAIL': 'foo@foo.com', + 'PHONE': '123456789', + 'FAX': '123456788', + 'USERNAME': 'fooadmin', + 'PASSWORD': 'wps'}) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.getpass.getpass') + @mock.patch('tethys_apps.cli.docker_commands.input') + @mock.patch('tethys_apps.cli.docker_commands.get_containers_to_create') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_install_docker_containers_wps_defaults_password(self, mock_dc, mock_containers_to_create, mock_input, + mock_getpass, mock_pretty_output): + mock_containers_to_create.return_value = ['tethys_wps'] + + cli_docker_commands.install_docker_containers(mock_dc, force=False, defaults=True) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(2, len(po_call_args)) + self.assertEquals('\nInstalling the 52 North WPS Docker container...', po_call_args[0][0][0]) + self.assertIn('Finished installing Docker containers', po_call_args[1][0][0]) + mock_input.assert_not_called() + mock_getpass.assert_not_called() + mock_dc.create_container.assert_called_once_with(name='tethys_wps', + image='ciwater/n52wps:3.3.1', + environment={'NAME': 'NONE', + 'POSITION': 'NONE', + 'ADDRESS': 'NONE', + 'CITY': 'NONE', + 'STATE': 'NONE', + 'COUNTRY': 'NONE', + 'POSTAL_CODE': 'NONE', + 'EMAIL': 'NONE', + 'PHONE': 'NONE', + 'FAX': 'NONE', + 'USERNAME': 'wps', + 'PASSWORD': 'wps'}) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_image') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_start_docker_containers_already_running(self, mock_dc, mock_dc_image, mock_dc_status, mock_pretty_output): + mock_dc_image.return_value = [{'Names': ['/tethys_postgis'], 'Image': '/tethys_postgis'}, + {'Names': ['/tethys_geoserver'], 'Image': '/tethys_geoserver'}, + {'Names': ['/tethys_wps'], 'Image': '/tethys_wps'}] + mock_dc_status.return_value = {'tethys_postgis': True, 'tethys_geoserver': True, 'tethys_wps': True} + + cli_docker_commands.start_docker_containers(mock_dc) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('PostGIS container already running...', po_call_args[0][0][0]) + self.assertEquals('GeoServer container already running...', po_call_args[1][0][0]) + self.assertEquals('52 North WPS container already running...', po_call_args[2][0][0]) + mock_dc_image.assert_called_once_with(mock_dc) + mock_dc_status.assert_called_with(mock_dc) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_image') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_start_docker_containers_starting_cluster(self, mock_dc, mock_dc_image, mock_dc_status, mock_pretty_output): + mock_dc_image.return_value = {'tethys_geoserver': 'cluster'} + mock_dc_status.return_value = {'tethys_postgis': False, 'tethys_geoserver': False, 'tethys_wps': False} + + cli_docker_commands.start_docker_containers(mock_dc) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('Starting PostGIS container...', po_call_args[0][0][0]) + self.assertEquals('Starting GeoServer container...', po_call_args[1][0][0]) + self.assertEquals('Starting 52 North WPS container...', po_call_args[2][0][0]) + mock_dc_image.assert_called_once_with(mock_dc) + mock_dc_status.assert_called_with(mock_dc) + mock_dc.start.assert_any_call(container='tethys_postgis', port_bindings={5432: '5435'}) + mock_dc.start.assert_any_call(container='tethys_geoserver', port_bindings={8181: '8181', + 8081: ('0.0.0.0', 8081), + 8082: ('0.0.0.0', 8082), + 8083: ('0.0.0.0', 8083), + 8084: ('0.0.0.0', 8084)}) + mock_dc.start.assert_any_call(container='tethys_wps', port_bindings={8080: '8282'}) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_image') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_start_docker_containers_starting_no_cluster(self, mock_dc, mock_dc_image, mock_dc_status, + mock_pretty_output): + mock_dc_image.return_value = {'tethys_geoserver': 'foo'} + mock_dc_status.return_value = {'tethys_postgis': False, 'tethys_geoserver': False, 'tethys_wps': False} + + cli_docker_commands.start_docker_containers(mock_dc) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('Starting PostGIS container...', po_call_args[0][0][0]) + self.assertEquals('Starting GeoServer container...', po_call_args[1][0][0]) + self.assertEquals('Starting 52 North WPS container...', po_call_args[2][0][0]) + mock_dc_image.assert_called_once_with(mock_dc) + mock_dc_status.assert_called_with(mock_dc) + mock_dc.start.assert_any_call(container='tethys_postgis', port_bindings={5432: '5435'}) + mock_dc.start.assert_any_call(container='tethys_geoserver', port_bindings={8080: '8181'}) + mock_dc.start.assert_any_call(container='tethys_wps', port_bindings={8080: '8282'}) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_image') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_start_docker_containers_notinstalled(self, mock_dc, mock_dc_image, mock_dc_status, + mock_pretty_output): + mock_dc_image.return_value = {'tethys_geoserver': 'foo'} + mock_dc_status.return_value = {} + + cli_docker_commands.start_docker_containers(mock_dc) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('PostGIS container not installed...', po_call_args[0][0][0]) + self.assertEquals('GeoServer container not installed...', po_call_args[1][0][0]) + self.assertEquals('52 North WPS container not installed...', po_call_args[2][0][0]) + mock_dc_image.assert_called_once_with(mock_dc) + mock_dc_status.assert_called_with(mock_dc) + mock_dc.start.assert_not_called() + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_image') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_start_docker_containers_postgis_exception(self, mock_dc, mock_dc_image, mock_dc_status, + mock_pretty_output): + mock_dc_image.return_value = {'tethys_geoserver': 'foo'} + mock_dc_status.return_value = {'tethys_postgis': False, 'tethys_geoserver': False, 'tethys_wps': False} + mock_dc.start.side_effect = Exception + + self.assertRaises(Exception, cli_docker_commands.start_docker_containers, mock_dc, + containers=['postgis']) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('Starting PostGIS container...', po_call_args[0][0][0]) + mock_dc_image.assert_called_once_with(mock_dc) + mock_dc_status.assert_called_with(mock_dc) + mock_dc.start.assert_called_once() + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_image') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_start_docker_containers_geoserver_exception(self, mock_dc, mock_dc_image, mock_dc_status, + mock_pretty_output): + mock_dc_image.return_value = {'tethys_geoserver': 'foo'} + mock_dc_status.return_value = {'tethys_postgis': False, 'tethys_geoserver': False, 'tethys_wps': False} + mock_dc.start.side_effect = Exception + + self.assertRaises(Exception, cli_docker_commands.start_docker_containers, mock_dc, + containers=['geoserver']) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('Starting GeoServer container...', po_call_args[0][0][0]) + mock_dc_image.assert_called_once_with(mock_dc) + mock_dc_status.assert_called_with(mock_dc) + mock_dc.start.assert_called_once() + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_image') + @mock.patch('tethys_apps.cli.docker_commands.DockerClient') + def test_start_docker_containers_wps_exception(self, mock_dc, mock_dc_image, mock_dc_status, mock_pretty_output): + mock_dc_image.return_value = {'tethys_geoserver': 'foo'} + mock_dc_status.return_value = {'tethys_postgis': False, 'tethys_geoserver': False, 'tethys_wps': False} + mock_dc.start.side_effect = Exception + + self.assertRaises(Exception, cli_docker_commands.start_docker_containers, mock_dc, containers=['wps']) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('Starting 52 North WPS container...', po_call_args[0][0][0]) + mock_dc_image.assert_called_once_with(mock_dc) + mock_dc_status.assert_called_with(mock_dc) + mock_dc.start.assert_called_once() + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + def test_stop_docker_containers_already_stopped(self, mock_dc_status, mock_pretty_output): + mock_docker_client = mock.MagicMock() + mock_dc_status.return_value = {'tethys_postgis': False, 'tethys_geoserver': False, 'tethys_wps': False} + + cli_docker_commands.stop_docker_containers(mock_docker_client) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('PostGIS container already stopped.', po_call_args[0][0][0]) + self.assertEquals('GeoServer container already stopped.', po_call_args[1][0][0]) + self.assertEquals('52 North WPS container already stopped.', po_call_args[2][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + def test_stop_docker_containers_stopping(self, mock_dc_status, mock_pretty_output): + mock_docker_client = mock.MagicMock() + mock_docker_client.stop.return_value = True + mock_dc_status.return_value = {'tethys_postgis': True, 'tethys_geoserver': True, 'tethys_wps': True} + + cli_docker_commands.stop_docker_containers(mock_docker_client) + + mock_docker_client.stop.assert_any_call(container='tethys_postgis') + mock_docker_client.stop.assert_any_call(container='tethys_geoserver') + mock_docker_client.stop.assert_any_call(container='tethys_wps') + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertEquals('Stopping PostGIS container...', po_call_args[0][0][0]) + self.assertEquals('Stopping GeoServer container...', po_call_args[1][0][0]) + self.assertEquals('Stopping 52 North WPS container...', po_call_args[2][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.get_docker_container_status') + def test_stop_docker_containers_not_installed(self, mock_dc_status, mock_pretty_output): + mock_docker_client = mock.MagicMock() + mock_docker_client.stop.side_effect = KeyError + mock_dc_status.return_value = {'tethys_postgis': True, 'tethys_geoserver': True, 'tethys_wps': True} + + cli_docker_commands.stop_docker_containers(mock_docker_client) + + mock_docker_client.stop.assert_any_call(container='tethys_postgis') + mock_docker_client.stop.assert_any_call(container='tethys_geoserver') + mock_docker_client.stop.assert_any_call(container='tethys_wps') + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(6, len(po_call_args)) + self.assertEquals('Stopping PostGIS container...', po_call_args[0][0][0]) + self.assertEquals('PostGIS container not installed...', po_call_args[1][0][0]) + self.assertEquals('Stopping GeoServer container...', po_call_args[2][0][0]) + self.assertEquals('GeoServer container not installed...', po_call_args[3][0][0]) + self.assertEquals('Stopping 52 North WPS container...', po_call_args[4][0][0]) + self.assertEquals('52 North WPS container not installed...', po_call_args[5][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.curses') + @mock.patch('tethys_apps.cli.docker_commands.platform.system') + def test_log_pull_stream_linux_with_id_bad_status(self, mock_platform_system, mock_curses, mock_pretty_output): + mock_stream = ['{ "id":"358464", "status":"foo", "progress":"bar" }'] + mock_platform_system.return_value = 'Linux' + mock_curses.initscr().getmaxyx.return_value = 1, 80 + + cli_docker_commands.log_pull_stream(mock_stream) + + mock_curses.initscr().addstr.assert_any_call(0, 0, u'foo ' + u' ') + mock_curses.initscr().addstr.assert_called_with(1, 0, '--- ' + ' ') + mock_curses.initscr().refresh.assert_called_once() + mock_curses.noecho.assert_called_once() + mock_curses.cbreak.assert_called_once() + mock_curses.echo.assert_called_once() + mock_curses.nocbreak.assert_called_once() + mock_curses.endwin.assert_called_once() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.curses') + @mock.patch('tethys_apps.cli.docker_commands.platform.system') + def test_log_pull_stream_linux_with_id_progress_status(self, mock_platform_system, mock_curses, mock_pretty_output): + mock_stream = ['{ "id":"358464", "status":"Downloading", "progress":"bar" }'] + mock_platform_system.return_value = 'Linux' + mock_curses.initscr().getmaxyx.return_value = 1, 80 + + cli_docker_commands.log_pull_stream(mock_stream) + + mock_curses.initscr().addstr.assert_called_with(0, 0, '358464: Downloading bar ' + ' ') + mock_curses.initscr().refresh.assert_called_once() + mock_curses.noecho.assert_called_once() + mock_curses.cbreak.assert_called_once() + mock_curses.echo.assert_called_once() + mock_curses.nocbreak.assert_called_once() + mock_curses.endwin.assert_called_once() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.curses') + @mock.patch('tethys_apps.cli.docker_commands.platform.system') + def test_log_pull_stream_linux_with_id_status(self, mock_platform_system, mock_curses, mock_pretty_output): + mock_stream = ['{ "id":"358464", "status":"Downloading", "progress":"bar" }\r\n' + '{ "id":"358464", "status":"Pulling fs layer", "progress":"baz" }'] + mock_platform_system.return_value = 'Linux' + mock_curses.initscr().getmaxyx.return_value = 1, 80 + + cli_docker_commands.log_pull_stream(mock_stream) + + mock_curses.initscr().addstr.assert_called_with(0, 0, '358464: Downloading bar ' + ' ') + mock_curses.initscr().refresh.assert_called() + mock_curses.noecho.assert_called_once() + mock_curses.cbreak.assert_called_once() + mock_curses.echo.assert_called_once() + mock_curses.nocbreak.assert_called_once() + mock_curses.endwin.assert_called_once() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.curses') + @mock.patch('tethys_apps.cli.docker_commands.platform.system') + def test_log_pull_stream_linux_with_no_id(self, mock_platform_system, mock_curses, mock_pretty_output): + mock_stream = ['{ "status":"foo", "progress":"bar" }'] + mock_platform_system.return_value = 'Linux' + mock_curses.initscr().getmaxyx.return_value = 1, 80 + + cli_docker_commands.log_pull_stream(mock_stream) + + mock_curses.initscr().addstr.assert_not_called() + mock_curses.initscr().refresh.assert_not_called() + mock_curses.noecho.assert_called_once() + mock_curses.cbreak.assert_called_once() + mock_curses.echo.assert_called_once() + mock_curses.nocbreak.assert_called_once() + mock_curses.endwin.assert_called_once() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals(u'foo', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.pretty_output') + @mock.patch('tethys_apps.cli.docker_commands.platform.system') + def test_log_pull_stream_windows(self, mock_platform_system, mock_pretty_output): + mock_stream = ['{ "id":"358464", "status":"Downloading", "progress":"bar" }'] + mock_platform_system.return_value = 'Windows' + + cli_docker_commands.log_pull_stream(mock_stream) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEquals('358464:Downloading bar', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.docker_commands.input') + def test_validate_numeric_cli_input_second_empty_value(self, mock_input): + mock_input.side_effect = [''] + ret = cli_docker_commands.validate_numeric_cli_input(value='555', default=1, max=1) + self.assertEqual('1', ret) + + @mock.patch('tethys_apps.cli.docker_commands.input') + def test_validate_choice_cli_input_second_empty_value(self, mock_input): + mock_input.side_effect = [''] + ret = cli_docker_commands.validate_choice_cli_input(value='555', choices=['foo', 'bar'], default='foo') + self.assertEqual('foo', ret) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_gen_commands.py b/tests/unit_tests/test_tethys_apps/test_cli/test_gen_commands.py new file mode 100644 index 000000000..7551db80b --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_gen_commands.py @@ -0,0 +1,322 @@ +import unittest +import mock +import tethys_apps.cli.gen_commands +from tethys_apps.cli.gen_commands import get_environment_value, get_settings_value, generate_command +from tethys_apps.cli.gen_commands import GEN_SETTINGS_OPTION, GEN_NGINX_OPTION, GEN_UWSGI_SERVICE_OPTION,\ + GEN_UWSGI_SETTINGS_OPTION + +try: + reload +except NameError: # Python 3 + from imp import reload + + +class CLIGenCommandsTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_get_environment_value(self): + result = get_environment_value(value_name='DJANGO_SETTINGS_MODULE') + + self.assertEqual('tethys_portal.settings', result) + + def test_get_environment_value_bad(self): + self.assertRaises(EnvironmentError, get_environment_value, + value_name='foo_bar_baz_bad_environment_value_foo_bar_baz') + + def test_get_settings_value(self): + result = get_settings_value(value_name='INSTALLED_APPS') + + self.assertIn('tethys_apps', result) + + def test_get_settings_value_bad(self): + self.assertRaises(ValueError, get_settings_value, value_name='foo_bar_baz_bad_setting_foo_bar_baz') + + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_settings_option(self, mock_os_path_isfile, mock_file): + mock_args = mock.MagicMock() + mock_args.type = GEN_SETTINGS_OPTION + mock_args.directory = None + mock_os_path_isfile.return_value = False + + generate_command(args=mock_args) + + mock_os_path_isfile.assert_called_once() + mock_file.assert_called() + + @mock.patch('tethys_apps.cli.gen_commands.get_settings_value') + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_nginx_option(self, mock_os_path_isfile, mock_file, mock_settings): + mock_args = mock.MagicMock() + mock_args.type = GEN_NGINX_OPTION + mock_args.directory = None + mock_os_path_isfile.return_value = False + mock_settings.side_effect = ['/foo/workspace', '/foo/static'] + + generate_command(args=mock_args) + + mock_os_path_isfile.assert_called_once() + mock_file.assert_called() + mock_settings.assert_any_call('TETHYS_WORKSPACES_ROOT') + mock_settings.assert_called_with('STATIC_ROOT') + + @mock.patch('tethys_apps.cli.gen_commands.Context') + @mock.patch('tethys_apps.cli.gen_commands.linux_distribution') + @mock.patch('tethys_apps.cli.gen_commands.os.path.exists') + @mock.patch('tethys_apps.cli.gen_commands.get_environment_value') + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_uwsgi_service_option_nginx_conf_redhat(self, mock_os_path_isfile, mock_file, mock_env, + mock_os_path_exists, mock_linux_distribution, + mock_context): + mock_args = mock.MagicMock() + mock_args.type = GEN_UWSGI_SERVICE_OPTION + mock_args.directory = None + mock_os_path_isfile.return_value = False + mock_env.side_effect = ['/foo/conda', 'conda_env'] + mock_os_path_exists.return_value = True + mock_linux_distribution.return_value = ['redhat'] + # First open is for the Template, next two are for /etc/nginx/nginx.conf and /etc/passwd, and the final + # open is to "write" out the resulting file. The middle two opens return information about a user, while + # the first and last use MagicMock. + handlers = (mock_file.return_value, + mock.mock_open(read_data='user foo_user').return_value, + mock.mock_open(read_data='foo_user:x:1000:1000:Foo User,,,:/foo/nginx:/bin/bash').return_value, + mock_file.return_value) + mock_file.side_effect = handlers + + generate_command(args=mock_args) + + mock_os_path_isfile.assert_called_once() + mock_file.assert_called() + mock_env.assert_any_call('CONDA_HOME') + mock_env.assert_called_with('CONDA_ENV_NAME') + mock_os_path_exists.assert_called_once_with('/etc/nginx/nginx.conf') + context = mock_context().update.call_args_list[0][0][0] + self.assertEqual('http-', context['user_option_prefix']) + + @mock.patch('tethys_apps.cli.gen_commands.Context') + @mock.patch('tethys_apps.cli.gen_commands.linux_distribution') + @mock.patch('tethys_apps.cli.gen_commands.os.path.exists') + @mock.patch('tethys_apps.cli.gen_commands.get_environment_value') + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_uwsgi_service_option_nginx_conf_ubuntu(self, mock_os_path_isfile, mock_file, mock_env, + mock_os_path_exists, mock_linux_distribution, + mock_context): + mock_args = mock.MagicMock() + mock_args.type = GEN_UWSGI_SERVICE_OPTION + mock_args.directory = None + mock_os_path_isfile.return_value = False + mock_env.side_effect = ['/foo/conda', 'conda_env'] + mock_os_path_exists.return_value = True + mock_linux_distribution.return_value = 'ubuntu' + # First open is for the Template, next two are for /etc/nginx/nginx.conf and /etc/passwd, and the final + # open is to "write" out the resulting file. The middle two opens return information about a user, while + # the first and last use MagicMock. + handlers = (mock_file.return_value, + mock.mock_open(read_data='user foo_user').return_value, + mock.mock_open(read_data='foo_user:x:1000:1000:Foo User,,,:/foo/nginx:/bin/bash').return_value, + mock_file.return_value) + mock_file.side_effect = handlers + + generate_command(args=mock_args) + + mock_os_path_isfile.assert_called_once() + mock_file.assert_called() + mock_env.assert_any_call('CONDA_HOME') + mock_env.assert_called_with('CONDA_ENV_NAME') + mock_os_path_exists.assert_called_once_with('/etc/nginx/nginx.conf') + context = mock_context().update.call_args_list[0][0][0] + self.assertEqual('', context['user_option_prefix']) + + @mock.patch('tethys_apps.cli.gen_commands.Context') + @mock.patch('tethys_apps.cli.gen_commands.linux_distribution') + @mock.patch('tethys_apps.cli.gen_commands.os.path.exists') + @mock.patch('tethys_apps.cli.gen_commands.get_environment_value') + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_uwsgi_service_option_nginx_conf_not_linux(self, mock_os_path_isfile, mock_file, mock_env, + mock_os_path_exists, mock_linux_distribution, + mock_context): + mock_args = mock.MagicMock() + mock_args.type = GEN_UWSGI_SERVICE_OPTION + mock_args.directory = None + mock_os_path_isfile.return_value = False + mock_env.side_effect = ['/foo/conda', 'conda_env'] + mock_os_path_exists.return_value = True + mock_linux_distribution.side_effect = Exception + # First open is for the Template, next two are for /etc/nginx/nginx.conf and /etc/passwd, and the final + # open is to "write" out the resulting file. The middle two opens return information about a user, while + # the first and last use MagicMock. + handlers = (mock_file.return_value, + mock.mock_open(read_data='user foo_user').return_value, + mock.mock_open(read_data='foo_user:x:1000:1000:Foo User,,,:/foo/nginx:/bin/bash').return_value, + mock_file.return_value) + mock_file.side_effect = handlers + + generate_command(args=mock_args) + + mock_os_path_isfile.assert_called_once() + mock_file.assert_called() + mock_env.assert_any_call('CONDA_HOME') + mock_env.assert_called_with('CONDA_ENV_NAME') + mock_os_path_exists.assert_called_once_with('/etc/nginx/nginx.conf') + context = mock_context().update.call_args_list[0][0][0] + self.assertEqual('', context['user_option_prefix']) + + @mock.patch('tethys_apps.cli.gen_commands.get_environment_value') + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_uwsgi_service_option(self, mock_os_path_isfile, mock_file, mock_env): + mock_args = mock.MagicMock() + mock_args.type = GEN_UWSGI_SERVICE_OPTION + mock_args.directory = None + mock_os_path_isfile.return_value = False + mock_env.side_effect = ['/foo/conda', 'conda_env'] + + generate_command(args=mock_args) + + mock_os_path_isfile.assert_called_once() + mock_file.assert_called() + mock_env.assert_any_call('CONDA_HOME') + mock_env.assert_called_with('CONDA_ENV_NAME') + + @mock.patch('tethys_apps.cli.gen_commands.linux_distribution') + @mock.patch('tethys_apps.cli.gen_commands.get_environment_value') + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_uwsgi_service_option_distro(self, mock_os_path_isfile, mock_file, mock_env, + mock_distribution): + mock_args = mock.MagicMock() + mock_args.type = GEN_UWSGI_SERVICE_OPTION + mock_args.directory = None + mock_os_path_isfile.return_value = False + mock_env.side_effect = ['/foo/conda', 'conda_env'] + mock_distribution.return_value = ('redhat', 'linux', '') + + generate_command(args=mock_args) + + mock_os_path_isfile.assert_called_once() + mock_file.assert_called() + mock_env.assert_any_call('CONDA_HOME') + mock_env.assert_called_with('CONDA_ENV_NAME') + + @mock.patch('tethys_apps.cli.gen_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.gen_commands.get_environment_value') + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_uwsgi_settings_option_directory(self, mock_os_path_isfile, mock_file, mock_env, + mock_os_path_isdir): + mock_args = mock.MagicMock() + mock_args.type = GEN_UWSGI_SETTINGS_OPTION + mock_args.directory = '/foo/temp' + mock_os_path_isfile.return_value = False + mock_env.side_effect = ['/foo/conda', 'conda_env'] + mock_os_path_isdir.return_value = True + + generate_command(args=mock_args) + + mock_os_path_isfile.assert_called_once() + mock_file.assert_called() + mock_os_path_isdir.assert_called_once_with(mock_args.directory) + mock_env.assert_any_call('CONDA_HOME') + mock_env.assert_called_with('CONDA_ENV_NAME') + + @mock.patch('tethys_apps.cli.gen_commands.print') + @mock.patch('tethys_apps.cli.gen_commands.exit') + @mock.patch('tethys_apps.cli.gen_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.gen_commands.get_environment_value') + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_uwsgi_settings_option_bad_directory(self, mock_os_path_isfile, mock_file, mock_env, + mock_os_path_isdir, mock_exit, mock_print): + mock_args = mock.MagicMock() + mock_args.type = GEN_UWSGI_SETTINGS_OPTION + mock_args.directory = '/foo/temp' + mock_os_path_isfile.return_value = False + mock_env.side_effect = ['/foo/conda', 'conda_env'] + mock_os_path_isdir.return_value = False + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, generate_command, args=mock_args) + + mock_os_path_isfile.assert_not_called() + mock_file.assert_called_once() + mock_os_path_isdir.assert_called_once_with(mock_args.directory) + + # Check if print is called correctly + rts_call_args = mock_print.call_args_list + self.assertIn('ERROR: ', rts_call_args[0][0][0]) + self.assertIn('is not a valid directory', rts_call_args[0][0][0]) + + mock_env.assert_any_call('CONDA_HOME') + mock_env.assert_called_with('CONDA_ENV_NAME') + + @mock.patch('tethys_apps.cli.gen_commands.print') + @mock.patch('tethys_apps.cli.gen_commands.exit') + @mock.patch('tethys_apps.cli.gen_commands.input') + @mock.patch('tethys_apps.cli.gen_commands.get_environment_value') + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_uwsgi_settings_pre_existing_input_exit(self, mock_os_path_isfile, mock_file, mock_env, + mock_input, mock_exit, mock_print): + mock_args = mock.MagicMock() + mock_args.type = GEN_UWSGI_SETTINGS_OPTION + mock_args.directory = None + mock_args.overwrite = False + mock_os_path_isfile.return_value = True + mock_env.side_effect = ['/foo/conda', 'conda_env'] + mock_input.side_effect = ['foo', 'no'] + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, generate_command, args=mock_args) + + mock_os_path_isfile.assert_called_once() + mock_file.assert_called() + + # Check if print is called correctly + rts_call_args = mock_print.call_args_list + self.assertIn('Generation of', rts_call_args[0][0][0]) + self.assertIn('cancelled', rts_call_args[0][0][0]) + + mock_env.assert_any_call('CONDA_HOME') + mock_env.assert_called_with('CONDA_ENV_NAME') + + @mock.patch('tethys_apps.cli.gen_commands.get_environment_value') + @mock.patch('tethys_apps.cli.gen_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.gen_commands.os.path.isfile') + def test_generate_command_uwsgi_settings_pre_existing_overwrite(self, mock_os_path_isfile, mock_file, mock_env): + mock_args = mock.MagicMock() + mock_args.type = GEN_UWSGI_SETTINGS_OPTION + mock_args.directory = None + mock_args.overwrite = True + mock_os_path_isfile.return_value = True + mock_env.side_effect = ['/foo/conda', 'conda_env'] + + generate_command(args=mock_args) + + mock_os_path_isfile.assert_called_once() + mock_file.assert_called() + mock_env.assert_any_call('CONDA_HOME') + mock_env.assert_called_with('CONDA_ENV_NAME') + + @mock.patch('tethys_apps.cli.gen_commands.os.environ') + def test_django_settings_module_error(self, mock_environ): + mock_environ.side_effect = Exception + try: + reload(tethys_apps.cli.gen_commands) + except Exception: + pass + + self.assertTrue(tethys_apps.cli.gen_commands.settings.configured) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_link_commands.py b/tests/unit_tests/test_tethys_apps/test_cli/test_link_commands.py new file mode 100644 index 000000000..54ab68a9c --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_link_commands.py @@ -0,0 +1,74 @@ +import unittest +import mock +import tethys_apps.cli.link_commands as link_commands + + +class TestLinkCommands(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.cli.link_commands.exit') + @mock.patch('tethys_apps.cli.link_commands.link_service_to_app_setting') + @mock.patch('tethys_apps.cli.link_commands.pretty_output') + def test_link_commands(self, _, mock_link_app_setting, mock_exit): + args = mock.MagicMock(service='persistent_connection:super_conn', setting='epanet:database:epanet_2') + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, link_commands.link_command, args) + + mock_link_app_setting.assert_called_with('persistent_connection', 'super_conn', 'epanet', 'database', + 'epanet_2') + mock_exit.assert_called_with(0) + + @mock.patch('tethys_apps.cli.link_commands.exit') + @mock.patch('tethys_apps.cli.link_commands.link_service_to_app_setting') + @mock.patch('tethys_apps.cli.link_commands.pretty_output') + def test_link_commands_index_error(self, mock_pretty_output, mock_link_app_setting, mock_exit): + args = mock.MagicMock(service='con1', setting='db:database') + # NOTE: We have the mocked exit function raise a SystemExit exception to break the code + # execution like the original exit function would have done. + mock_exit.side_effect = SystemExit + + try: + self.assertRaises(IndexError, link_commands.link_command, args) + except SystemExit: + pass + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertIn("Incorrect argument format", po_call_args[0][0][0]) + mock_link_app_setting.assert_not_called() + mock_exit.assert_called_with(1) + + @mock.patch('tethys_apps.cli.link_commands.exit') + @mock.patch('tethys_apps.cli.link_commands.link_service_to_app_setting') + @mock.patch('tethys_apps.cli.link_commands.pretty_output') + def test_link_commands_with_exception(self, mock_pretty_output, mock_link_app_setting, mock_exit): + # NOTE: We have the mocked exit function raise a SystemExit exception to break the code + # execution like the original exit function would have done. + mock_link_app_setting = mock.MagicMock() + mock_link_app_setting.return_value = None + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, link_commands.link_command, None) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertIn("An unexpected error occurred. Please try again.", po_call_args[1][0][0]) + mock_exit.assert_called_with(1) + + @mock.patch('tethys_apps.cli.link_commands.exit') + @mock.patch('tethys_apps.cli.link_commands.link_service_to_app_setting', return_value=False) + @mock.patch('tethys_apps.cli.link_commands.pretty_output') + def test_link_commands_with_no_success(self, _, mock_link_app_setting, mock_exit): + # NOTE: We have the mocked exit function raise a SystemExit exception to break the code + # execution like the original exit function would have done. + args = mock.MagicMock(service='persistent_connection:super_conn', setting='epanet:database:epanet_2') + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, link_commands.link_command, args) + + mock_link_app_setting.assert_called_with('persistent_connection', 'super_conn', 'epanet', 'database', + 'epanet_2') + mock_exit.assert_called_with(1) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_list_commands.py b/tests/unit_tests/test_tethys_apps/test_cli/test_list_commands.py new file mode 100644 index 000000000..2252a2644 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_list_commands.py @@ -0,0 +1,82 @@ +import unittest +import mock + +from tethys_apps.cli.list_command import list_command +try: + from StringIO import StringIO +except ImportError: + from io import StringIO # noqa: F401 + + +class ListCommandTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.cli.list_command.print') + @mock.patch('tethys_apps.cli.list_command.get_installed_tethys_extensions') + @mock.patch('tethys_apps.cli.list_command.get_installed_tethys_apps') + def test_list_command_installed_apps(self, mock_installed_apps, mock_installed_extensions, mock_print): + mock_args = mock.MagicMock() + mock_installed_apps.return_value = {'foo': '/foo', 'bar': "/bar"} + mock_installed_extensions.return_value = {} + + list_command(mock_args) + + mock_installed_apps.assert_called_once() + + # Check if print is called correctly + rts_call_args = mock_print.call_args_list + + check_list = [] + for i in range(len(rts_call_args)): + check_list.append(rts_call_args[i][0][0]) + + self.assertIn('Apps:', check_list) + self.assertIn(' foo', check_list) + self.assertIn(' bar', check_list) + + @mock.patch('tethys_apps.cli.list_command.print') + @mock.patch('tethys_apps.cli.list_command.get_installed_tethys_extensions') + @mock.patch('tethys_apps.cli.list_command.get_installed_tethys_apps') + def test_list_command_installed_extensions(self, mock_installed_apps, mock_installed_extensions, mock_print): + mock_args = mock.MagicMock() + mock_installed_apps.return_value = {} + mock_installed_extensions.return_value = {'baz': '/baz'} + + list_command(mock_args) + + # Check if print is called correctly + rts_call_args = mock_print.call_args_list + check_list = [] + for i in range(len(rts_call_args)): + check_list.append(rts_call_args[i][0][0]) + + self.assertIn('Extensions:', check_list) + self.assertIn(' baz', check_list) + + @mock.patch('tethys_apps.cli.list_command.print') + @mock.patch('tethys_apps.cli.list_command.get_installed_tethys_extensions') + @mock.patch('tethys_apps.cli.list_command.get_installed_tethys_apps') + def test_list_command_installed_both(self, mock_installed_apps, mock_installed_extensions, mock_print): + mock_args = mock.MagicMock() + mock_installed_apps.return_value = {'foo': '/foo', 'bar': "/bar"} + mock_installed_extensions.return_value = {'baz': '/baz'} + + list_command(mock_args) + + # Check if print is called correctly + rts_call_args = mock_print.call_args_list + + check_list = [] + for i in range(len(rts_call_args)): + check_list.append(rts_call_args[i][0][0]) + + self.assertIn('Apps:', check_list) + self.assertIn(' foo', check_list) + self.assertIn(' bar', check_list) + self.assertIn('Extensions:', check_list) + self.assertIn(' baz', check_list) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_manage_commands.py b/tests/unit_tests/test_tethys_apps/test_cli/test_manage_commands.py new file mode 100644 index 000000000..c547697f4 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_manage_commands.py @@ -0,0 +1,279 @@ +import unittest +import mock +import tethys_apps.cli.manage_commands as manage_commands +from tethys_apps.cli.manage_commands import MANAGE_START, MANAGE_SYNCDB, MANAGE_COLLECTSTATIC, \ + MANAGE_COLLECTWORKSPACES, MANAGE_COLLECT, MANAGE_CREATESUPERUSER, MANAGE_SYNC + + +class TestManageCommands(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_get_manage_path(self): + # mock the input args with manage attribute + args = mock.MagicMock(manage='') + + # call the method + ret = manage_commands.get_manage_path(args=args) + + # check whether the response has manage + self.assertIn('manage.py', ret) + + @mock.patch('tethys_apps.cli.manage_commands.pretty_output') + @mock.patch('tethys_apps.cli.manage_commands.exit') + def test_get_manage_path_error(self, mock_exit, mock_pretty_output): + # mock the system exit + mock_exit.side_effect = SystemExit + + # mock the input args with manage attribute + args = mock.MagicMock(manage='foo') + + self.assertRaises(SystemExit, manage_commands.get_manage_path, args=args) + + # check the mock exit value + mock_exit.assert_called_with(1) + mock_pretty_output.assert_called() + + @mock.patch('tethys_apps.cli.manage_commands.run_process') + def test_manage_command_manage_start(self, mock_run_process): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_START, port='8080') + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # get the call arguments for the run process mock method + process_call_args = mock_run_process.call_args_list + + # check the values from the argument list + self.assertEquals('python', process_call_args[0][0][0][0]) + self.assertIn('manage.py', process_call_args[0][0][0][1]) + self.assertEquals('runserver', process_call_args[0][0][0][2]) + self.assertEquals('8080', process_call_args[0][0][0][3]) + + @mock.patch('tethys_apps.cli.manage_commands.run_process') + def test_manage_command_manage_start_with_no_port(self, mock_run_process): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_START, port='') + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # get the call arguments for the run process mock method + process_call_args = mock_run_process.call_args_list + + # check the values from the argument list + self.assertEquals('python', process_call_args[0][0][0][0]) + self.assertIn('manage.py', process_call_args[0][0][0][1]) + self.assertEquals('runserver', process_call_args[0][0][0][2]) + + @mock.patch('tethys_apps.cli.manage_commands.run_process') + def test_manage_command_manage_syncdb(self, mock_run_process): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_SYNCDB, port='8080') + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # get the call arguments for the run process mock method + process_call_args = mock_run_process.call_args_list + + # intermediate process + self.assertEquals('python', process_call_args[0][0][0][0]) + self.assertIn('manage.py', process_call_args[0][0][0][1]) + self.assertEquals('makemigrations', process_call_args[0][0][0][2]) + + # primary process + self.assertEquals('python', process_call_args[1][0][0][0]) + self.assertIn('manage.py', process_call_args[1][0][0][1]) + self.assertEquals('migrate', process_call_args[1][0][0][2]) + + @mock.patch('tethys_apps.cli.manage_commands.run_process') + def test_manage_command_manage_manage_collectstatic(self, mock_run_process): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_COLLECTSTATIC, port='8080', noinput=False) + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # get the call arguments for the run process mock method + process_call_args = mock_run_process.call_args_list + + # intermediate process + self.assertEquals('python', process_call_args[0][0][0][0]) + self.assertIn('manage.py', process_call_args[0][0][0][1]) + self.assertEquals('pre_collectstatic', process_call_args[0][0][0][2]) + + # primary process + self.assertEquals('python', process_call_args[1][0][0][0]) + self.assertIn('manage.py', process_call_args[1][0][0][1]) + self.assertEquals('collectstatic', process_call_args[1][0][0][2]) + self.assertNotIn('--noinput', process_call_args[1][0][0]) + + @mock.patch('tethys_apps.cli.manage_commands.run_process') + def test_manage_command_manage_manage_collectstatic_with_no_input(self, mock_run_process): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_COLLECTSTATIC, port='8080', noinput=True) + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # get the call arguments for the run process mock method + process_call_args = mock_run_process.call_args_list + + # intermediate process + self.assertEquals('python', process_call_args[0][0][0][0]) + self.assertIn('manage.py', process_call_args[0][0][0][1]) + self.assertEquals('pre_collectstatic', process_call_args[0][0][0][2]) + + # primary process + self.assertEquals('python', process_call_args[1][0][0][0]) + self.assertIn('manage.py', process_call_args[1][0][0][1]) + self.assertEquals('collectstatic', process_call_args[1][0][0][2]) + self.assertEquals('--noinput', process_call_args[1][0][0][3]) + + @mock.patch('tethys_apps.cli.manage_commands.run_process') + def test_manage_command_manage_manage_collect_workspace(self, mock_run_process): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_COLLECTWORKSPACES, port='8080', force=True) + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # get the call arguments for the run process mock method + process_call_args = mock_run_process.call_args_list + + # check the values from the argument list + self.assertEquals('python', process_call_args[0][0][0][0]) + self.assertIn('manage.py', process_call_args[0][0][0][1]) + self.assertEquals('collectworkspaces', process_call_args[0][0][0][2]) + self.assertEquals('--force', process_call_args[0][0][0][3]) + + @mock.patch('tethys_apps.cli.manage_commands.run_process') + def test_manage_command_manage_manage_collect_workspace_with_no_force(self, mock_run_process): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_COLLECTWORKSPACES, force=False) + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # get the call arguments for the run process mock method + process_call_args = mock_run_process.call_args_list + + # check the values from the argument list + self.assertEquals('python', process_call_args[0][0][0][0]) + self.assertIn('manage.py', process_call_args[0][0][0][1]) + self.assertEquals('collectworkspaces', process_call_args[0][0][0][2]) + self.assertNotIn('--force', process_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.manage_commands.run_process') + def test_manage_command_manage_manage_collect(self, mock_run_process): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_COLLECT, port='8080', noinput=False) + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # get the call arguments for the run process mock method + process_call_args = mock_run_process.call_args_list + + # pre_collectstatic + self.assertEquals('python', process_call_args[0][0][0][0]) + self.assertIn('manage.py', process_call_args[0][0][0][1]) + self.assertEquals('pre_collectstatic', process_call_args[0][0][0][2]) + + # collectstatic + self.assertEquals('python', process_call_args[1][0][0][0]) + self.assertIn('manage.py', process_call_args[1][0][0][1]) + self.assertEquals('collectstatic', process_call_args[1][0][0][2]) + self.assertNotIn('--noinput', process_call_args[1][0][0]) + + # collectworkspaces + self.assertEquals('python', process_call_args[2][0][0][0]) + self.assertIn('manage.py', process_call_args[2][0][0][1]) + self.assertEquals('collectworkspaces', process_call_args[2][0][0][2]) + + @mock.patch('tethys_apps.cli.manage_commands.run_process') + def test_manage_command_manage_manage_collect_no_input(self, mock_run_process): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_COLLECT, port='8080', noinput=True) + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # get the call arguments for the run process mock method + process_call_args = mock_run_process.call_args_list + + # pre_collectstatic + self.assertEquals('python', process_call_args[0][0][0][0]) + self.assertIn('manage.py', process_call_args[0][0][0][1]) + self.assertEquals('pre_collectstatic', process_call_args[0][0][0][2]) + + # collectstatic + self.assertEquals('python', process_call_args[1][0][0][0]) + self.assertIn('manage.py', process_call_args[1][0][0][1]) + self.assertEquals('collectstatic', process_call_args[1][0][0][2]) + self.assertEquals('--noinput', process_call_args[1][0][0][3]) + + # collectworkspaces + self.assertEquals('python', process_call_args[2][0][0][0]) + self.assertIn('manage.py', process_call_args[2][0][0][1]) + self.assertEquals('collectworkspaces', process_call_args[2][0][0][2]) + + @mock.patch('tethys_apps.cli.manage_commands.run_process') + def test_manage_command_manage_manage_create_super_user(self, mock_run_process): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_CREATESUPERUSER, port='8080') + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # get the call arguments for the run process mock method + process_call_args = mock_run_process.call_args_list + + # check the values from the argument list + self.assertEquals('python', process_call_args[0][0][0][0]) + self.assertIn('manage.py', process_call_args[0][0][0][1]) + self.assertEquals('createsuperuser', process_call_args[0][0][0][2]) + + @mock.patch('tethys_apps.harvester.SingletonHarvester') + def test_manage_command_manage_manage_sync(self, MockSingletonHarvester): + # mock the input args + args = mock.MagicMock(manage='', command=MANAGE_SYNC, port='8080') + + # call the testing method with the mock args + manage_commands.manage_command(args) + + # mock the singleton harvester + MockSingletonHarvester.assert_called() + MockSingletonHarvester().harvest.assert_called() + + @mock.patch('tethys_apps.cli.manage_commands.subprocess.call') + @mock.patch('tethys_apps.cli.manage_commands.set_testing_environment') + def test_run_process(self, mock_te_call, mock_subprocess_call): + + # mock the process + mock_process = ['test'] + + manage_commands.run_process(mock_process) + + self.assertEqual(2, len(mock_te_call.call_args_list)) + + mock_subprocess_call.assert_called_with(mock_process) + + @mock.patch('tethys_apps.cli.manage_commands.subprocess.call') + @mock.patch('tethys_apps.cli.manage_commands.set_testing_environment') + def test_run_process_keyboardinterrupt(self, mock_te_call, mock_subprocess_call): + + # mock the process + mock_process = ['foo'] + + mock_subprocess_call.side_effect = KeyboardInterrupt + + manage_commands.run_process(mock_process) + mock_subprocess_call.assert_called_with(mock_process) + mock_te_call.assert_called_once() diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_scaffold_commands.py b/tests/unit_tests/test_tethys_apps/test_cli/test_scaffold_commands.py new file mode 100644 index 000000000..3ea125acd --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_scaffold_commands.py @@ -0,0 +1,1255 @@ +import unittest +import mock +import os +import sys + +from tethys_apps.cli.scaffold_commands import proper_name_validator, get_random_color, theme_color_validator, \ + render_path, scaffold_command + +if sys.version_info[0] < 3: + callable_mock_path = '__builtin__.callable' +else: + callable_mock_path = 'builtins.callable' + + +class TestScaffoldCommands(unittest.TestCase): + + def setUp(self): + self.app_prefix = 'tethysapp' + self.extensions_prefix = 'tethysext' + self.scaffold_templates_dir = 'scaffold_templates' + self.extension_template_dir = 'extension_templates' + self.app_template_dir = 'app_templates' + self.template_suffix = '_tmpl' + self.app_path = os.path.join(os.path.dirname(__file__), self.scaffold_templates_dir, self.app_template_dir) + self.extension_path = os.path.join(os.path.dirname(__file__), self.scaffold_templates_dir, + self.extension_template_dir) + + def tearDown(self): + pass + + def test_proper_name_validator(self): + expected_value = 'foo' + expected_default = 'bar' + ret = proper_name_validator(expected_value, expected_default) + self.assertTrue(ret[0]) + self.assertEquals('foo', ret[1]) + + def test_proper_name_validator_value_as_default(self): + expected_value = 'bar' + expected_default = 'bar' + ret = proper_name_validator(expected_value, expected_default) + self.assertTrue(ret[0]) + self.assertEquals('bar', ret[1]) + + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + def test_proper_name_validator_warning(self, mock_pretty_output): + expected_value = 'foo_' + expected_default = 'bar' + ret = proper_name_validator(expected_value, expected_default) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals('Warning: Illegal characters were detected in proper name "foo_". ' + 'They have been replaced or removed with valid characters: "foo "', po_call_args[0][0][0]) + self.assertTrue(ret[0]) + self.assertEquals('foo ', ret[1]) + + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + def test_proper_name_validator_error(self, mock_pretty_output): + expected_value = '@@' + expected_default = 'bar' + ret = proper_name_validator(expected_value, expected_default) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals('Error: Proper name can only contain letters and numbers and spaces.', po_call_args[0][0][0]) + self.assertFalse(ret[0]) + self.assertEquals('@@', ret[1]) + + @mock.patch('tethys_apps.cli.scaffold_commands.random.choice') + def test_get_random_color(self, mock_choice): + mock_choice.return_value = '#16a085' + ret = get_random_color() + self.assertEquals(mock_choice.return_value, ret) + + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + def test_theme_color_validator_same_default_value(self, mock_random_color): + expected_value = 'foo' + expected_default = 'foo' + mock_random_color.return_value = '#16a085' + ret = theme_color_validator(expected_value, expected_default) + self.assertTrue(ret[0]) + self.assertEquals('#16a085', ret[1]) + + def test_theme_color_validator(self): + expected_value = '#8e44ad' + expected_default = '#16a085' + ret = theme_color_validator(expected_value, expected_default) + self.assertTrue(ret[0]) + self.assertEquals('#8e44ad', ret[1]) + + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + def test_theme_color_validator_exception(self, mock_pretty_output): + expected_value = 'foo' + expected_default = 'bar' + ret = theme_color_validator(expected_value, expected_default) + mock_pretty_output.assert_called() + self.assertFalse(ret[0]) + self.assertEquals('foo', ret[1]) + + def test_render_path_with_plus(self): + expected_path = '+ite1+' + expected_context = {'ite1': 'foo'} + ret = render_path(expected_path, expected_context) + self.assertEquals('foo', ret) + + def test_render_path(self): + expected_path = 'variable' + mock_context = mock.MagicMock() + ret = render_path(expected_path, mock_context) + self.assertEquals('variable', ret) + + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch(callable_mock_path) + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command(self, _, __, mock_makedirs, mock_os_walk, mock_render_path, mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, mock_random_color, mock_callable, mock_logger): + + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'project_name' + mock_args.use_defaults = True + mock_args.overwrite = True + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + # mocking the validate template call return value + mock_os_path_isdir.return_value = [True, True] + + mock_callable.return_value = True + + mock_template_context = mock.MagicMock() + + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + scaffold_command(args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_called() + + mock_render_path.assert_called() + + mock_makedirs.assert_called_with(mock_cuurent_project_root) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[0][0][0]) + self.assertIn('Created:', po_call_args[1][0][0]) + self.assertIn('Created:', po_call_args[2][0][0]) + self.assertIn('Created:', po_call_args[3][0][0]) + self.assertIn('Created:', po_call_args[4][0][0]) + self.assertIn('Created:', po_call_args[5][0][0]) + self.assertEquals('Successfully scaffolded new project "project_name"', po_call_args[6][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) + self.assertEquals('Loading template: "/foo/baz"', mock_log_call_args[6][0][0]) + self.assertEquals('Rendering template: "/foo/baz"', mock_log_call_args[7][0][0]) + self.assertEquals('Loading template: "/foo/bar/spam"', mock_log_call_args[8][0][0]) + self.assertEquals('Rendering template: "/foo/bar/spam"', mock_log_call_args[9][0][0]) + self.assertEquals('Loading template: "/foo/bar/eggs"', mock_log_call_args[10][0][0]) + self.assertEquals('Rendering template: "/foo/bar/eggs"', mock_log_call_args[11][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.exit') + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + def test_scaffold_command_with_not_valid_template(self, mock_os_path_isdir, mock_pretty_output, mock_logger, + mock_exit): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = '@@' + mock_args.use_defaults = True + mock_args.overwrite = True + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + mock_os_path_isdir.return_value = False + + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, scaffold_command, args=mock_args) + + mock_pretty_output.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Error: "template_name" is not a valid template.', + po_call_args[0][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch(callable_mock_path) + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command_with_no_extension(self, _, __, mock_makedirs, mock_os_walk, mock_render_path, + mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, mock_random_color, + mock_callable, mock_logger): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = None + mock_args.template = 'template_name' + mock_args.name = 'project_name' + mock_args.use_defaults = True + mock_args.overwrite = True + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + mock_os_path_isdir.return_value = [True, True] + + mock_callable.return_value = True + + mock_template_context = mock.MagicMock() + + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + scaffold_command(args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_called() + + mock_render_path.assert_called() + + mock_makedirs.assert_called_with(mock_cuurent_project_root) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Creating new Tethys project named "tethysapp-project_name".', po_call_args[0][0][0]) + self.assertIn('Created:', po_call_args[1][0][0]) + self.assertIn('Created:', po_call_args[2][0][0]) + self.assertIn('Created:', po_call_args[3][0][0]) + self.assertIn('Created:', po_call_args[4][0][0]) + self.assertIn('Created:', po_call_args[5][0][0]) + self.assertEquals('Successfully scaffolded new project "project_name"', po_call_args[6][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) + self.assertEquals('Loading template: "/foo/baz"', mock_log_call_args[6][0][0]) + self.assertEquals('Rendering template: "/foo/baz"', mock_log_call_args[7][0][0]) + self.assertEquals('Loading template: "/foo/bar/spam"', mock_log_call_args[8][0][0]) + self.assertEquals('Rendering template: "/foo/bar/spam"', mock_log_call_args[9][0][0]) + self.assertEquals('Loading template: "/foo/bar/eggs"', mock_log_call_args[10][0][0]) + self.assertEquals('Rendering template: "/foo/bar/eggs"', mock_log_call_args[11][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch(callable_mock_path) + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command_with_uppercase_project_name(self, _, __, mock_makedirs, mock_os_walk, mock_render_path, + mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, mock_random_color, + mock_callable, mock_logger): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'PROJECT_NAME' + mock_args.use_defaults = True + mock_args.overwrite = True + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + # mocking the validate template call return value + mock_os_path_isdir.return_value = [True, True] + + mock_callable.return_value = True + + mock_template_context = mock.MagicMock() + + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + scaffold_command(args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_called() + + mock_render_path.assert_called() + + mock_makedirs.assert_called_with(mock_cuurent_project_root) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Warning: Uppercase characters in project name "PROJECT_NAME" changed to ' + 'lowercase: "project_name".', po_call_args[0][0][0]) + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[1][0][0]) + self.assertIn('Created:', po_call_args[2][0][0]) + self.assertIn('Created:', po_call_args[3][0][0]) + self.assertIn('Created:', po_call_args[4][0][0]) + self.assertIn('Created:', po_call_args[5][0][0]) + self.assertIn('Created:', po_call_args[6][0][0]) + self.assertEquals('Successfully scaffolded new project "project_name"', po_call_args[7][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) + self.assertEquals('Loading template: "/foo/baz"', mock_log_call_args[6][0][0]) + self.assertEquals('Rendering template: "/foo/baz"', mock_log_call_args[7][0][0]) + self.assertEquals('Loading template: "/foo/bar/spam"', mock_log_call_args[8][0][0]) + self.assertEquals('Rendering template: "/foo/bar/spam"', mock_log_call_args[9][0][0]) + self.assertEquals('Loading template: "/foo/bar/eggs"', mock_log_call_args[10][0][0]) + self.assertEquals('Rendering template: "/foo/bar/eggs"', mock_log_call_args[11][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.exit') + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + def test_scaffold_command_with_wrong_project_name(self, mock_os_path_isdir, mock_pretty_output, mock_logger, + mock_exit): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = '@@' + mock_args.use_defaults = True + mock_args.overwrite = True + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + mock_os_path_isdir.return_value = True + + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, scaffold_command, args=mock_args) + + mock_pretty_output.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Error: Invalid characters in project name "@@". Only letters, numbers, and underscores.', + po_call_args[0][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch(callable_mock_path) + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command_with_project_warning(self, _, __, mock_makedirs, mock_os_walk, mock_render_path, + mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, mock_random_color, + mock_callable, mock_logger): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'project-name' + mock_args.use_defaults = True + mock_args.overwrite = True + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + mock_os_path_isdir.return_value = [True, True] + + mock_callable.return_value = True + + mock_template_context = mock.MagicMock() + + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + scaffold_command(args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_called() + + mock_render_path.assert_called() + + mock_makedirs.assert_called_with(mock_cuurent_project_root) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Warning: Dashes in project name "project-name" have been replaced with underscores ' + '"project_name"', po_call_args[0][0][0]) + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[1][0][0]) + self.assertIn('Created:', po_call_args[2][0][0]) + self.assertIn('Created:', po_call_args[3][0][0]) + self.assertIn('Created:', po_call_args[4][0][0]) + self.assertIn('Created:', po_call_args[5][0][0]) + self.assertIn('Created:', po_call_args[6][0][0]) + self.assertEquals('Successfully scaffolded new project "project_name"', po_call_args[7][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) + self.assertEquals('Loading template: "/foo/baz"', mock_log_call_args[6][0][0]) + self.assertEquals('Rendering template: "/foo/baz"', mock_log_call_args[7][0][0]) + self.assertEquals('Loading template: "/foo/bar/spam"', mock_log_call_args[8][0][0]) + self.assertEquals('Rendering template: "/foo/bar/spam"', mock_log_call_args[9][0][0]) + self.assertEquals('Loading template: "/foo/bar/eggs"', mock_log_call_args[10][0][0]) + self.assertEquals('Rendering template: "/foo/bar/eggs"', mock_log_call_args[11][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.proper_name_validator') + @mock.patch('tethys_apps.cli.scaffold_commands.input') + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch(callable_mock_path) + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command_with_no_defaults(self, _, __, mock_makedirs, mock_os_walk, mock_render_path, + mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, mock_random_color, + mock_callable, mock_logger, mock_input, mock_proper_name_validator): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'project-name' + mock_args.use_defaults = False + mock_args.overwrite = True + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + mock_os_path_isdir.return_value = [True, True] + + mock_callable.side_effect = [True, False, False, False, False] + + mock_template_context = mock.MagicMock() + + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + mock_input.side_effect = ['test1', 'test2', 'test3', 'test4', 'test5'] + mock_proper_name_validator.return_value = True, 'foo' + + scaffold_command(args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_called() + + mock_render_path.assert_called() + + # mock the create root directory + mock_makedirs.assert_called_with(mock_cuurent_project_root) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Warning: Dashes in project name "project-name" have been replaced with underscores ' + '"project_name"', po_call_args[0][0][0]) + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[1][0][0]) + self.assertIn('Created:', po_call_args[2][0][0]) + self.assertIn('Created:', po_call_args[3][0][0]) + self.assertIn('Created:', po_call_args[4][0][0]) + self.assertIn('Created:', po_call_args[5][0][0]) + self.assertIn('Created:', po_call_args[6][0][0]) + self.assertEquals('Successfully scaffolded new project "project_name"', po_call_args[7][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('foo', mock_log_call_args[4][0][0]) + self.assertIn('test2', mock_log_call_args[4][0][0]) + self.assertIn('test3', mock_log_call_args[4][0][0]) + self.assertIn('test4', mock_log_call_args[4][0][0]) + self.assertIn('test5', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) + self.assertEquals('Loading template: "/foo/baz"', mock_log_call_args[6][0][0]) + self.assertEquals('Rendering template: "/foo/baz"', mock_log_call_args[7][0][0]) + self.assertEquals('Loading template: "/foo/bar/spam"', mock_log_call_args[8][0][0]) + self.assertEquals('Rendering template: "/foo/bar/spam"', mock_log_call_args[9][0][0]) + self.assertEquals('Loading template: "/foo/bar/eggs"', mock_log_call_args[10][0][0]) + self.assertEquals('Rendering template: "/foo/bar/eggs"', mock_log_call_args[11][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.proper_name_validator') + @mock.patch('tethys_apps.cli.scaffold_commands.input') + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch(callable_mock_path) + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + @mock.patch('tethys_apps.cli.scaffold_commands.exit') + def test_scaffold_command_with_no_defaults_input_exception(self, mock_exit, _, __, mock_makedirs, mock_os_walk, + mock_render_path, mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, + mock_random_color, mock_callable, + mock_logger, mock_input, mock_proper_name_validator): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'project-name' + mock_args.use_defaults = False + mock_args.overwrite = True + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + # mocking the validate template call return value + mock_os_path_isdir.return_value = [True, True] + + mock_callable.side_effect = [True, False, False, False, False] + + mock_template_context = mock.MagicMock() + + # testing: walk the template directory, creating the templates and directories in the new project as we go + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + mock_exit.side_effect = SystemExit + + mock_input.side_effect = KeyboardInterrupt + + mock_proper_name_validator.return_value = True, 'foo' + + self.assertRaises(SystemExit, scaffold_command, args=mock_args) + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_not_called() + + mock_render_path.assert_not_called() + + # mock the create root directory + mock_makedirs.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Warning: Dashes in project name "project-name" have been replaced with underscores ' + '"project_name"', po_call_args[0][0][0]) + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[1][0][0]) + self.assertIn('Scaffolding cancelled.', po_call_args[2][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.proper_name_validator') + @mock.patch('tethys_apps.cli.scaffold_commands.input') + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch('tethys_apps.cli.scaffold_commands.callable') + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command_with_no_defaults_invalid_response(self, _, __, mock_makedirs, mock_os_walk, + mock_render_path, + mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, + mock_random_color, + mock_callable, mock_logger, mock_input, + mock_proper_name_validator): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'project-name' + mock_args.use_defaults = False + mock_args.overwrite = True + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + # mocking the validate template call return value + mock_os_path_isdir.return_value = [True, True] + + mock_callable.side_effect = [True, False, False, False, False, False] + + mock_template_context = mock.MagicMock() + + # testing: walk the template directory, creating the templates and directories in the new project as we go + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + mock_input.side_effect = ['test1', 'test1_a', 'test2', 'test3', 'test4', 'test5'] + mock_proper_name_validator.return_value = False, 'foo' + + scaffold_command(args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_called() + + mock_render_path.assert_called() + + # mock the create root directory + mock_makedirs.assert_called_with(mock_cuurent_project_root) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Warning: Dashes in project name "project-name" have been replaced with underscores ' + '"project_name"', po_call_args[0][0][0]) + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[1][0][0]) + self.assertIn('Invalid response: foo', po_call_args[2][0][0]) + self.assertIn('Created:', po_call_args[3][0][0]) + self.assertIn('Created:', po_call_args[4][0][0]) + self.assertIn('Created:', po_call_args[5][0][0]) + self.assertIn('Created:', po_call_args[6][0][0]) + self.assertIn('Created:', po_call_args[7][0][0]) + self.assertEquals('Successfully scaffolded new project "project_name"', po_call_args[8][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('test1_a', mock_log_call_args[4][0][0]) + self.assertIn('test2', mock_log_call_args[4][0][0]) + self.assertIn('test3', mock_log_call_args[4][0][0]) + self.assertIn('test4', mock_log_call_args[4][0][0]) + self.assertIn('test5', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) + self.assertEquals('Loading template: "/foo/baz"', mock_log_call_args[6][0][0]) + self.assertEquals('Rendering template: "/foo/baz"', mock_log_call_args[7][0][0]) + self.assertEquals('Loading template: "/foo/bar/spam"', mock_log_call_args[8][0][0]) + self.assertEquals('Rendering template: "/foo/bar/spam"', mock_log_call_args[9][0][0]) + self.assertEquals('Loading template: "/foo/bar/eggs"', mock_log_call_args[10][0][0]) + self.assertEquals('Rendering template: "/foo/bar/eggs"', mock_log_call_args[11][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.input') + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command_with_no_overwrite(self, _, __, mock_makedirs, mock_os_walk, mock_render_path, + mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, mock_random_color, + mock_logger, mock_input): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'project-name' + mock_args.use_defaults = True + mock_args.overwrite = False + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + # mocking the validate template call return value + mock_os_path_isdir.return_value = [True, True] + + mock_template_context = mock.MagicMock() + + # testing: walk the template directory, creating the templates and directories in the new project as we go + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + mock_input.side_effect = ['y'] + + scaffold_command(args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_called() + + mock_render_path.assert_called() + + # mock the create root directory + mock_makedirs.assert_called_with(mock_cuurent_project_root) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Warning: Dashes in project name "project-name" have been replaced with underscores ' + '"project_name"', po_call_args[0][0][0]) + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[1][0][0]) + self.assertIn('Created:', po_call_args[2][0][0]) + self.assertIn('Created:', po_call_args[3][0][0]) + self.assertIn('Created:', po_call_args[4][0][0]) + self.assertIn('Created:', po_call_args[5][0][0]) + self.assertIn('Created:', po_call_args[6][0][0]) + self.assertEquals('Successfully scaffolded new project "project_name"', po_call_args[7][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) + self.assertEquals('Loading template: "/foo/baz"', mock_log_call_args[6][0][0]) + self.assertEquals('Rendering template: "/foo/baz"', mock_log_call_args[7][0][0]) + self.assertEquals('Loading template: "/foo/bar/spam"', mock_log_call_args[8][0][0]) + self.assertEquals('Rendering template: "/foo/bar/spam"', mock_log_call_args[9][0][0]) + self.assertEquals('Loading template: "/foo/bar/eggs"', mock_log_call_args[10][0][0]) + self.assertEquals('Rendering template: "/foo/bar/eggs"', mock_log_call_args[11][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.exit') + @mock.patch('tethys_apps.cli.scaffold_commands.input') + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command_with_no_overwrite_keyboard_interrupt(self, _, __, mock_makedirs, mock_os_walk, + mock_render_path, + mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, + mock_random_color, + mock_logger, mock_input, mock_exit): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'project-name' + mock_args.use_defaults = True + mock_args.overwrite = False + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + # mocking the validate template call return value + mock_os_path_isdir.return_value = [True, True] + + mock_template_context = mock.MagicMock() + + # testing: walk the template directory, creating the templates and directories in the new project as we go + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + mock_exit.side_effect = SystemExit + + mock_input.side_effect = KeyboardInterrupt + + self.assertRaises(SystemExit, scaffold_command, args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_not_called() + + mock_render_path.assert_not_called() + + # mock the create root directory + mock_makedirs.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Warning: Dashes in project name "project-name" have been replaced with underscores ' + '"project_name"', po_call_args[0][0][0]) + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[1][0][0]) + self.assertIn('Scaffolding cancelled.', po_call_args[2][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.exit') + @mock.patch('tethys_apps.cli.scaffold_commands.input') + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command_with_no_overwrite_cancel(self, _, __, mock_makedirs, mock_os_walk, + mock_render_path, + mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, + mock_random_color, + mock_logger, mock_input, mock_exit): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'project-name' + mock_args.use_defaults = True + mock_args.overwrite = False + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + # mocking the validate template call return value + mock_os_path_isdir.return_value = [True, True] + + mock_template_context = mock.MagicMock() + + # testing: walk the template directory, creating the templates and directories in the new project as we go + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + mock_exit.side_effect = SystemExit + + mock_input.side_effect = ['n'] + + self.assertRaises(SystemExit, scaffold_command, args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_not_called() + + mock_render_path.assert_not_called() + + # mock the create root directory + mock_makedirs.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Warning: Dashes in project name "project-name" have been replaced with underscores ' + '"project_name"', po_call_args[0][0][0]) + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[1][0][0]) + self.assertIn('Scaffolding cancelled.', po_call_args[2][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.exit') + @mock.patch('tethys_apps.cli.scaffold_commands.input') + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command_with_no_overwrite_os_error(self, _, __, mock_makedirs, mock_os_walk, + mock_render_path, + mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, + mock_random_color, + mock_logger, mock_input, mock_exit): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'project-name' + mock_args.use_defaults = True + mock_args.overwrite = False + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + # mocking the validate template call return value + mock_os_path_isdir.return_value = [True, True] + + mock_template_context = mock.MagicMock() + + # testing: walk the template directory, creating the templates and directories in the new project as we go + mock_context.return_value = mock_template_context + + mock_cuurent_project_root = mock.MagicMock() + mock_render_path.return_value = mock_cuurent_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + mock_exit.side_effect = SystemExit + + mock_input.side_effect = ['y'] + + mock_rmt.side_effect = OSError + + self.assertRaises(SystemExit, scaffold_command, args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_called_once() + + mock_render_path.assert_not_called() + + # mock the create root directory + mock_makedirs.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Warning: Dashes in project name "project-name" have been replaced with underscores ' + '"project_name"', po_call_args[0][0][0]) + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[1][0][0]) + self.assertIn('Error: Unable to overwrite', po_call_args[2][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) + + @mock.patch('tethys_apps.cli.scaffold_commands.logging.getLogger') + @mock.patch('tethys_apps.cli.scaffold_commands.get_random_color') + @mock.patch('tethys_apps.cli.scaffold_commands.pretty_output') + @mock.patch('tethys_apps.cli.scaffold_commands.os.path.isdir') + @mock.patch('tethys_apps.cli.scaffold_commands.shutil.rmtree') + @mock.patch('tethys_apps.cli.scaffold_commands.Context') + @mock.patch('tethys_apps.cli.scaffold_commands.render_path') + @mock.patch('tethys_apps.cli.scaffold_commands.os.walk') + @mock.patch('tethys_apps.cli.scaffold_commands.os.makedirs') + @mock.patch('tethys_apps.cli.scaffold_commands.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.cli.scaffold_commands.Template') + def test_scaffold_command_with_unicode_decode_error(self, mock_template, _, mock_makedirs, mock_os_walk, + mock_render_path, + mock_context, mock_rmt, + mock_os_path_isdir, mock_pretty_output, + mock_random_color, + mock_logger): + # mock the input args + mock_args = mock.MagicMock() + + mock_args.extension = '.ext' + mock_args.template = 'template_name' + mock_args.name = 'project-name' + mock_args.use_defaults = True + mock_args.overwrite = True + + # mock the log + mock_log = mock.MagicMock() + + # mock the getlogger from logging + mock_logger.return_value = mock_log + + # mocking the validate template call return value + mock_os_path_isdir.return_value = [True, True] + mock_template.side_effect = UnicodeDecodeError('foo', 'bar'.encode(), 1, 2, 'baz') + + mock_template_context = mock.MagicMock() + + # testing: walk the template directory, creating the templates and directories in the new project as we go + mock_context.return_value = mock_template_context + + mock_current_project_root = mock.MagicMock() + mock_render_path.return_value = mock_current_project_root + + mock_os_walk.return_value = [ + ('/foo', ('bar',), ('baz',)), + ('/foo/bar', (), ('spam', 'eggs')), + ] + + mock_makedirs.return_value = True + + scaffold_command(args=mock_args) + + mock_pretty_output.assert_called() + + mock_random_color.assert_called() + + # check shutil.rmtree call + mock_rmt.assert_called_once() + + mock_render_path.assert_called() + + # mock the create root directory + mock_makedirs.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + + self.assertEquals('Warning: Dashes in project name "project-name" have been replaced with underscores ' + '"project_name"', po_call_args[0][0][0]) + self.assertEquals('Creating new Tethys project named "tethysext-project_name".', po_call_args[1][0][0]) + self.assertIn('Created', po_call_args[2][0][0]) + self.assertIn('Created', po_call_args[3][0][0]) + self.assertIn('Successfully scaffolded new project "project_name"', po_call_args[4][0][0]) + + mock_log_call_args = mock_log.debug.call_args_list + self.assertIn('Command args', mock_log_call_args[0][0][0]) + self.assertIn('APP_PATH', mock_log_call_args[1][0][0]) + self.assertIn('EXTENSION_PATH', mock_log_call_args[2][0][0]) + self.assertIn('Template root directory', mock_log_call_args[3][0][0]) + self.assertIn('Template context', mock_log_call_args[4][0][0]) + self.assertIn('Project root path', mock_log_call_args[5][0][0]) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_scheduler_commands.py b/tests/unit_tests/test_tethys_apps/test_cli/test_scheduler_commands.py new file mode 100644 index 000000000..0fcf9e719 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_scheduler_commands.py @@ -0,0 +1,286 @@ +import unittest +import mock + +from tethys_apps.cli.scheduler_commands import scheduler_create_command, schedulers_list_command, \ + schedulers_remove_command +from django.core.exceptions import ObjectDoesNotExist + + +class SchedulerCommandsTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.cli.scheduler_commands.pretty_output') + @mock.patch('tethys_apps.cli.scheduler_commands.exit') + @mock.patch('tethys_compute.models.Scheduler') + def test_scheduler_create_command(self, mock_scheduler, mock_exit, mock_pretty_output): + """ + Test for scheduler_create_command. + Runs through and saves. + :param mock_scheduler: mock for Scheduler + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_scheduler.objects.filter().first.return_value = False + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, scheduler_create_command, mock_args) + + mock_scheduler.assert_called_with( + name=mock_args.name, + host=mock_args.endpoint, + username=mock_args.username, + password=mock_args.password, + private_key_path=mock_args.private_key_path, + private_key_pass=mock_args.private_key_pass + ) + mock_scheduler().save.assert_called() + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual('Scheduler created successfully!', po_call_args[0][0][0]) + mock_exit.assert_called_with(0) + + @mock.patch('tethys_apps.cli.scheduler_commands.pretty_output') + @mock.patch('tethys_apps.cli.scheduler_commands.exit') + @mock.patch('tethys_compute.models.Scheduler') + def test_scheduler_create_command_existing_scheduler(self, mock_scheduler, mock_exit, mock_pretty_output): + """ + Test for scheduler_create_command. + For when a scheduler already exists. + :param mock_scheduler: mock for Scheduler + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_scheduler.objects.filter().first.return_value = True + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, scheduler_create_command, mock_args) + + mock_scheduler.objects.filter.assert_called() + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertIn('already exists', po_call_args[0][0][0]) + mock_exit.assert_called_with(0) + + @mock.patch('tethys_apps.cli.scheduler_commands.pretty_output') + @mock.patch('tethys_compute.models.Scheduler') + def test_schedulers_list_command(self, mock_scheduler, mock_pretty_output): + """ + Test for schedulers_list_command. + For use with multiple schedulers. + :param mock_scheduler: mock for Scheduler + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_scheduler1 = mock.MagicMock(name='test1') + mock_scheduler1.name = 'test_name1' + mock_scheduler1.host = 'test_host1' + mock_scheduler1.username = 'test_user1' + mock_scheduler1.private_key_path = 'test_path1' + mock_scheduler1.private_key_pass = 'test_private_key_path1' + mock_scheduler2 = mock.MagicMock() + mock_scheduler2.name = 'test_name2' + mock_scheduler2.host = 'test_host2' + mock_scheduler2.username = 'test_user2' + mock_scheduler2.private_key_path = 'test_path2' + mock_scheduler2.private_key_pass = 'test_private_key_path2' + mock_scheduler.objects.all.return_value = [mock_scheduler1, mock_scheduler2] + mock_args = mock.MagicMock() + schedulers_list_command(mock_args) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(3, len(po_call_args)) + self.assertIn('Name', po_call_args[0][0][0]) + self.assertIn('Host', po_call_args[0][0][0]) + self.assertIn('Username', po_call_args[0][0][0]) + self.assertIn('Password', po_call_args[0][0][0]) + self.assertIn('Private Key Path', po_call_args[0][0][0]) + self.assertIn('Private Key Pass', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.scheduler_commands.pretty_output') + @mock.patch('tethys_compute.models.Scheduler') + def test_schedulers_list_command_no_schedulers(self, mock_scheduler, mock_pretty_output): + """ + Test for schedulers_list_command. + For use with no schedulers. + :param mock_scheduler: mock for Scheduler + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_scheduler.objects.all.return_value = [] + mock_args = mock.MagicMock() + schedulers_list_command(mock_args) + + mock_scheduler.objects.all.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('There are no Schedulers registered in Tethys.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.scheduler_commands.pretty_output') + @mock.patch('tethys_apps.cli.scheduler_commands.exit') + @mock.patch('tethys_compute.models.Scheduler') + def test_schedulers_remove_command_force(self, mock_scheduler, mock_exit, mock_pretty_output): + """ + Test for schedulers_remove_command. + Runs through, forcing a delete + :param mock_scheduler: mock for Scheduler + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = True + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, schedulers_remove_command, mock_args) + + mock_scheduler.objects.get.assert_called() + mock_scheduler.objects.get().delete.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Successfully removed Scheduler', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.scheduler_commands.input') + @mock.patch('tethys_apps.cli.scheduler_commands.pretty_output') + @mock.patch('tethys_apps.cli.scheduler_commands.exit') + @mock.patch('tethys_compute.models.Scheduler') + def test_schedulers_remove_command_force_invalid_proceed_char(self, mock_scheduler, mock_exit, mock_pretty_output, + mock_input): + """ + Test for schedulers_remove_command. + Runs through, not forcing a delete, and when prompted to delete, gives an invalid answer + :param mock_scheduler: mock for Scheduler + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :param mock_input: mock for handling raw_input requests + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = False + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + mock_input.side_effect = ['foo', 'N'] + + self.assertRaises(SystemExit, schedulers_remove_command, mock_args) + + mock_scheduler.objects.get.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Aborted. Scheduler not removed.', po_call_args[0][0][0]) + + po_call_args = mock_input.call_args_list + self.assertEqual(2, len(po_call_args)) + self.assertEqual('Are you sure you want to delete this Scheduler? [y/n]: ', po_call_args[0][0][0]) + self.assertEqual('Please enter either "y" or "n": ', po_call_args[1][0][0]) + + @mock.patch('tethys_apps.cli.scheduler_commands.input') + @mock.patch('tethys_apps.cli.scheduler_commands.pretty_output') + @mock.patch('tethys_apps.cli.scheduler_commands.exit') + @mock.patch('tethys_compute.models.Scheduler') + def test_schedulers_remove_command_no_force_proceed(self, mock_scheduler, mock_exit, mock_pretty_output, + mock_input): + """ + Test for schedulers_remove_command. + Runs through, not forcing a delete, and when prompted to delete, gives a valid answer to delete + :param mock_scheduler: mock for Scheduler + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :param mock_input: mock for handling raw_input requests + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = False + mock_input.side_effect = ['Y'] + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, schedulers_remove_command, mock_args) + + mock_scheduler.objects.get.assert_called() + mock_scheduler.objects.get().delete.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Successfully removed Scheduler', po_call_args[0][0][0]) + + po_call_args = mock_input.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Are you sure you want to delete this Scheduler? [y/n]: ', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.scheduler_commands.input') + @mock.patch('tethys_apps.cli.scheduler_commands.pretty_output') + @mock.patch('tethys_apps.cli.scheduler_commands.exit') + @mock.patch('tethys_compute.models.Scheduler') + def test_schedulers_remove_command_no_force_no_proceed(self, mock_scheduler, mock_exit, mock_pretty_output, + mock_input): + """ + Test for schedulers_remove_command. + Runs through, not forcing a delete, and when prompted to delete, gives a valid answer to not delete + :param mock_scheduler: mock for Scheduler + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :param mock_input: mock for handling raw_input requests + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = False + mock_input.side_effect = ['N'] + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, schedulers_remove_command, mock_args) + + mock_scheduler.objects.get.assert_called() + mock_scheduler.objects.get().delete.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Aborted. Scheduler not removed.', po_call_args[0][0][0]) + + po_call_args = mock_input.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Are you sure you want to delete this Scheduler? [y/n]: ', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.scheduler_commands.pretty_output') + @mock.patch('tethys_apps.cli.scheduler_commands.exit') + @mock.patch('tethys_compute.models.Scheduler') + def test_schedulers_remove_command_does_not_exist(self, mock_scheduler, mock_exit, mock_pretty_output): + """ + Test for schedulers_remove_command. + For handling the Scheduler throwing ObjectDoesNotExist + :param mock_scheduler: mock for Scheduler + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_scheduler.objects.get.side_effect = ObjectDoesNotExist + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, schedulers_remove_command, mock_args) + + mock_scheduler.objects.get.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Command aborted.', po_call_args[0][0][0]) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_services_commands.py b/tests/unit_tests/test_tethys_apps/test_cli/test_services_commands.py new file mode 100644 index 000000000..3ee8c06f6 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_services_commands.py @@ -0,0 +1,574 @@ +try: + from StringIO import StringIO +except ImportError: + from io import StringIO # noqa: F401 +import unittest +import mock + +from tethys_apps.cli.services_commands import services_create_persistent_command, services_remove_persistent_command,\ + services_create_spatial_command, services_remove_spatial_command, services_list_command +from django.core.exceptions import ObjectDoesNotExist +from django.db.utils import IntegrityError + + +class ServicesCommandsTest(unittest.TestCase): + """ + Tests for tethys_apps.cli.services_commands + """ + + # Dictionary used in some of the tests + my_dict = {'id': 'Id_foo', 'name': 'Name_foo', 'host': 'Host_foo', 'port': 'Port_foo', 'endpoint': 'EndPoint_foo', + 'public_endpoint': 'PublicEndPoint_bar', 'apikey': 'APIKey_foo'} + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_services.models.PersistentStoreService') + def test_services_create_persistent_command(self, mock_service, mock_pretty_output): + """ + Test for services_create_persistent_command. + For running the test without any errors or problems. + :param mock_service: mock for PersistentStoreService + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + services_create_persistent_command(mock_args) + + mock_service.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Successfully created new Persistent Store Service!', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_services.models.PersistentStoreService') + def test_services_create_persistent_command_exception_indexerror(self, mock_service, mock_pretty_output): + """ + Test for services_create_persistent_command. + For running the test with an IndexError exception thrown. + :param mock_service: mock for PersistentStoreService + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_service.side_effect = IndexError + services_create_persistent_command(mock_args) + + mock_service.assert_called() + mock_service.objects.get().save.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('The connection argument (-c) must be of the form', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_services.models.PersistentStoreService') + def test_services_create_persistent_command_exception_integrityerror(self, mock_service, mock_pretty_output): + """ + Test for services_create_persistent_command. + For running the test with an IntegrityError exception thrown. + :param mock_service: mock for PersistentStoreService + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_service.side_effect = IntegrityError + services_create_persistent_command(mock_args) + + mock_service.assert_called() + mock_service.objects.get().save.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Persistent Store Service with name', po_call_args[0][0][0]) + self.assertIn('already exists. Command aborted.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_apps.cli.services_commands.exit') + @mock.patch('tethys_services.models.PersistentStoreService') + def test_services_remove_persistent_command_Exceptions(self, mock_service, mock_exit, mock_pretty_output): + """ + Test for services_remove_persistent_command + Test for handling all exceptions thrown by the function. + :param mock_service: mock for PersistentStoreService + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = True + mock_service.objects.get.side_effect = [ValueError, ObjectDoesNotExist] + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, services_remove_persistent_command, mock_args) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('A Persistent Store Service with ID/Name', po_call_args[0][0][0]) + self.assertIn('does not exist', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_apps.cli.services_commands.exit') + @mock.patch('tethys_services.models.PersistentStoreService') + def test_services_remove_persistent_command_force(self, mock_service, mock_exit, mock_pretty_output): + """ + Test for services_remove_persistent_command + Test for forcing a delete of the service + :param mock_service: mock for PersistentStoreService + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = True + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, services_remove_persistent_command, mock_args) + + mock_service.objects.get().delete.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Successfully removed Persistent Store Service', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.input') + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_apps.cli.services_commands.exit') + @mock.patch('tethys_services.models.PersistentStoreService') + def test_services_remove_persistent_command_no_proceed_invalid_char(self, mock_service, mock_exit, + mock_pretty_output, mock_input): + """ + Test for services_remove_persistent_command + Handles answering the prompt to delete with invalid characters, and answering no. + :param mock_service: mock for PersistentStoreService + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :param mock_input: mock for handling raw_input requests + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = False + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + mock_input.side_effect = ['foo', 'N'] + + self.assertRaises(SystemExit, services_remove_persistent_command, mock_args) + + mock_service.objects.get().delete.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Aborted. Persistent Store Service not removed.', po_call_args[0][0][0]) + + po_call_args = mock_input.call_args_list + self.assertEqual(2, len(po_call_args)) + self.assertEqual('Are you sure you want to delete this Persistent Store Service? [y/n]: ', + po_call_args[0][0][0]) + self.assertEqual('Please enter either "y" or "n": ', po_call_args[1][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.input') + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_apps.cli.services_commands.exit') + @mock.patch('tethys_services.models.PersistentStoreService') + def test_services_remove_persistent_command_proceed(self, mock_service, mock_exit, mock_pretty_output, mock_input): + """ + Test for services_remove_persistent_command + Handles answering the prompt to delete with invalid characters by answering yes + :param mock_service: mock for PersistentStoreService + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :param mock_input: mock for handling raw_input requests + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = False + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + mock_input.side_effect = ['y'] + + self.assertRaises(SystemExit, services_remove_persistent_command, mock_args) + + mock_service.objects.get().delete.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Successfully removed Persistent Store Service', po_call_args[0][0][0]) + + po_call_args = mock_input.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Are you sure you want to delete this Persistent Store Service? [y/n]: ', + po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_services.models.SpatialDatasetService') + def test_services_create_spatial_command_IndexError(self, mock_service, mock_pretty_output): + """ + Test for services_create_spatial_command + Handles an IndexError exception + :param mock_service: mock for SpatialDatasetService + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_args.connection = 'IndexError:9876@IndexError' # No 'http' or '://' + + services_create_spatial_command(mock_args) + + mock_service.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('The connection argument (-c) must be of the form', po_call_args[0][0][0]) + self.assertIn('":@//:".', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_services.models.SpatialDatasetService') + def test_services_create_spatial_command_FormatError(self, mock_service, mock_pretty_output): + """ + Test for services_create_spatial_command + Handles an FormatError exception + :param mock_service: mock for SpatialDatasetService + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_args.connection = 'foo:pass@http:://foo:1234' + mock_args.public_endpoint = 'foo@foo:foo' # No 'http' or '://' + + services_create_spatial_command(mock_args) + + mock_service.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('The public_endpoint argument (-p) must be of the form ', po_call_args[0][0][0]) + self.assertIn('"//:".', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_services.models.SpatialDatasetService') + def test_services_create_spatial_command_IntegrityError(self, mock_service, mock_pretty_output): + """ + Test for services_create_spatial_command + Handles an IntegrityError exception + :param mock_service: mock for SpatialDatasetService + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_args.connection = 'foo:pass@http:://foo:1234' + mock_args.public_endpoint = 'http://foo:1234' + mock_service.side_effect = IntegrityError + + services_create_spatial_command(mock_args) + + mock_service.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Spatial Dataset Service with name ', po_call_args[0][0][0]) + self.assertIn('already exists. Command aborted.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_services.models.SpatialDatasetService') + def test_services_create_spatial_command(self, mock_service, mock_pretty_output): + """ + Test for services_create_spatial_command + For going through the function and saving + :param mock_service: mock for SpatialDatasetService + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_args.connection = 'foo:pass@http:://foo:1234' + mock_args.public_endpoint = 'http://foo:1234' + mock_service.return_value = mock.MagicMock() + + services_create_spatial_command(mock_args) + + mock_service.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Successfully created new Spatial Dataset Service!', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_apps.cli.services_commands.exit') + @mock.patch('tethys_services.models.SpatialDatasetService') + def test_services_remove_spatial_command_Exceptions(self, mock_service, mock_exit, mock_pretty_output): + """ + Test for services_remove_spatial_command + Handles testing all of the exceptions thrown + :param mock_service: mock for SpatialDatasetService + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_service.objects.get.side_effect = [ValueError, ObjectDoesNotExist] + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, services_remove_spatial_command, mock_args) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('A Spatial Dataset Service with ID/Name', po_call_args[0][0][0]) + self.assertIn('does not exist.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_apps.cli.services_commands.exit') + @mock.patch('tethys_services.models.SpatialDatasetService') + def test_services_remove_spatial_command_force(self, mock_service, mock_exit, mock_pretty_output): + """ + Test for services_remove_spatial_command + For when a delete is forced + :param mock_service: mock for SpatialDatasetService + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = True + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, services_remove_spatial_command, mock_args) + + mock_service.objects.get().delete.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Successfully removed Spatial Dataset Service', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.input') + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_apps.cli.services_commands.exit') + @mock.patch('tethys_services.models.SpatialDatasetService') + def test_services_remove_spatial_command_no_proceed_invalid_char(self, mock_service, mock_exit, + mock_pretty_output, mock_input): + """ + Test for services_remove_spatial_command + For when deleting is not forced, and when prompted, giving an invalid answer, then no delete + :param mock_service: mock for SpatialDatasetService + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :param mock_input: mock for handling raw_input requests + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = False + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + mock_input.side_effect = ['foo', 'N'] + + self.assertRaises(SystemExit, services_remove_spatial_command, mock_args) + + mock_service.objects.get().delete.assert_not_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Aborted. Spatial Dataset Service not removed.', po_call_args[0][0][0]) + + po_call_args = mock_input.call_args_list + self.assertEqual(2, len(po_call_args)) + self.assertEqual('Are you sure you want to delete this Persistent Store Service? [y/n]: ', + po_call_args[0][0][0]) + self.assertEqual('Please enter either "y" or "n": ', po_call_args[1][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.input') + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_apps.cli.services_commands.exit') + @mock.patch('tethys_services.models.SpatialDatasetService') + def test_services_remove_spatial_command_proceed(self, mock_service, mock_exit, mock_pretty_output, mock_input): + """ + Test for services_remove_spatial_command + For when deleting is not forced, and when prompted, giving a valid answer to delete + :param mock_service: mock for SpatialDatasetService + :param mock_exit: mock for handling exit() code in function + :param mock_pretty_output: mock for pretty_output text + :param mock_input: mock for handling raw_input requests + :return: + """ + mock_args = mock.MagicMock() + mock_args.force = False + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + mock_input.side_effect = ['y'] + + self.assertRaises(SystemExit, services_remove_spatial_command, mock_args) + + mock_service.objects.get().delete.assert_called() + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Successfully removed Spatial Dataset Service', po_call_args[0][0][0]) + + po_call_args = mock_input.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Are you sure you want to delete this Persistent Store Service? [y/n]: ', + po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.print') + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_services.models.PersistentStoreService') + @mock.patch('tethys_services.models.SpatialDatasetService') + @mock.patch('tethys_apps.cli.services_commands.model_to_dict') + def test_services_list_command_not_spatial_not_persistent(self, mock_mtd, mock_spatial, mock_persistent, + mock_pretty_output, mock_print): + """ + Test for services_list_command + Both spatial and persistent are not set, so both are processed + :param mock_mtd: mock for model_to_dict to return a dictionary + :param mock_spatial: mock for SpatialDatasetService + :param mock_persistent: mock for PersistentStoreService + :param mock_pretty_output: mock for pretty_output text + :param mock_stdout: mock for text written with print statements + :return: + """ + mock_mtd.return_value = self.my_dict + mock_args = mock.MagicMock() + mock_args.spatial = False + mock_args.persistent = False + mock_spatial.objects.order_by('id').all.return_value = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock()] + mock_persistent.objects.order_by('id').all.return_value = [mock.MagicMock(), mock.MagicMock()] + + services_list_command(mock_args) + + # Check expected pretty_output + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(4, len(po_call_args)) + self.assertIn('Persistent Store Services:', po_call_args[0][0][0]) + self.assertIn('ID', po_call_args[1][0][0]) + self.assertIn('Name', po_call_args[1][0][0]) + self.assertIn('Host', po_call_args[1][0][0]) + self.assertIn('Port', po_call_args[1][0][0]) + self.assertNotIn('Endpoint', po_call_args[1][0][0]) + self.assertNotIn('Public Endpoint', po_call_args[1][0][0]) + self.assertNotIn('API Key', po_call_args[1][0][0]) + self.assertIn('Spatial Dataset Services:', po_call_args[2][0][0]) + self.assertIn('ID', po_call_args[3][0][0]) + self.assertIn('Name', po_call_args[3][0][0]) + self.assertNotIn('Host', po_call_args[3][0][0]) + self.assertNotIn('Port', po_call_args[3][0][0]) + self.assertIn('Endpoint', po_call_args[3][0][0]) + self.assertIn('Public Endpoint', po_call_args[3][0][0]) + self.assertIn('API Key', po_call_args[3][0][0]) + + # Check text written with Python's print + rts_call_args = mock_print.call_args_list + self.assertIn(self.my_dict['id'], rts_call_args[0][0][0]) + self.assertIn(self.my_dict['name'], rts_call_args[0][0][0]) + self.assertIn(self.my_dict['host'], rts_call_args[0][0][0]) + self.assertIn(self.my_dict['port'], rts_call_args[0][0][0]) + self.assertIn(self.my_dict['id'], rts_call_args[4][0][0]) + self.assertIn(self.my_dict['name'], rts_call_args[4][0][0]) + self.assertNotIn(self.my_dict['host'], rts_call_args[4][0][0]) + self.assertNotIn(self.my_dict['port'], rts_call_args[4][0][0]) + self.assertIn(self.my_dict['endpoint'], rts_call_args[4][0][0]) + self.assertIn(self.my_dict['public_endpoint'], rts_call_args[4][0][0]) + self.assertIn(self.my_dict['apikey'], rts_call_args[4][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.print') + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_services.models.SpatialDatasetService') + @mock.patch('tethys_apps.cli.services_commands.model_to_dict') + def test_services_list_command_spatial(self, mock_mtd, mock_spatial, mock_pretty_output, mock_print): + """ + Test for services_list_command + Only spatial is set + :param mock_mtd: mock for model_to_dict to return a dictionary + :param mock_spatial: mock for SpatialDatasetService + :param mock_pretty_output: mock for pretty_output text + :param mock_stdout: mock for text written with print statements + :return: + """ + mock_mtd.return_value = self.my_dict + mock_args = mock.MagicMock() + mock_args.spatial = True + mock_args.persistent = False + mock_spatial.objects.order_by('id').all.return_value = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock()] + + services_list_command(mock_args) + + # Check expected pretty_output + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(2, len(po_call_args)) + self.assertIn('Spatial Dataset Services:', po_call_args[0][0][0]) + self.assertIn('ID', po_call_args[1][0][0]) + self.assertIn('Name', po_call_args[1][0][0]) + self.assertNotIn('Host', po_call_args[1][0][0]) + self.assertNotIn('Port', po_call_args[1][0][0]) + self.assertIn('Endpoint', po_call_args[1][0][0]) + self.assertIn('Public Endpoint', po_call_args[1][0][0]) + self.assertIn('API Key', po_call_args[1][0][0]) + + # Check text written with Python's print + rts_call_args = mock_print.call_args_list + + self.assertIn(self.my_dict['id'], rts_call_args[2][0][0]) + self.assertIn(self.my_dict['name'], rts_call_args[2][0][0]) + self.assertNotIn(self.my_dict['host'], rts_call_args[2][0][0]) + self.assertNotIn(self.my_dict['port'], rts_call_args[2][0][0]) + self.assertIn(self.my_dict['endpoint'], rts_call_args[2][0][0]) + self.assertIn(self.my_dict['public_endpoint'], rts_call_args[2][0][0]) + self.assertIn(self.my_dict['apikey'], rts_call_args[2][0][0]) + + @mock.patch('tethys_apps.cli.services_commands.print') + @mock.patch('tethys_apps.cli.services_commands.pretty_output') + @mock.patch('tethys_services.models.PersistentStoreService') + @mock.patch('tethys_apps.cli.services_commands.model_to_dict') + def test_services_list_command_persistent(self, mock_mtd, mock_persistent, mock_pretty_output, mock_print): + """ + Test for services_list_command + Only persistent is set + :param mock_mtd: mock for model_to_dict to return a dictionary + :param mock_persistent: mock for PersistentStoreService + :param mock_pretty_output: mock for pretty_output text + :param mock_stdout: mock for text written with print statements + :return: + """ + mock_mtd.return_value = self.my_dict + mock_args = mock.MagicMock() + mock_args.spatial = False + mock_args.persistent = True + mock_persistent.objects.order_by('id').all.return_value = [mock.MagicMock(), mock.MagicMock()] + + services_list_command(mock_args) + + # Check expected pretty_output + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(2, len(po_call_args)) + self.assertIn('Persistent Store Services:', po_call_args[0][0][0]) + self.assertIn('ID', po_call_args[1][0][0]) + self.assertIn('Name', po_call_args[1][0][0]) + self.assertIn('Host', po_call_args[1][0][0]) + self.assertIn('Port', po_call_args[1][0][0]) + self.assertNotIn('Endpoint', po_call_args[1][0][0]) + self.assertNotIn('Public Endpoint', po_call_args[1][0][0]) + self.assertNotIn('API Key', po_call_args[1][0][0]) + + # Check text written with Python's print + rts_call_args = mock_print.call_args_list + + self.assertIn(self.my_dict['id'], rts_call_args[1][0][0]) + self.assertIn(self.my_dict['name'], rts_call_args[1][0][0]) + self.assertIn(self.my_dict['host'], rts_call_args[1][0][0]) + self.assertIn(self.my_dict['port'], rts_call_args[1][0][0]) + self.assertNotIn(self.my_dict['endpoint'], rts_call_args[1][0][0]) + self.assertNotIn(self.my_dict['public_endpoint'], rts_call_args[1][0][0]) + self.assertNotIn(self.my_dict['apikey'], rts_call_args[1][0][0]) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_syncstores_command.py b/tests/unit_tests/test_tethys_apps/test_cli/test_syncstores_command.py new file mode 100644 index 000000000..03dfbd680 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_syncstores_command.py @@ -0,0 +1,119 @@ +import unittest +import mock + +from tethys_apps.cli.syncstores_command import syncstores_command + + +class SyncstoresCommandTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.cli.syncstores_command.subprocess.call') + @mock.patch('tethys_apps.cli.syncstores_command.get_manage_path') + def test_syncstores_command_no_args(self, mock_get_manage_path, mock_subprocess_call): + mock_args = mock.MagicMock() + mock_args.refresh = False + mock_args.firsttime = False + mock_args.database = False + mock_args.app = False + mock_get_manage_path.return_value = '/foo/manage.py' + mock_subprocess_call.return_value = True + + syncstores_command(mock_args) + + mock_get_manage_path.assert_called_once() + mock_subprocess_call.assert_called_once() + mock_subprocess_call.assert_called_with(['python', '/foo/manage.py', 'syncstores']) + + @mock.patch('tethys_apps.cli.syncstores_command.subprocess.call') + @mock.patch('tethys_apps.cli.syncstores_command.get_manage_path') + def test_syncstores_command_args_no_refresh(self, mock_get_manage_path, mock_subprocess_call): + mock_args = mock.MagicMock() + mock_args.refresh = False + mock_args.firsttime = True + mock_args.database = 'foo_db' + mock_args.app = ['foo_app'] + mock_get_manage_path.return_value = '/foo/manage.py' + mock_subprocess_call.return_value = True + + syncstores_command(mock_args) + + mock_get_manage_path.assert_called_once() + mock_subprocess_call.assert_called_once() + mock_subprocess_call.assert_called_with(['python', '/foo/manage.py', 'syncstores', '-f', '-d', 'foo_db', + 'foo_app']) + + @mock.patch('tethys_apps.cli.syncstores_command.subprocess.call') + @mock.patch('tethys_apps.cli.syncstores_command.get_manage_path') + def test_syncstores_command_no_args_assert(self, mock_get_manage_path, mock_subprocess_call): + mock_args = mock.MagicMock() + mock_args.refresh = False + mock_args.firsttime = False + mock_args.database = False + mock_args.app = False + mock_get_manage_path.return_value = '/foo/manage.py' + mock_subprocess_call.side_effect = KeyboardInterrupt + + syncstores_command(mock_args) + + mock_get_manage_path.assert_called_once() + mock_subprocess_call.assert_called_once() + mock_subprocess_call.assert_called_with(['python', '/foo/manage.py', 'syncstores']) + + @mock.patch('tethys_apps.cli.syncstores_command.input') + @mock.patch('tethys_apps.cli.syncstores_command.subprocess.call') + @mock.patch('tethys_apps.cli.syncstores_command.get_manage_path') + def test_syncstores_command_refresh_continue(self, mock_get_manage_path, mock_subprocess_call, mock_input): + mock_args = mock.MagicMock() + mock_args.refresh = True + mock_args.firsttime = False + mock_args.database = False + mock_args.app = ['foo_app'] + mock_get_manage_path.return_value = '/foo/manage.py' + mock_subprocess_call.return_value = True + mock_input.side_effect = ['foo', 'y'] + + syncstores_command(mock_args) + + mock_get_manage_path.assert_called_once() + mock_subprocess_call.assert_called_once() + mock_subprocess_call.assert_called_with(['python', '/foo/manage.py', 'syncstores', '-r', 'foo_app']) + po_call_args = mock_input.call_args_list + self.assertEqual(2, len(po_call_args)) + self.assertIn('WARNING', po_call_args[0][0][0]) + self.assertIn('Invalid option. Do you wish to continue?', po_call_args[1][0][0]) + + @mock.patch('tethys_apps.cli.syncstores_command.print') + @mock.patch('tethys_apps.cli.syncstores_command.exit') + @mock.patch('tethys_apps.cli.syncstores_command.input') + @mock.patch('tethys_apps.cli.syncstores_command.subprocess.call') + @mock.patch('tethys_apps.cli.syncstores_command.get_manage_path') + def test_syncstores_command_refresh_exit(self, mock_get_manage_path, mock_subprocess_call, mock_input, mock_exit, + mock_print): + mock_args = mock.MagicMock() + mock_args.refresh = True + mock_args.firsttime = False + mock_args.database = False + mock_args.app = ['foo_app'] + mock_get_manage_path.return_value = '/foo/manage.py' + mock_subprocess_call.return_value = True + mock_input.side_effect = ['n'] + mock_exit.side_effect = SystemExit + + self.assertRaises(SystemExit, syncstores_command, mock_args) + mock_exit.assert_called_with(0) + + mock_get_manage_path.assert_called_once() + mock_subprocess_call.assert_not_called() + + po_call_args = mock_input.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('WARNING', po_call_args[0][0][0]) + + # Check print statement + rts_call_args = mock_print.call_args_list + self.assertIn('Operation cancelled by user.', rts_call_args[0][0][0]) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_test_command.py b/tests/unit_tests/test_tethys_apps/test_cli/test_test_command.py new file mode 100644 index 000000000..32f1b2633 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_test_command.py @@ -0,0 +1,212 @@ +import unittest +import mock + +from tethys_apps.cli.test_command import test_command + + +class TestCommandTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.cli.test_command.run_process') + @mock.patch('tethys_apps.cli.test_command.os.path.join') + @mock.patch('tethys_apps.cli.test_command.get_manage_path') + def test_test_command_no_coverage_file(self, mock_get_manage_path, mock_join, mock_run_process): + mock_args = mock.MagicMock() + mock_args.coverage = False + mock_args.coverage_html = False + mock_args.file = 'foo_file' + mock_args.unit = False + mock_args.gui = False + mock_get_manage_path.return_value = '/foo/manage.py' + mock_join.return_value = '/foo' + mock_run_process.return_value = 0 + + self.assertRaises(SystemExit, test_command, mock_args) + mock_get_manage_path.assert_called() + mock_join.assert_called() + mock_run_process.assert_called_once() + mock_run_process.assert_called_with(['python', '/foo/manage.py', 'test', 'foo_file']) + + @mock.patch('tethys_apps.cli.test_command.run_process') + @mock.patch('tethys_apps.cli.test_command.os.path.join') + @mock.patch('tethys_apps.cli.test_command.get_manage_path') + def test_test_command_coverage_unit(self, mock_get_manage_path, mock_join, mock_run_process): + mock_args = mock.MagicMock() + mock_args.coverage = True + mock_args.coverage_html = False + mock_args.file = None + mock_args.unit = True + mock_args.gui = False + mock_get_manage_path.return_value = '/foo/manage.py' + mock_join.return_value = '/foo' + mock_run_process.return_value = 0 + + self.assertRaises(SystemExit, test_command, mock_args) + mock_get_manage_path.assert_called() + mock_join.assert_called() + mock_run_process.assert_called() + mock_run_process.assert_any_call(['coverage', 'run', '--rcfile=/foo', '/foo/manage.py', 'test', '/foo']) + mock_run_process.assert_called_with(['coverage', 'report', '--rcfile=/foo']) + + @mock.patch('tethys_apps.cli.test_command.run_process') + @mock.patch('tethys_apps.cli.test_command.os.path.join') + @mock.patch('tethys_apps.cli.test_command.get_manage_path') + def test_test_command_coverage_unit_file_app_package(self, mock_get_manage_path, mock_join, mock_run_process): + mock_args = mock.MagicMock() + mock_args.coverage = True + mock_args.coverage_html = False + mock_args.file = '/foo/tethys_apps.tethysapp.foo' + mock_args.unit = True + mock_args.gui = False + mock_get_manage_path.return_value = '/foo/manage.py' + mock_join.return_value = '/foo' + mock_run_process.return_value = 0 + + self.assertRaises(SystemExit, test_command, mock_args) + mock_get_manage_path.assert_called() + mock_join.assert_called() + mock_run_process.assert_called() + mock_run_process.assert_any_call(['coverage', 'run', '--source=tethys_apps.tethysapp.foo,tethysapp.foo', + '/foo/manage.py', 'test', '/foo/tethys_apps.tethysapp.foo']) + mock_run_process.assert_called_with(['coverage', 'report']) + + @mock.patch('tethys_apps.cli.test_command.run_process') + @mock.patch('tethys_apps.cli.test_command.os.path.join') + @mock.patch('tethys_apps.cli.test_command.get_manage_path') + def test_test_command_coverage_html_unit_file_app_package(self, mock_get_manage_path, mock_join, mock_run_process): + mock_args = mock.MagicMock() + mock_args.coverage = False + mock_args.coverage_html = True + mock_args.file = '/foo/tethys_apps.tethysapp.foo' + mock_args.unit = True + mock_args.gui = False + mock_get_manage_path.return_value = '/foo/manage.py' + mock_join.return_value = '/foo' + mock_run_process.return_value = 0 + + self.assertRaises(SystemExit, test_command, mock_args) + mock_get_manage_path.assert_called() + mock_join.assert_called() + mock_run_process.assert_called() + mock_run_process.assert_any_call(['coverage', 'run', '--source=tethys_apps.tethysapp.foo,tethysapp.foo', + '/foo/manage.py', 'test', '/foo/tethys_apps.tethysapp.foo']) + mock_run_process.assert_any_call(['coverage', 'html', '--directory=/foo']) + mock_run_process.assert_called_with(['open', '/foo']) + + @mock.patch('tethys_apps.cli.test_command.run_process') + @mock.patch('tethys_apps.cli.test_command.os.path.join') + @mock.patch('tethys_apps.cli.test_command.get_manage_path') + def test_test_command_coverage_unit_file_extension_package(self, mock_get_manage_path, mock_join, mock_run_process): + mock_args = mock.MagicMock() + mock_args.coverage = True + mock_args.coverage_html = False + mock_args.file = '/foo/tethysext.foo' + mock_args.unit = True + mock_args.gui = False + mock_get_manage_path.return_value = '/foo/manage.py' + mock_join.return_value = '/foo' + mock_run_process.return_value = 0 + + self.assertRaises(SystemExit, test_command, mock_args) + mock_get_manage_path.assert_called() + mock_join.assert_called() + mock_run_process.assert_called() + mock_run_process.assert_any_call(['coverage', 'run', '--source=tethysext.foo,tethysext.foo', '/foo/manage.py', + 'test', '/foo/tethysext.foo']) + mock_run_process.assert_called_with(['coverage', 'report']) + + @mock.patch('tethys_apps.cli.test_command.run_process') + @mock.patch('tethys_apps.cli.test_command.os.path.join') + @mock.patch('tethys_apps.cli.test_command.get_manage_path') + def test_test_command_coverage_html_gui_file(self, mock_get_manage_path, mock_join, mock_run_process): + mock_args = mock.MagicMock() + mock_args.coverage = False + mock_args.coverage_html = True + mock_args.file = 'foo_file' + mock_args.unit = False + mock_args.gui = True + mock_get_manage_path.return_value = '/foo/manage.py' + mock_join.return_value = '/foo/bar' + mock_run_process.return_value = 0 + + self.assertRaises(SystemExit, test_command, mock_args) + mock_get_manage_path.assert_called() + mock_join.assert_called() + mock_run_process.assert_called() + mock_run_process.assert_any_call(['coverage', 'run', '--rcfile=/foo/bar', '/foo/manage.py', 'test', 'foo_file']) + mock_run_process.assert_any_call(['coverage', 'html', '--rcfile=/foo/bar']) + mock_run_process.assert_called_with(['open', '/foo/bar']) + + @mock.patch('tethys_apps.cli.test_command.webbrowser.open_new_tab') + @mock.patch('tethys_apps.cli.test_command.run_process') + @mock.patch('tethys_apps.cli.test_command.os.path.join') + @mock.patch('tethys_apps.cli.test_command.get_manage_path') + def test_test_command_coverage_html_gui_file_exception(self, mock_get_manage_path, mock_join, mock_run_process, + mock_open_new_tab): + mock_args = mock.MagicMock() + mock_args.coverage = False + mock_args.coverage_html = True + mock_args.file = 'foo_file' + mock_args.unit = False + mock_args.gui = True + mock_get_manage_path.return_value = '/foo/manage.py' + mock_join.side_effect = ['/foo/bar', '/foo/bar2', '/foo/bar3', '/foo/bar4', '/foo/bar5', '/foo/bar6'] + mock_run_process.side_effect = [0, 0, 1] + mock_open_new_tab.return_value = 1 + + self.assertRaises(SystemExit, test_command, mock_args) + mock_get_manage_path.assert_called() + mock_join.assert_called() + mock_run_process.assert_called() + mock_run_process.assert_any_call(['coverage', 'run', '--rcfile=/foo/bar2', '/foo/manage.py', + 'test', 'foo_file']) + mock_run_process.assert_any_call(['coverage', 'html', '--rcfile=/foo/bar2']) + mock_run_process.assert_called_with(['open', '/foo/bar3']) + mock_open_new_tab.assert_called_once() + mock_open_new_tab.assert_called_with('/foo/bar4') + + @mock.patch('tethys_apps.cli.test_command.run_process') + @mock.patch('tethys_apps.cli.test_command.os.path.join') + @mock.patch('tethys_apps.cli.test_command.get_manage_path') + def test_test_command_unit_no_file(self, mock_get_manage_path, mock_join, mock_run_process): + mock_args = mock.MagicMock() + mock_args.coverage = False + mock_args.coverage_html = False + mock_args.file = None + mock_args.unit = True + mock_args.gui = False + mock_get_manage_path.return_value = '/foo/manage.py' + mock_join.return_value = '/foo' + mock_run_process.return_value = 0 + + self.assertRaises(SystemExit, test_command, mock_args) + + mock_get_manage_path.assert_called() + mock_join.assert_called() + mock_run_process.assert_called_once() + mock_run_process.assert_called_with(['python', '/foo/manage.py', 'test', '/foo']) + + @mock.patch('tethys_apps.cli.test_command.run_process') + @mock.patch('tethys_apps.cli.test_command.os.path.join') + @mock.patch('tethys_apps.cli.test_command.get_manage_path') + def test_test_command_gui_no_file(self, mock_get_manage_path, mock_join, mock_run_process): + mock_args = mock.MagicMock() + mock_args.coverage = False + mock_args.coverage_html = False + mock_args.file = None + mock_args.unit = False + mock_args.gui = True + mock_get_manage_path.return_value = '/foo/manage.py' + mock_join.return_value = '/foo' + mock_run_process.return_value = 0 + + self.assertRaises(SystemExit, test_command, mock_args) + mock_get_manage_path.assert_called() + mock_join.assert_called() + mock_run_process.assert_called_once() + mock_run_process.assert_called_with(['python', '/foo/manage.py', 'test', '/foo']) diff --git a/tests/unit_tests/test_tethys_apps/test_cli/test_uninstall_command.py b/tests/unit_tests/test_tethys_apps/test_cli/test_uninstall_command.py new file mode 100644 index 000000000..b2312b2bc --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_cli/test_uninstall_command.py @@ -0,0 +1,58 @@ +import unittest +import mock + +from tethys_apps.cli.uninstall_command import uninstall_command + + +class UninstallCommandTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.cli.uninstall_command.subprocess.call') + @mock.patch('tethys_apps.cli.uninstall_command.get_manage_path') + def test_uninstall_command(self, mock_get_manage_path, mock_subprocess_call): + mock_args = mock.MagicMock() + mock_args.is_extension = True + mock_args.app_or_extension = 'foo_ext' + mock_get_manage_path.return_value = '/foo/manage.py' + mock_subprocess_call.return_value = True + + uninstall_command(mock_args) + + mock_get_manage_path.assert_called_once() + mock_subprocess_call.assert_called_once() + mock_subprocess_call.assert_called_with(['python', '/foo/manage.py', 'tethys_app_uninstall', 'foo_ext', '-e']) + + @mock.patch('tethys_apps.cli.uninstall_command.subprocess.call') + @mock.patch('tethys_apps.cli.uninstall_command.get_manage_path') + def test_uninstall_command_no_extension(self, mock_get_manage_path, mock_subprocess_call): + mock_args = mock.MagicMock() + mock_args.is_extension = False + mock_args.app_or_extension = 'foo_app' + mock_get_manage_path.return_value = '/foo/manage.py' + mock_subprocess_call.return_value = True + + uninstall_command(mock_args) + + mock_get_manage_path.assert_called_once() + mock_subprocess_call.assert_called_once() + mock_subprocess_call.assert_called_with(['python', '/foo/manage.py', 'tethys_app_uninstall', 'foo_app']) + + @mock.patch('tethys_apps.cli.uninstall_command.subprocess.call') + @mock.patch('tethys_apps.cli.uninstall_command.get_manage_path') + def test_uninstall_command_assert(self, mock_get_manage_path, mock_subprocess_call): + mock_args = mock.MagicMock() + mock_args.is_extension = True + mock_args.app_or_extension = 'foo_ext' + mock_get_manage_path.return_value = '/foo/manage.py' + mock_subprocess_call.side_effect = KeyboardInterrupt + + uninstall_command(mock_args) + + mock_get_manage_path.assert_called_once() + mock_subprocess_call.assert_called_once() + mock_subprocess_call.assert_called_with(['python', '/foo/manage.py', 'tethys_app_uninstall', 'foo_ext', '-e']) diff --git a/tests/unit_tests/test_tethys_apps/test_context_processors.py b/tests/unit_tests/test_tethys_apps/test_context_processors.py new file mode 100644 index 000000000..3e77a0c11 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_context_processors.py @@ -0,0 +1,83 @@ +import unittest +import mock + +from django.db import models +from tethys_apps.context_processors import tethys_apps_context +from tethys_apps.models import TethysApp +from tethys_compute.utilities import ListField + + +class ContextProcessorsTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.context_processors.get_active_app') + def test_tethys_apps_context(self, mock_get_active_app): + mock_args = mock.MagicMock() + app = TethysApp() + app.id = 'foo.id' + app.name = models.CharField(max_length=200, default='') + app.name.value = 'foo.name' + app.icon = models.CharField(max_length=200, default='') + app.icon.value = 'foo.icon' + app.color = models.CharField(max_length=10, default='') + app.color.value = '#foobar' + app.tags = models.CharField(max_length=200, blank=True, default='') + app.tags.value = 'tags' + app.description = models.TextField(max_length=1000, blank=True, default='') + app.description.value = 'foo.description' + app.enable_feedback = models.BooleanField(default=True) + app.enable_feedback.value = False + app.feedback_emails = ListField(default='', blank=True) + mock_get_active_app.return_value = app + + context = tethys_apps_context(mock_args) + + mock_get_active_app.assert_called_once() + self.assertEqual('foo.id', context['tethys_app']['id']) + self.assertEqual('foo.name', context['tethys_app']['name'].value) + self.assertEqual('foo.icon', context['tethys_app']['icon'].value) + self.assertEqual('#foobar', context['tethys_app']['color'].value) + self.assertEqual('tags', context['tethys_app']['tags'].value) + self.assertEqual('foo.description', context['tethys_app']['description'].value) + self.assertFalse('enable_feedback' in context['tethys_app']) + self.assertFalse('feedback_emails' in context['tethys_app']) + + @mock.patch('tethys_apps.context_processors.get_active_app') + def test_tethys_apps_context_feedback(self, mock_get_active_app): + mock_args = mock.MagicMock() + app = TethysApp() + app.id = 'foo.id' + app.name = models.CharField(max_length=200, default='') + app.name.value = 'foo.name' + app.icon = models.CharField(max_length=200, default='') + app.icon.value = 'foo.icon' + app.color = models.CharField(max_length=10, default='') + app.color.value = '#foobar' + app.tags = models.CharField(max_length=200, blank=True, default='') + app.tags.value = 'tags' + app.description = models.TextField(max_length=1000, blank=True, default='') + app.description.value = 'foo.description' + app.enable_feedback = models.BooleanField(default=True) + app.enable_feedback.value = True + app.feedback_emails = ListField(default='', blank=True) + app.feedback_emails.append('foo.feedback') + mock_get_active_app.return_value = app + + context = tethys_apps_context(mock_args) + + mock_get_active_app.assert_called_once() + self.assertEqual('foo.id', context['tethys_app']['id']) + self.assertEqual('foo.name', context['tethys_app']['name'].value) + self.assertEqual('foo.icon', context['tethys_app']['icon'].value) + self.assertEqual('#foobar', context['tethys_app']['color'].value) + self.assertEqual('tags', context['tethys_app']['tags'].value) + self.assertEqual('foo.description', context['tethys_app']['description'].value) + self.assertTrue('enable_feedback' in context['tethys_app']) + self.assertTrue('feedback_emails' in context['tethys_app']) + self.assertEqual(True, context['tethys_app']['enable_feedback'].value) + self.assertEqual(['foo.feedback'], context['tethys_app']['feedback_emails']) diff --git a/tests/unit_tests/test_tethys_apps/test_decorators.py b/tests/unit_tests/test_tethys_apps/test_decorators.py new file mode 100644 index 000000000..bd727add4 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_decorators.py @@ -0,0 +1,149 @@ +import unittest +import mock + +from django.contrib.auth.models import AnonymousUser +from django.test import RequestFactory +from django.http import HttpResponseRedirect + +from tethys_sdk.permissions import permission_required +from tests.factories.django_user import UserFactory + + +class DecoratorsTest(unittest.TestCase): + + def setUp(self): + self.request_factory = RequestFactory() + self.user = UserFactory() + + def tearDown(self): + pass + + @mock.patch('tethys_apps.decorators.messages') + @mock.patch('tethys_apps.decorators.has_permission', return_value=False) + def test_permission_required_no_pass_authenticated(self, _, mock_messages): + request = self.request_factory.get('/apps/test-app') + request.user = self.user + + @permission_required('create_projects') + def create_projects(request, *args, **kwargs): + return "expected_result" + + ret = create_projects(request) + + mock_messages.add_message.assert_called() + self.assertIsInstance(ret, HttpResponseRedirect) + self.assertEqual('/apps/', ret.url) + + @mock.patch('tethys_apps.decorators.messages') + @mock.patch('tethys_apps.decorators.has_permission', return_value=False) + def test_permission_required_no_pass_authenticated_with_referrer(self, _, mock_messages): + request = self.request_factory.get('/apps/test-app') + request.user = self.user + request.META['HTTP_REFERER'] = 'http://testserver/foo/bar' + + @permission_required('create_projects') + def create_projects(request, *args, **kwargs): + return "expected_result" + + ret = create_projects(request) + + mock_messages.add_message.assert_called() + self.assertIsInstance(ret, HttpResponseRedirect) + self.assertEqual('/foo/bar', ret.url) + + @mock.patch('tethys_apps.decorators.messages') + @mock.patch('tethys_apps.decorators.has_permission', return_value=False) + def test_permission_required_no_pass_not_authenticated(self, _, mock_messages): + request = self.request_factory.get('/apps/test-app') + request.user = AnonymousUser() + + @permission_required('create_projects') + def create_projects(request, *args, **kwargs): + return "expected_result" + + ret = create_projects(request) + + mock_messages.add_message.assert_called() + self.assertIsInstance(ret, HttpResponseRedirect) + self.assertIn('/accounts/login/', ret.url) + + @mock.patch('tethys_apps.decorators.messages') + @mock.patch('tethys_apps.decorators.has_permission', return_value=False) + def test_permission_required_message(self, _, mock_messages): + request = self.request_factory.get('/apps/test-app') + request.user = self.user + msg = 'A different message.' + + @permission_required('create_projects', message=msg) + def create_projects(request, *args, **kwargs): + return "expected_result" + + ret = create_projects(request) + + mock_messages.add_message.assert_called_with(request, mock_messages.WARNING, msg) + self.assertIsInstance(ret, HttpResponseRedirect) + self.assertEqual('/apps/', ret.url) + + def test_blank_permissions(self): + self.assertRaises(ValueError, permission_required) + + @mock.patch('tethys_apps.decorators.has_permission', return_value=True) + def test_multiple_permissions(self, mock_has_permission): + request = self.request_factory.get('/apps/test-app') + request.user = self.user + + @permission_required('create_projects', 'delete_projects') + def multiple_permissions(request, *args, **kwargs): + return "expected_result" + + ret = multiple_permissions(request) + self.assertEqual(ret, "expected_result") + hp_call_args = mock_has_permission.call_args_list + self.assertEqual(2, len(hp_call_args)) + self.assertEqual('create_projects', hp_call_args[0][0][1]) + self.assertEqual('delete_projects', hp_call_args[1][0][1]) + + @mock.patch('tethys_apps.decorators.has_permission', return_value=True) + def test_multiple_permissions_OR(self, _): + request = self.request_factory.get('/apps/test-app') + request.user = self.user + + @permission_required('create_projects', 'delete_projects', use_or=True) + def multiple_permissions_or(request, *args, **kwargs): + return "expected_result" + + self.assertEqual(multiple_permissions_or(request), "expected_result") + + @mock.patch('tethys_apps.decorators.tethys_portal_error', return_value=False) + @mock.patch('tethys_apps.decorators.has_permission', return_value=False) + def test_permission_required_exception_403(self, _, mock_tp_error): + request = self.request_factory.get('/apps/test-app') + request.user = self.user + + @permission_required('create_projects', raise_exception=True) + def exception_403(request, *args, **kwargs): + return "expected_result" + + exception_403(request) + mock_tp_error.handler_403.assert_called_with(request) + + def test_permission_required_no_request(self): + @permission_required('create_projects') + def no_request(request, *args, **kwargs): + return "expected_result" + + self.assertRaises(ValueError, no_request) + + @mock.patch('tethys_apps.decorators.has_permission', return_value=True) + def test_multiple_permissions_class_method(self, _): + request = self.request_factory.get('/apps/test-app') + request.user = self.user + + class Foo(object): + @permission_required('create_projects') + def method(self, request, *args, **kwargs): + return "expected_result" + + f = Foo() + + self.assertEqual(f.method(request), "expected_result") diff --git a/tests/unit_tests/test_tethys_apps/test_exceptions.py b/tests/unit_tests/test_tethys_apps/test_exceptions.py new file mode 100644 index 000000000..45386882c --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_exceptions.py @@ -0,0 +1,39 @@ +import unittest +from tethys_apps.exceptions import TethysAppSettingDoesNotExist, TethysAppSettingNotAssigned, \ + PersistentStoreDoesNotExist, PersistentStoreExists, PersistentStoreInitializerError, PersistentStorePermissionError + + +def raise_exception(exc, *args, **kwargs): + raise exc(*args, **kwargs) + + +class TestExceptions(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_tethys_app_setting_does_not_exist(self): + self.assertRaises(TethysAppSettingDoesNotExist, raise_exception, TethysAppSettingDoesNotExist, 'setting-type', + 'setting-name', 'app-name') + exc = TethysAppSettingDoesNotExist('setting-type', 'setting-name', 'app-name') + self.assertIn('setting-type', str(exc)) + self.assertIn('setting-name', str(exc)) + self.assertIn('app-name', str(exc)) + self.assertIn('does not exist', str(exc)) + + def test_tethys_app_settign_not_assigned(self): + self.assertRaises(TethysAppSettingNotAssigned, raise_exception, TethysAppSettingNotAssigned) + + def test_persistent_store_does_not_exist(self): + self.assertRaises(PersistentStoreDoesNotExist, raise_exception, PersistentStoreDoesNotExist) + + def test_persistent_store_exists(self): + self.assertRaises(PersistentStoreExists, raise_exception, PersistentStoreExists) + + def test_persistent_store_permission_error(self): + self.assertRaises(PersistentStorePermissionError, raise_exception, PersistentStorePermissionError) + + def test_persistent_store_initializer_error(self): + self.assertRaises(PersistentStoreInitializerError, raise_exception, PersistentStoreInitializerError) diff --git a/tests/unit_tests/test_tethys_apps/test_harvester.py b/tests/unit_tests/test_tethys_apps/test_harvester.py new file mode 100644 index 000000000..7c36eb966 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_harvester.py @@ -0,0 +1,242 @@ +import io +import unittest +import mock + +from django.db.utils import ProgrammingError +from tethys_apps.harvester import SingletonHarvester +from tethys_apps.base.testing.environment import set_testing_environment + + +class HarvesterTest(unittest.TestCase): + + def setUp(self): + set_testing_environment(False) + + def tearDown(self): + set_testing_environment(True) + + @mock.patch('tethys_apps.harvester.tethys_log') + @mock.patch('sys.stdout', new_callable=io.StringIO) + def test_harvest_extensions_apps(self, mock_stdout, _): + """ + Test for SingletonHarvester.harvest. + Checks for expected text output + :param mock_stdout: mock for text output + :return: + """ + shv = SingletonHarvester() + shv.harvest() + + self.assertIn('Loading Tethys Extensions...', mock_stdout.getvalue()) + self.assertIn('Tethys Extensions Loaded:', mock_stdout.getvalue()) + self.assertIn('test_extension', mock_stdout.getvalue()) + + @mock.patch('sys.stdout', new_callable=io.StringIO) + @mock.patch('pkgutil.iter_modules') + def test_harvest_extensions_exception(self, mock_pkgutil, mock_stdout): + """ + Test for SingletonHarvester.harvest. + With an exception thrown, when harvesting the extensions + :param mock_pkgutil: mock for the exception + :param mock_stdout: mock for the text output + :return: + """ + mock_pkgutil.side_effect = Exception + + shv = SingletonHarvester() + shv.harvest_extensions() + + self.assertIn('Loading Tethys Extensions...', mock_stdout.getvalue()) + self.assertNotIn('Tethys Extensions Loaded:', mock_stdout.getvalue()) + self.assertNotIn('test_extension', mock_stdout.getvalue()) + + def test_harvest_get_url_patterns(self): + """ + Test for SingletonHarvester.get_url_patterns + :return: + """ + shv = SingletonHarvester() + app_url_patterns, extension_url_patterns = shv.get_url_patterns() + self.assertGreaterEqual(len(app_url_patterns), 1) + self.assertIn('test_app', app_url_patterns) + self.assertGreaterEqual(len(extension_url_patterns), 1) + self.assertIn('test_extension', extension_url_patterns) + + def test_harvest_validate_extension(self): + """ + Test for SingletonHarvester._validate_extension + :return: + """ + mock_args = mock.MagicMock() + + shv = SingletonHarvester() + extension = shv._validate_extension(mock_args) + self.assertEqual(mock_args, extension) + + def test_harvest_validate_app(self): + """ + Test for SingletonHarvester._validate_app + Gives invalid icon and color information which is altered by the function + :return: + """ + mock_args = mock.MagicMock() + mock_args.icon = '/foo_icon' # prepended slash + mock_args.color = 'foo_color' # missing prepended #, not 6 or 3 digit hex color + + shv = SingletonHarvester() + validate = shv._validate_app(mock_args) + + self.assertEqual('foo_icon', validate.icon) + self.assertEqual('', validate.color) + + @mock.patch('sys.stdout', new_callable=io.StringIO) + @mock.patch('tethys_apps.harvester.tethys_log.exception') + def test_harvest_extension_instances_ImportError(self, mock_logexception, mock_stdout): + """ + Test for SingletonHarvester._harvest_extension_instances + With an ImportError exception thrown due to invalid argument information passed + :param mock_logexception: mock for the tethys_log exception + :param mock_stdout: mock for the text output + :return: + """ + mock_args = mock.MagicMock() + dict_ext = {'foo': 'foo_ext'} + mock_args = dict_ext + + shv = SingletonHarvester() + shv._harvest_extension_instances(mock_args) + + valid_ext_instances = [] + valid_extension_modules = {} + + self.assertEqual(valid_ext_instances, shv.extensions) + self.assertEqual(valid_extension_modules, shv.extension_modules) + mock_logexception.assert_called_once() + self.assertIn('Tethys Extensions Loaded:', mock_stdout.getvalue()) + + @mock.patch('sys.stdout', new_callable=io.StringIO) + @mock.patch('tethys_apps.harvester.tethys_log.exception') + @mock.patch('tethys_apps.harvester.issubclass') + def test_harvest_extension_instances_TypeError(self, mock_subclass, mock_logexception, mock_stdout): + """ + Test for SingletonHarvester._harvest_extension_instances + With a TypeError exception mocked up + :param mock_subclass: mock for the TypeError exception + :param mock_logexception: mock for the tethys_log exception + :param mock_stdout: mock for the text output + :return: + """ + mock_args = mock.MagicMock() + dict_ext = {'test_extension': 'tethysext.test_extension'} + mock_args = dict_ext + mock_subclass.side_effect = TypeError + + shv = SingletonHarvester() + shv._harvest_extension_instances(mock_args) + + valid_ext_instances = [] + valid_extension_modules = {} + + self.assertEqual(valid_ext_instances, shv.extensions) + self.assertEqual(valid_extension_modules, shv.extension_modules) + mock_logexception.assert_not_called() + mock_subclass.assert_called() + self.assertIn('Tethys Extensions Loaded:', mock_stdout.getvalue()) + + @mock.patch('sys.stdout', new_callable=io.StringIO) + @mock.patch('tethys_apps.harvester.tethys_log.exception') + def test_harvest_app_instances_ImportError(self, mock_logexception, mock_stdout): + """ + Test for SingletonHarvester._harvest_app_instances + With an ImportError exception thrown due to invalid argument information passed + :param mock_logexception: mock for the tethys_log exception + :param mock_stdout: mock for the text output + :return: + """ + mock_args = mock.MagicMock() + list_apps = ['foo'] + mock_args = list_apps + + shv = SingletonHarvester() + shv._harvest_app_instances(mock_args) + + valid_app_instance_list = [] + + self.assertEqual(valid_app_instance_list, shv.apps) + mock_logexception.assert_called_once() + self.assertIn('Tethys Apps Loaded:', mock_stdout.getvalue()) + + @mock.patch('sys.stdout', new_callable=io.StringIO) + @mock.patch('tethys_apps.harvester.tethys_log.exception') + @mock.patch('tethys_apps.harvester.issubclass') + def test_harvest_app_instances_TypeError(self, mock_subclass, mock_logexception, mock_stdout): + """ + Test for SingletonHarvester._harvest_app_instances + :param mock_subclass: mock for the TypeError exception + :param mock_logexception: mock for the tethys_log exception + :param mock_stdout: mock for the text output + :return: + """ + mock_args = mock.MagicMock() + list_apps = [u'.gitignore', u'test_app', u'__init__.py', u'__init__.pyc'] + mock_args = list_apps + mock_subclass.side_effect = TypeError + + shv = SingletonHarvester() + shv._harvest_app_instances(mock_args) + + valid_app_instance_list = [] + + self.assertEqual(valid_app_instance_list, shv.apps) + mock_logexception.assert_not_called() + mock_subclass.assert_called() + self.assertIn('Tethys Apps Loaded:', mock_stdout.getvalue()) + + @mock.patch('sys.stdout', new_callable=io.StringIO) + @mock.patch('tethys_apps.harvester.tethys_log.exception') + @mock.patch('tethys_apps.tethysapp.test_app.app.TestApp.url_maps') + def test_harvest_app_instances_Exceptions1(self, mock_url_maps, mock_logexception, mock_stdout): + """ + Test for SingletonHarvester._harvest_app_instances + For the exception on lines 230-234 + With an exception mocked up for the url_patterns + :param mock_url_maps: mock for url_patterns to thrown an Exception + :param mock_logexception: mock for the tethys_log exception + :param mock_stdout: mock for the text output + :return: + """ + mock_args = mock.MagicMock() + list_apps = [u'.gitignore', u'test_app', u'__init__.py', u'__init__.pyc'] + mock_args = list_apps + mock_url_maps.side_effect = ImportError + + shv = SingletonHarvester() + shv._harvest_app_instances(mock_args) + + mock_logexception.assert_called() + mock_url_maps.assert_called() + self.assertIn('Tethys Apps Loaded:', mock_stdout.getvalue()) + + @mock.patch('sys.stdout', new_callable=io.StringIO) + @mock.patch('tethys_apps.harvester.tethys_log.warning') + @mock.patch('tethys_apps.tethysapp.test_app.app.TestApp.register_app_permissions') + def test_harvest_app_instances_Exceptions2(self, mock_permissions, mock_logwarning, mock_stdout): + """ + Test for SingletonHarvester._harvest_app_instances + For the exception on lines 239-240 + With an exception mocked up for register_app_permissions + :param mock_permissions: mock for throwing a ProgrammingError exception + :param mock_logerror: mock for the tethys_log error + :param mock_stdout: mock for the text output + :return: + """ + list_apps = [u'.gitignore', u'test_app', u'__init__.py', u'__init__.pyc'] + mock_permissions.side_effect = ProgrammingError + + shv = SingletonHarvester() + shv._harvest_app_instances(list_apps) + + mock_logwarning.assert_called() + mock_permissions.assert_called() + self.assertIn('Tethys Apps Loaded:', mock_stdout.getvalue()) + self.assertIn('test_app', mock_stdout.getvalue()) diff --git a/tests/unit_tests/test_tethys_apps/test_helpers.py b/tests/unit_tests/test_tethys_apps/test_helpers.py new file mode 100644 index 000000000..8f64e71dd --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_helpers.py @@ -0,0 +1,48 @@ +import unittest +import mock + +from tethys_apps import helpers + + +class TethysAppsHelpersTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_get_tethysapp_dir(self): + # Get the absolute path to the tethysapp directory + result = helpers.get_tethysapp_dir() + self.assertIn('/tethys_apps/tethysapp', result) + + def test_get_installed_tethys_apps(self): + # Get list of apps installed in the tethysapp directory + result = helpers.get_installed_tethys_apps() + self.assertTrue('test_app' in result) + + @mock.patch('tethys_apps.helpers.os.path.isdir') + @mock.patch('tethys_apps.helpers.os.listdir') + @mock.patch('tethys_apps.helpers.get_tethysapp_dir') + def test_get_installed_tethys_apps_mock(self, mock_dir, mock_listdir, mock_isdir): + # Get list of apps installed in the mock directory + mock_dir.return_value = '/foo/bar' + mock_listdir.return_value = ['.gitignore', 'foo_app', '__init__.py', '__init__.pyc'] + mock_isdir.side_effect = [False, True, False, False] + result = helpers.get_installed_tethys_apps() + self.assertTrue('foo_app' in result) + + def test_get_installed_tethys_extensions(self): + # Get a list of installed extensions + result = helpers.get_installed_tethys_extensions() + self.assertTrue('test_extension' in result) + + @mock.patch('tethys_apps.helpers.SingletonHarvester') + def test_get_installed_tethys_extensions_error(self, mock_harvester): + # Mock the extension_modules variable with bad data + mock_harvester().extension_modules = {'foo_invalid_foo': 'tethysext.foo_invalid_foo'} + + # Get a list of installed extensions + result = helpers.get_installed_tethys_extensions() + self.assertEqual({}, result) diff --git a/tests/unit_tests/test_tethys_apps/test_management/__init__.py b/tests/unit_tests/test_tethys_apps/test_management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_apps/test_management/test_commands/__init__.py b/tests/unit_tests/test_tethys_apps/test_management/test_commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_collectworkspaces.py b/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_collectworkspaces.py new file mode 100644 index 000000000..bc4198081 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_collectworkspaces.py @@ -0,0 +1,228 @@ +import unittest +import mock + +from tethys_apps.management.commands import collectworkspaces + + +class ManagementCommandsCollectWorkspacesTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_collectworkspaces_add_arguments(self): + from argparse import ArgumentParser + parser = ArgumentParser() + cmd = collectworkspaces.Command() + cmd.add_arguments(parser) + + self.assertIn('[-f]', parser.format_usage()) + self.assertIn('--force', parser.format_help()) + self.assertIn('Force the overwrite the app directory', parser.format_help()) + + @mock.patch('tethys_apps.management.commands.collectworkspaces.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.exit') + @mock.patch('tethys_apps.management.commands.collectworkspaces.settings') + def test_collectworkspaces_handle_no_atts(self, mock_settings, mock_exit, mock_print): + mock_settings.TETHYS_WORKSPACES_ROOT = None + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + cmd = collectworkspaces.Command() + self.assertRaises(SystemExit, cmd.handle) + + check_msg = 'WARNING: Cannot find the TETHYS_WORKSPACES_ROOT setting in the settings.py file. ' \ + 'Please provide the path to the static directory using the TETHYS_WORKSPACES_ROOT ' \ + 'setting and try again.' + + mock_print.assert_called_with(check_msg) + + @mock.patch('tethys_apps.management.commands.collectworkspaces.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.isdir') + @mock.patch('tethys_apps.management.commands.collectworkspaces.get_installed_tethys_apps') + @mock.patch('tethys_apps.management.commands.collectworkspaces.settings') + def test_collectworkspaces_handle_no_force_not_dir(self, mock_settings, mock_get_apps, mock_os_path_isdir, + mock_print): + mock_settings.TETHYS_WORKSPACES_ROOT = '/foo/workspace' + mock_get_apps.return_value = {'foo_app': '/foo/testing/tests/foo_app'} + mock_os_path_isdir.return_value = False + + cmd = collectworkspaces.Command() + cmd.handle(force=False) + + mock_get_apps.assert_called_once() + mock_os_path_isdir.assert_called_once_with('/foo/testing/tests/foo_app/workspaces') + + msg_info = 'INFO: Moving workspace directories of apps to "/foo/workspace" and linking back.' + + msg_warning = 'WARNING: The workspace_path for app "foo_app" is not a directory. Skipping...' + + print_call_args = mock_print.call_args_list + + self.assertEqual(msg_info, print_call_args[0][0][0]) + + self.assertEqual(msg_warning, print_call_args[1][0][0]) + + @mock.patch('tethys_apps.management.commands.collectworkspaces.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.islink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.isdir') + @mock.patch('tethys_apps.management.commands.collectworkspaces.get_installed_tethys_apps') + @mock.patch('tethys_apps.management.commands.collectworkspaces.settings') + def test_collectworkspaces_handle_no_force_is_link(self, mock_settings, mock_get_apps, mock_os_path_isdir, + mock_os_path_islink, mock_print): + mock_settings.TETHYS_WORKSPACES_ROOT = '/foo/workspace' + mock_get_apps.return_value = {'foo_app': '/foo/testing/tests/foo_app'} + mock_os_path_isdir.return_value = True + mock_os_path_islink.return_value = True + + cmd = collectworkspaces.Command() + cmd.handle(force=False) + + mock_get_apps.assert_called_once() + mock_os_path_isdir.assert_called_once_with('/foo/testing/tests/foo_app/workspaces') + mock_os_path_islink.assert_called_once_with('/foo/testing/tests/foo_app/workspaces') + msg_in = 'INFO: Moving workspace directories of apps to "/foo/workspace" and linking back.' + mock_print.assert_called_with(msg_in) + + @mock.patch('tethys_apps.management.commands.collectworkspaces.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.symlink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.shutil.move') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.exists') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.islink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.isdir') + @mock.patch('tethys_apps.management.commands.collectworkspaces.get_installed_tethys_apps') + @mock.patch('tethys_apps.management.commands.collectworkspaces.settings') + def test_collectworkspaces_handle_not_exists(self, mock_settings, mock_get_apps, mock_os_path_isdir, + mock_os_path_islink, mock_os_path_exists, mock_shutil_move, + mock_os_symlink, mock_print): + mock_settings.TETHYS_WORKSPACES_ROOT = '/foo/workspace' + mock_get_apps.return_value = {'foo_app': '/foo/testing/tests/foo_app'} + mock_os_path_isdir.side_effect = [True, True] + mock_os_path_islink.return_value = False + mock_os_path_exists.return_value = False + mock_shutil_move.return_value = True + mock_os_symlink.return_value = True + + cmd = collectworkspaces.Command() + cmd.handle(force=True) + + mock_get_apps.assert_called_once() + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_app/workspaces') + mock_os_path_isdir.assert_called_with('/foo/workspace/foo_app') + mock_os_path_islink.assert_called_once_with('/foo/testing/tests/foo_app/workspaces') + mock_os_path_exists.assert_called_once_with('/foo/workspace/foo_app') + mock_shutil_move.assert_called_once_with('/foo/testing/tests/foo_app/workspaces', '/foo/workspace/foo_app') + msg_first_info = 'INFO: Moving workspace directories of apps to "/foo/workspace" and linking back.' + msg_second_info = 'INFO: Successfully linked "workspaces" directory to ' \ + 'TETHYS_WORKSPACES_ROOT for app "foo_app".' + + print_call_args = mock_print.call_args_list + + self.assertEqual(msg_first_info, print_call_args[0][0][0]) + + self.assertEqual(msg_second_info, print_call_args[1][0][0]) + + @mock.patch('tethys_apps.management.commands.collectworkspaces.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.shutil.rmtree') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.symlink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.shutil.move') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.exists') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.islink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.isdir') + @mock.patch('tethys_apps.management.commands.collectworkspaces.get_installed_tethys_apps') + @mock.patch('tethys_apps.management.commands.collectworkspaces.settings') + def test_collectworkspaces_handle_exists_no_force(self, mock_settings, mock_get_apps, mock_os_path_isdir, + mock_os_path_islink, mock_os_path_exists, mock_shutil_move, + mock_os_symlink, mock_shutil_rmtree, mock_print): + mock_settings.TETHYS_WORKSPACES_ROOT = '/foo/workspace' + mock_get_apps.return_value = {'foo_app': '/foo/testing/tests/foo_app'} + mock_os_path_isdir.side_effect = [True, True] + mock_os_path_islink.return_value = False + mock_os_path_exists.return_value = True + mock_shutil_move.return_value = True + mock_os_symlink.return_value = True + mock_shutil_rmtree.return_value = True + + cmd = collectworkspaces.Command() + cmd.handle(force=False) + + mock_get_apps.assert_called_once() + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_app/workspaces') + mock_os_path_isdir.assert_called_with('/foo/workspace/foo_app') + mock_os_path_islink.assert_called_once_with('/foo/testing/tests/foo_app/workspaces') + mock_os_path_exists.assert_called_once_with('/foo/workspace/foo_app') + mock_shutil_move.assert_not_called() + mock_shutil_rmtree.called_once_with('/foo/workspace/foo_app', ignore_errors=True) + + msg_first_info = 'INFO: Moving workspace directories of apps to "/foo/workspace" and linking back.' + + msg_warning = 'WARNING: Workspace directory for app "foo_app" already exists in the ' \ + 'TETHYS_WORKSPACES_ROOT directory. A symbolic link is being created to the existing directory. ' \ + 'To force overwrite the existing directory, re-run the command with the "-f" argument.' + msg_second_info = 'INFO: Successfully linked "workspaces" directory to ' \ + 'TETHYS_WORKSPACES_ROOT for app "foo_app".' + + print_call_args = mock_print.call_args_list + + self.assertEqual(msg_first_info, print_call_args[0][0][0]) + + self.assertEqual(msg_warning, print_call_args[1][0][0]) + + self.assertEqual(msg_second_info, print_call_args[2][0][0]) + + @mock.patch('tethys_apps.management.commands.collectworkspaces.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.remove') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.shutil.rmtree') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.symlink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.shutil.move') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.exists') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.islink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.isdir') + @mock.patch('tethys_apps.management.commands.collectworkspaces.get_installed_tethys_apps') + @mock.patch('tethys_apps.management.commands.collectworkspaces.settings') + def test_collectworkspaces_handle_exists_force_exception(self, mock_settings, mock_get_apps, mock_os_path_isdir, + mock_os_path_islink, mock_os_path_exists, mock_shutil_move, + mock_os_symlink, mock_shutil_rmtree, mock_os_remove, + mock_print): + mock_settings.TETHYS_WORKSPACES_ROOT = '/foo/workspace' + mock_get_apps.return_value = {'foo_app': '/foo/testing/tests/foo_app'} + mock_os_path_isdir.side_effect = [True, True] + mock_os_path_islink.return_value = False + mock_os_path_exists.return_value = True + mock_shutil_move.return_value = True + mock_os_symlink.return_value = True + mock_shutil_rmtree.return_value = True + mock_os_remove.side_effect = OSError + + cmd = collectworkspaces.Command() + cmd.handle(force=True) + + mock_get_apps.assert_called_once() + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_app/workspaces') + mock_os_path_isdir.assert_called_with('/foo/workspace/foo_app') + mock_os_path_islink.assert_called_once_with('/foo/testing/tests/foo_app/workspaces') + mock_os_path_exists.assert_called_once_with('/foo/workspace/foo_app') + mock_shutil_move.assert_called_once_with('/foo/testing/tests/foo_app/workspaces', '/foo/workspace/foo_app') + mock_shutil_rmtree.called_once_with('/foo/testing/tests/foo_app/workspaces', ignore_errors=True) + mock_os_remove.assert_called_once_with('/foo/workspace/foo_app') + + msg_first_info = 'INFO: Moving workspace directories of apps to "/foo/workspace" and linking back.' + + msg_second_info = 'INFO: Successfully linked "workspaces" directory to ' \ + 'TETHYS_WORKSPACES_ROOT for app "foo_app".' + + msg_warning = 'WARNING: Workspace directory for app "foo_app" already exists in the TETHYS_WORKSPACES_ROOT ' \ + 'directory. A symbolic link is being created to the existing directory. To force overwrite ' \ + 'the existing directory, re-run the command with the "-f" argument.' + + print_call_args = mock_print.call_args_list + + self.assertEqual(msg_first_info, print_call_args[0][0][0]) + + self.assertEqual(msg_second_info, print_call_args[1][0][0]) + + for i in range(len(print_call_args)): + self.assertNotEquals(msg_warning, print_call_args[i][0][0]) diff --git a/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_pre_collectstatic.py b/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_pre_collectstatic.py new file mode 100644 index 000000000..f34c552e4 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_pre_collectstatic.py @@ -0,0 +1,248 @@ +import unittest +import mock + +from tethys_apps.management.commands import pre_collectstatic + + +class ManagementCommandsPreCollectStaticTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.management.commands.pre_collectstatic.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.exit') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.settings') + def test_handle_no_static_root(self, mock_settings, mock_exit, mock_print): + mock_settings.STATIC_ROOT = None + # NOTE: to prevent our tests from exiting prematurely, we change the behavior of exit to raise an exception + # to break the code execution, which we catch below. + mock_exit.side_effect = SystemExit + + cmd = pre_collectstatic.Command() + self.assertRaises(SystemExit, cmd.handle) + + print_args = mock_print.call_args_list + + msg_warning = 'WARNING: Cannot find the STATIC_ROOT setting in the settings.py file. Please provide the ' \ + 'path to the static directory using the STATIC_ROOT setting and try again.' + self.assertEqual(msg_warning, print_args[0][0][0]) + + @mock.patch('tethys_apps.management.commands.pre_collectstatic.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.symlink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.isdir') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.remove') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.get_installed_tethys_extensions') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.get_installed_tethys_apps') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.settings') + def test_handle_public_not_static(self, mock_settings, mock_get_apps, mock_get_extensions, mock_os_remove, + mock_os_path_isdir, mock_os_symlink, mock_print): + mock_settings.STATIC_ROOT = '/foo/testing/tests' + mock_get_apps.return_value = {'foo_app': '/foo/testing/tests/foo_app'} + mock_get_extensions.return_value = {'foo_extension': '/foo/testing/tests/foo_extension'} + mock_os_remove.return_value = True + mock_os_path_isdir.return_value = True + mock_os_symlink.return_value = True + + cmd = pre_collectstatic.Command() + cmd.handle(options='foo') + + mock_get_apps.assert_called_once() + mock_get_extensions.assert_called_once() + mock_os_remove.assert_any_call('/foo/testing/tests/foo_app') + mock_os_remove.assert_any_call('/foo/testing/tests/foo_extension') + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_app/public') + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_extension/public') + mock_os_symlink.assert_any_call('/foo/testing/tests/foo_app/public', '/foo/testing/tests/foo_app') + mock_os_symlink.assert_any_call('/foo/testing/tests/foo_extension/public', + '/foo/testing/tests/foo_extension') + print_args = mock_print.call_args_list + + msg = 'INFO: Linking static and public directories of apps and extensions to "{0}".'\ + . format(mock_settings.STATIC_ROOT) + + msg_info_first = 'INFO: Successfully linked public directory to STATIC_ROOT for app "foo_app".' + + msg_info_second = 'INFO: Successfully linked public directory to STATIC_ROOT for app "foo_extension".' + + check_list = [] + for i in range(len(print_args)): + check_list.append(print_args[i][0][0]) + + self.assertIn(msg, check_list) + self.assertIn(msg_info_first, check_list) + self.assertIn(msg_info_second, check_list) + + msg_warning_not_in = 'WARNING: Cannot find the STATIC_ROOT setting' + msg_not_in = 'Please provide the path to the static directory' + info_not_in_first = 'INFO: Successfully linked static directory to STATIC_ROOT for app "foo_app".' + info_not_in_second = 'INFO: Successfully linked static directory to STATIC_ROOT for app "foo_extension".' + + for i in range(len(print_args)): + self.assertNotEquals(msg_warning_not_in, print_args[i][0][0]) + self.assertNotEquals(msg_not_in, print_args[i][0][0]) + self.assertNotEquals(info_not_in_first, print_args[i][0][0]) + self.assertNotEquals(info_not_in_second, print_args[i][0][0]) + + @mock.patch('tethys_apps.management.commands.pre_collectstatic.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.symlink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.isdir') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.shutil.rmtree') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.remove') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.get_installed_tethys_extensions') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.get_installed_tethys_apps') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.settings') + def test_handle_public_not_static_Exceptions(self, mock_settings, mock_get_apps, mock_get_extensions, + mock_os_remove, mock_shutil_rmtree, mock_os_path_isdir, + mock_os_symlink, mock_print): + mock_settings.STATIC_ROOT = '/foo/testing/tests' + mock_get_apps.return_value = {'foo_app': '/foo/testing/tests/foo_app'} + mock_get_extensions.return_value = {'foo_extension': '/foo/testing/tests/foo_extension'} + mock_os_remove.side_effect = OSError + mock_shutil_rmtree.side_effect = OSError + mock_os_path_isdir.return_value = True + mock_os_symlink.return_value = True + + cmd = pre_collectstatic.Command() + cmd.handle(options='foo') + + mock_get_apps.assert_called_once() + mock_get_extensions.assert_called_once() + mock_os_remove.assert_any_call('/foo/testing/tests/foo_app') + mock_os_remove.assert_any_call('/foo/testing/tests/foo_extension') + mock_shutil_rmtree.assert_any_call('/foo/testing/tests/foo_app') + mock_shutil_rmtree.assert_any_call('/foo/testing/tests/foo_extension') + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_app/public') + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_extension/public') + mock_os_symlink.assert_any_call('/foo/testing/tests/foo_app/public', '/foo/testing/tests/foo_app') + mock_os_symlink.assert_any_call('/foo/testing/tests/foo_extension/public', + '/foo/testing/tests/foo_extension') + msg_infor_1 = 'INFO: Linking static and public directories of apps and extensions to "{0}".'\ + .format(mock_settings.STATIC_ROOT) + msg_infor_2 = 'INFO: Successfully linked public directory to STATIC_ROOT for app "foo_app".' + msg_infor_3 = 'INFO: Successfully linked public directory to STATIC_ROOT for app "foo_extension".' + + warn_not_in = 'WARNING: Cannot find the STATIC_ROOT setting' + msg_not_in = 'Please provide the path to the static directory' + info_not_in_first = 'INFO: Successfully linked static directory to STATIC_ROOT for app "foo_app".' + info_not_in_second = 'INFO: Successfully linked static directory to STATIC_ROOT for app "foo_extension".' + + print_args = mock_print.call_args_list + + check_list = [] + for i in range(len(print_args)): + check_list.append(print_args[i][0][0]) + + self.assertIn(msg_infor_1, check_list) + self.assertIn(msg_infor_2, check_list) + self.assertIn(msg_infor_3, check_list) + + for i in range(len(print_args)): + self.assertNotEquals(warn_not_in, print_args[i][0][0]) + self.assertNotEquals(msg_not_in, print_args[i][0][0]) + self.assertNotEquals(info_not_in_first, print_args[i][0][0]) + self.assertNotEquals(info_not_in_second, print_args[i][0][0]) + + @mock.patch('tethys_apps.management.commands.pre_collectstatic.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.symlink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.isdir') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.remove') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.get_installed_tethys_extensions') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.get_installed_tethys_apps') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.settings') + def test_handle_not_public_static(self, mock_settings, mock_get_apps, mock_get_extensions, mock_os_remove, + mock_os_path_isdir, mock_os_symlink, mock_print): + mock_settings.STATIC_ROOT = '/foo/testing/tests' + mock_get_apps.return_value = {'foo_app': '/foo/testing/tests/foo_app'} + mock_get_extensions.return_value = {'foo_extension': '/foo/testing/tests/foo_extension'} + mock_os_remove.return_value = True + mock_os_path_isdir.side_effect = [False, True, False, True] + mock_os_symlink.return_value = True + + cmd = pre_collectstatic.Command() + cmd.handle(options='foo') + + mock_get_apps.assert_called_once() + mock_get_extensions.assert_called_once() + mock_os_remove.assert_any_call('/foo/testing/tests/foo_app') + mock_os_remove.assert_any_call('/foo/testing/tests/foo_extension') + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_app/static') + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_extension/static') + mock_os_symlink.assert_any_call('/foo/testing/tests/foo_app/static', '/foo/testing/tests/foo_app') + mock_os_symlink.assert_any_call('/foo/testing/tests/foo_extension/static', + '/foo/testing/tests/foo_extension') + + msg_info_one = 'INFO: Linking static and public directories of apps and extensions to "{0}".'\ + .format(mock_settings.STATIC_ROOT) + msg_info_two = 'INFO: Successfully linked static directory to STATIC_ROOT for app "foo_app".' + msg_info_three = 'INFO: Successfully linked static directory to STATIC_ROOT for app "foo_extension".' + + warn_not_in = 'WARNING: Cannot find the STATIC_ROOT setting' + msg_not_in = 'Please provide the path to the static directory' + info_not_in_first = 'INFO: Successfully linked public directory to STATIC_ROOT for app "foo_app".' + info_not_in_second = 'INFO: Successfully linked public directory to STATIC_ROOT for app "foo_extension".' + + print_args = mock_print.call_args_list + + check_list = [] + for i in range(len(print_args)): + check_list.append(print_args[i][0][0]) + + self.assertIn(msg_info_one, check_list) + self.assertIn(msg_info_two, check_list) + self.assertIn(msg_info_three, check_list) + + for i in range(len(print_args)): + self.assertNotEquals(warn_not_in, print_args[i][0][0]) + self.assertNotEquals(msg_not_in, print_args[i][0][0]) + self.assertNotEquals(info_not_in_first, print_args[i][0][0]) + self.assertNotEquals(info_not_in_second, print_args[i][0][0]) + + @mock.patch('tethys_apps.management.commands.pre_collectstatic.print') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.symlink') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.path.isdir') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.os.remove') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.get_installed_tethys_extensions') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.get_installed_tethys_apps') + @mock.patch('tethys_apps.management.commands.pre_collectstatic.settings') + def test_handle_not_public_not_static(self, mock_settings, mock_get_apps, mock_get_extensions, mock_os_remove, + mock_os_path_isdir, mock_os_symlink, mock_print): + mock_settings.STATIC_ROOT = '/foo/testing/tests' + mock_get_apps.return_value = {'foo_app': '/foo/testing/tests/foo_app'} + mock_get_extensions.return_value = {'foo_extension': '/foo/testing/tests/foo_extension'} + mock_os_remove.return_value = True + mock_os_path_isdir.side_effect = [False, False, False, False] + mock_os_symlink.return_value = True + + cmd = pre_collectstatic.Command() + cmd.handle(options='foo') + + mock_get_apps.assert_called_once() + mock_get_extensions.assert_called_once() + mock_os_remove.assert_any_call('/foo/testing/tests/foo_app') + mock_os_remove.assert_any_call('/foo/testing/tests/foo_extension') + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_app/static') + mock_os_path_isdir.assert_any_call('/foo/testing/tests/foo_extension/static') + mock_os_symlink.assert_not_called() + msg_info = 'INFO: Linking static and public directories of apps and extensions to "{0}".'\ + .format(mock_settings.STATIC_ROOT) + + warn_not_in = 'WARNING: Cannot find the STATIC_ROOT setting' + msg_not_in = 'Please provide the path to the static directory' + info_not_in_first = 'INFO: Successfully linked public directory to STATIC_ROOT for app "foo_app".' + info_not_in_second = 'INFO: Successfully linked public directory to STATIC_ROOT for app "foo_extension".' + info_not_in_third = 'INFO: Successfully linked static directory to STATIC_ROOT for app "foo_app".' + info_not_in_fourth = 'INFO: Successfully linked static directory to STATIC_ROOT for app "foo_extension".' + + print_args = mock_print.call_args_list + self.assertEqual(msg_info, print_args[0][0][0]) + + for i in range(len(print_args)): + self.assertNotEquals(warn_not_in, print_args[i][0][0]) + self.assertNotEquals(msg_not_in, print_args[i][0][0]) + self.assertNotEquals(info_not_in_first, print_args[i][0][0]) + self.assertNotEquals(info_not_in_second, print_args[i][0][0]) + self.assertNotEquals(info_not_in_third, print_args[i][0][0]) + self.assertNotEquals(info_not_in_fourth, print_args[i][0][0]) diff --git a/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_syncstores.py b/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_syncstores.py new file mode 100644 index 000000000..e37c7b338 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_syncstores.py @@ -0,0 +1,161 @@ +try: + from StringIO import StringIO +except ImportError: + from io import StringIO +import unittest +import mock + +from argparse import ArgumentParser +from tethys_apps.management.commands import syncstores + + +class ManagementCommandsSyncstoresTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_syncstores_add_arguments(self): + parser = ArgumentParser() + cmd = syncstores.Command() + cmd.add_arguments(parser) + self.assertIn('app_name', parser.format_usage()) + self.assertIn('[-r]', parser.format_usage()) + self.assertIn('[-f]', parser.format_usage()) + self.assertIn('[-d DATABASE]', parser.format_usage()) + self.assertIn('--refresh', parser.format_help()) + self.assertIn('--firsttime', parser.format_help()) + self.assertIn('--database DATABASE', parser.format_help()) + + @mock.patch('tethys_apps.management.commands.syncstores.Command.provision_persistent_stores') + def test_handle(self, mock_provision_persistent_stores): + # Mock the function, it will be tested elsewhere + mock_provision_persistent_stores.return_value = True + + cmd = syncstores.Command() + cmd.handle(app_name='foo') + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.models.TethysApp.persistent_store_database_settings') + @mock.patch('tethys_apps.models.TethysApp.persistent_store_database_settings') + @mock.patch('tethys_apps.models.TethysApp.persistent_store_database_settings') + @mock.patch('tethys_apps.models.TethysApp') + def test_provision_persistent_stores_all_apps_no_database(self, mock_app, mock_setting1, mock_setting2, + mock_setting3, mock_stdout): + # Mock arguments + mock_app_names = syncstores.ALL_APPS + mock_options = {'database': '', 'refresh': True, 'first_time': True} + + # Mock for ps db settings + mock_setting1.name = 'setting1_name' + mock_setting1.create_persistent_store_database.return_value = True + mock_setting2.name = 'setting2_name' + mock_setting2.create_persistent_store_database.return_value = True + mock_setting3.name = 'setting3_name' + mock_setting3.create_persistent_store_database.return_value = True + + # Mock for TethysApp (2 apps, 2 settings for first app, 1 setting for second app) + mock_app1 = mock.MagicMock() + mock_app1.persistent_store_database_settings = [mock_setting1, mock_setting2] + mock_app2 = mock.MagicMock() + mock_app2.persistent_store_database_settings = [mock_setting3] + mock_app.objects.all.return_value = [mock_app1, mock_app2] + + cmd = syncstores.Command() + cmd.provision_persistent_stores(app_names=mock_app_names, options=mock_options) + + mock_app.objects.all.assert_called_once() + mock_setting1.create_persistent_store_database.assert_called_once_with(refresh=True, force_first_time=True) + mock_setting2.create_persistent_store_database.assert_called_once_with(refresh=True, force_first_time=True) + mock_setting3.create_persistent_store_database.assert_called_once_with(refresh=True, force_first_time=True) + self.assertIn('Provisioning Persistent Stores...', mock_stdout.getvalue()) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.models.TethysApp.persistent_store_database_settings') + @mock.patch('tethys_apps.models.TethysApp.persistent_store_database_settings') + @mock.patch('tethys_apps.models.TethysApp.persistent_store_database_settings') + @mock.patch('tethys_apps.models.TethysApp') + def test_provision_persistent_stores_all_apps_database_no_match(self, mock_app, mock_setting1, mock_setting2, + mock_setting3, mock_stdout): + # Mock arguments + mock_app_names = syncstores.ALL_APPS + mock_options = {'database': '/foo/no_match', 'refresh': True, 'first_time': True} + + # Mock for ps db settings + mock_setting1.name = 'setting1_name' + mock_setting1.create_persistent_store_database.return_value = True + mock_setting2.name = 'setting2_name' + mock_setting2.create_persistent_store_database.return_value = True + mock_setting3.name = 'setting3_name' + mock_setting3.create_persistent_store_database.return_value = True + + # Mock for TethysApp (2 apps, 2 settings for first app, 1 setting for second app) + mock_app1 = mock.MagicMock() + mock_app1.persistent_store_database_settings = [mock_setting1, mock_setting2] + mock_app2 = mock.MagicMock() + mock_app2.persistent_store_database_settings = [mock_setting3] + mock_app.objects.all.return_value = [mock_app1, mock_app2] + + cmd = syncstores.Command() + cmd.provision_persistent_stores(app_names=mock_app_names, options=mock_options) + + mock_app.objects.all.assert_called_once() + mock_setting1.create_persistent_store_database.assert_not_called() + mock_setting2.create_persistent_store_database.assert_not_called() + mock_setting3.create_persistent_store_database.assert_not_called() + self.assertIn('Provisioning Persistent Stores...', mock_stdout.getvalue()) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.models.TethysApp.persistent_store_database_settings') + @mock.patch('tethys_apps.models.TethysApp.persistent_store_database_settings') + @mock.patch('tethys_apps.models.TethysApp.persistent_store_database_settings') + @mock.patch('tethys_apps.models.TethysApp') + def test_provision_persistent_stores_all_apps_database_single_match(self, mock_app, mock_setting1, mock_setting2, + mock_setting3, mock_stdout): + # Mock arguments + mock_app_names = syncstores.ALL_APPS + mock_options = {'database': '/foo/match', 'refresh': False, 'first_time': False} + + # Mock for ps db settings + mock_setting1.name = 'setting1_name' + mock_setting1.create_persistent_store_database.return_value = True + mock_setting2.name = '/foo/match' + mock_setting2.create_persistent_store_database.return_value = True + mock_setting3.name = 'setting3_name' + mock_setting3.create_persistent_store_database.return_value = True + + # Mock for TethysApp (2 apps, 2 settings for first app, 1 setting for second app) + mock_app1 = mock.MagicMock() + mock_app1.persistent_store_database_settings = [mock_setting1, mock_setting2] + mock_app2 = mock.MagicMock() + mock_app2.persistent_store_database_settings = [mock_setting3] + mock_app.objects.all.return_value = [mock_app1, mock_app2] + + cmd = syncstores.Command() + cmd.provision_persistent_stores(app_names=mock_app_names, options=mock_options) + + mock_app.objects.all.assert_called_once() + mock_setting1.create_persistent_store_database.assert_not_called() + mock_setting2.create_persistent_store_database.assert_called_once_with(refresh=False, force_first_time=False) + mock_setting3.create_persistent_store_database.assert_not_called() + self.assertIn('Provisioning Persistent Stores...', mock_stdout.getvalue()) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.models.TethysApp') + def test_provision_persistent_stores_given_apps_not_found(self, mock_app, mock_stdout): + # Mock arguments + mock_app_names = ['foo_missing'] + mock_options = {'database': '', 'refresh': True, 'first_time': True} + + # Mock for TethysApp (return no apps found) + mock_app.objects.filter.return_value = [] + + cmd = syncstores.Command() + cmd.provision_persistent_stores(app_names=mock_app_names, options=mock_options) + + mock_app.objects.filter.assert_called_once() + self.assertIn('The app named "foo_missing" cannot be found.', mock_stdout.getvalue()) + self.assertIn('Please make sure it is installed and try again.', mock_stdout.getvalue()) + self.assertIn('Provisioning Persistent Stores...', mock_stdout.getvalue()) diff --git a/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_tethys_app_uninstall.py b/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_tethys_app_uninstall.py new file mode 100644 index 000000000..a310794f2 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_management/test_commands/test_tethys_app_uninstall.py @@ -0,0 +1,124 @@ +try: + from StringIO import StringIO +except ImportError: + from io import StringIO +import unittest +import mock + +from argparse import ArgumentParser +from tethys_apps.management.commands import tethys_app_uninstall +from tethys_apps.models import TethysApp, TethysExtension + + +class ManagementCommandsTethysAppUninstallTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_tethys_app_uninstall_add_arguments(self): + parser = ArgumentParser('foo_parser') + cmd = tethys_app_uninstall.Command() + cmd.add_arguments(parser) + self.assertIn('foo_parser', parser.format_usage()) + self.assertIn('app_or_extension', parser.format_usage()) + self.assertIn('[-e]', parser.format_usage()) + self.assertIn('--extension', parser.format_help()) + + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.exit') + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.input') + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.get_installed_tethys_extensions') + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.get_installed_tethys_apps') + def test_tethys_app_uninstall_handle_apps_cancel(self, mock_installed_apps, mock_installed_extensions, mock_input, + mock_stdout, mock_exit): + mock_installed_apps.return_value = ['foo_app'] + mock_installed_extensions.return_value = {} + mock_input.side_effect = ['foo', 'no'] + mock_exit.side_effect = SystemExit + + cmd = tethys_app_uninstall.Command() + self.assertRaises(SystemExit, cmd.handle, app_or_extension=['tethysapp.foo_app'], is_extension=False) + + mock_installed_apps.assert_called_once() + mock_installed_extensions.assert_not_called() + self.assertIn('Uninstall cancelled by user.', mock_stdout.getvalue()) + + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.os.path.join') + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.os.remove') + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.shutil.rmtree') + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.subprocess.Popen') + @mock.patch('warnings.warn') + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.input') + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.get_installed_tethys_extensions') + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.get_installed_tethys_apps') + @mock.patch('tethys_apps.models.TethysExtension') + @mock.patch('tethys_apps.models.TethysApp') + def test_tethys_app_uninstall_handle_apps_delete_rmtree_Popen_remove_exceptions(self, mock_app, mock_extension, + mock_installed_apps, + mock_installed_extensions, + mock_input, mock_stdout, + mock_warnings, mock_popen, + mock_rmtree, mock_os_remove, + mock_join): + mock_app.objects.get.return_value = mock.MagicMock() + mock_app.objects.get().delete.return_value = True + mock_extension.objects.get.return_value = mock.MagicMock() + mock_extension.objects.get().delete.return_value = True + mock_installed_apps.return_value = {'foo_app': '/foo/foo_app'} + mock_installed_extensions.return_value = {} + mock_input.side_effect = ['yes'] + mock_popen.side_effect = KeyboardInterrupt + mock_rmtree.side_effect = OSError + mock_os_remove.side_effect = [True, Exception] + mock_join.return_value = '/foo/tethysapp-foo-app-nspkg.pth' + + cmd = tethys_app_uninstall.Command() + cmd.handle(app_or_extension=['tethysapp.foo_app'], is_extension=False) + + mock_installed_apps.assert_called_once() + mock_installed_extensions.assert_not_called() + self.assertIn('successfully uninstalled', mock_stdout.getvalue()) + mock_warnings.assert_not_called() # Don't do the TethysModel.DoesNotExist exception this test + mock_app.objects.get.assert_called() + mock_app.objects.get().delete.assert_called_once() + mock_extension.objects.get.assert_called() + mock_extension.objects.get().delete.assert_not_called() + mock_popen.assert_called_once_with(['pip', 'uninstall', '-y', 'tethysapp-foo_app'], stderr=-2, stdout=-1) + mock_rmtree.assert_called_once_with('/foo/foo_app') + mock_os_remove.assert_any_call('/foo/foo_app') + mock_os_remove.assert_called_with('/foo/tethysapp-foo-app-nspkg.pth') + mock_join.assert_called() + + @mock.patch('warnings.warn') + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.exit') + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.get_installed_tethys_extensions') + @mock.patch('tethys_apps.management.commands.tethys_app_uninstall.get_installed_tethys_apps') + @mock.patch('tethys_apps.models.TethysExtension') + @mock.patch('tethys_apps.models.TethysApp') + def test_tethys_app_uninstall_handle_module_and_db_not_found(self, mock_app, mock_extension, mock_installed_apps, + mock_installed_extensions, mock_stdout, + mock_exit, mock_warn): + # Raise DoesNotExist on db query + mock_app.objects.get.return_value = mock.MagicMock() + mock_app.DoesNotExist = TethysApp.DoesNotExist + mock_extension.DoesNotExist = TethysExtension.DoesNotExist + mock_app.objects.get.side_effect = TethysApp.DoesNotExist + mock_extension.objects.get.return_value = mock.MagicMock() + mock_extension.objects.get.side_effect = TethysExtension.DoesNotExist + + # No installed apps or extensions returned + mock_installed_apps.return_value = {} + mock_installed_extensions.return_value = {} + mock_exit.side_effect = SystemExit + + cmd = tethys_app_uninstall.Command() + self.assertRaises(SystemExit, cmd.handle, app_or_extension=['tethysext.foo_extension'], is_extension=True) + + mock_installed_apps.assert_not_called() + mock_installed_extensions.assert_called() + mock_warn.assert_called_once() diff --git a/tests/unit_tests/test_tethys_apps/test_models/__init__.py b/tests/unit_tests/test_tethys_apps/test_models/__init__.py new file mode 100644 index 000000000..e40fa7488 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/__init__.py @@ -0,0 +1,8 @@ +""" +******************************************************************************** +* Name: __init__.py +* Author: nswain +* Created On: August 15, 2018 +* Copyright: (c) Aquaveo 2018 +******************************************************************************** +""" diff --git a/tests/unit_tests/test_tethys_apps/test_models/test_CustomSetting.py b/tests/unit_tests/test_tethys_apps/test_models/test_CustomSetting.py new file mode 100644 index 000000000..fae379f80 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/test_CustomSetting.py @@ -0,0 +1,109 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_apps.models import TethysApp, CustomSetting +from django.core.exceptions import ValidationError + + +class CustomSettingTests(TethysTestCase): + def set_up(self): + self.test_app = TethysApp.objects.get(package='test_app') + pass + + def tear_down(self): + pass + + def test_clean_empty_validation_error(self): + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = '' + custom_setting.save() + + # Check ValidationError + ret = CustomSetting.objects.get(name='default_name') + + self.assertRaises(ValidationError, ret.clean) + + def test_clean_int_validation_error(self): + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = 'test' + custom_setting.type = 'INTEGER' + custom_setting.save() + + # Check ValidationError + ret = CustomSetting.objects.get(name='default_name') + self.assertRaises(ValidationError, ret.clean) + + def test_clean_float_validation_error(self): + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = 'test' + custom_setting.type = 'FLOAT' + custom_setting.save() + + # Check ValidationError + ret = CustomSetting.objects.get(name='default_name') + self.assertRaises(ValidationError, ret.clean) + + def test_clean_bool_validation_error(self): + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = 'test' + custom_setting.type = 'BOOLEAN' + custom_setting.save() + + # Check ValidationError + ret = CustomSetting.objects.get(name='default_name') + self.assertRaises(ValidationError, ret.clean) + + def test_get_value_empty(self): + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = '' + custom_setting.save() + + self.assertIsNone(CustomSetting.objects.get(name='default_name').get_value()) + + def test_get_value_string(self): + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = 'test_string' + custom_setting.type = 'STRING' + custom_setting.save() + + ret = CustomSetting.objects.get(name='default_name').get_value() + + self.assertEqual('test_string', ret) + + def test_get_value_float(self): + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = '3.14' + custom_setting.type = 'FLOAT' + custom_setting.save() + + ret = CustomSetting.objects.get(name='default_name').get_value() + self.assertEqual(3.14, ret) + + def test_get_value_integer(self): + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = '3' + custom_setting.type = 'INTEGER' + custom_setting.save() + + ret = CustomSetting.objects.get(name='default_name').get_value() + self.assertEqual(3, ret) + + def test_get_value_boolean_true(self): + test_cases = ['true', 'yes', 't', 'y', '1'] + for test in test_cases: + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = test + custom_setting.type = 'BOOLEAN' + custom_setting.save() + + ret = CustomSetting.objects.get(name='default_name').get_value() + self.assertTrue(ret) + + def test_get_value_boolean_false(self): + test_cases = ['false', 'no', 'f', 'n', '0'] + for test in test_cases: + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = test + custom_setting.type = 'BOOLEAN' + custom_setting.save() + + ret = CustomSetting.objects.get(name='default_name').get_value() + self.assertFalse(ret) diff --git a/tests/unit_tests/test_tethys_apps/test_models/test_DatasetServiceSetting.py b/tests/unit_tests/test_tethys_apps/test_models/test_DatasetServiceSetting.py new file mode 100644 index 000000000..51980ef00 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/test_DatasetServiceSetting.py @@ -0,0 +1,70 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_apps.models import TethysApp, DatasetServiceSetting +from django.core.exceptions import ValidationError +from tethys_services.models import DatasetService + + +class DatasetServiceSettingTests(TethysTestCase): + def set_up(self): + self.test_app = TethysApp.objects.get(package='test_app') + + pass + + def tear_down(self): + pass + + def test_clean_empty_validation_error(self): + ds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_ckan') + ds_setting.dataset_service = None + ds_setting.save() + # Check ValidationError + self.assertRaises(ValidationError, DatasetServiceSetting.objects.get(name='primary_ckan').clean) + + def test_get_value_None(self): + ds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_ckan') + ds_setting.dataset_service = None + ds_setting.save() + + ret = DatasetServiceSetting.objects.get(name='primary_ckan').get_value() + self.assertIsNone(ret) + + def test_get_value(self): + ds = DatasetService( + name='test_ds', + endpoint='http://localhost/api/3/action/', + public_endpoint='http://publichost/api/3/action/', + ) + ds.save() + ds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_ckan') + ds_setting.dataset_service = ds + ds_setting.save() + + ret = DatasetServiceSetting.objects.get(name='primary_ckan').get_value() + + self.assertEqual('CKAN', ret.get_engine().type) + self.assertEqual('test_ds', ret.name) + self.assertEqual('http://localhost/api/3/action/', ret.endpoint) + self.assertEqual('http://publichost/api/3/action/', ret.public_endpoint) + + def test_get_value_check_if(self): + ds = DatasetService( + name='test_ds', + endpoint='http://localhost/api/3/action/', + public_endpoint='http://publichost/api/3/action/', + ) + ds.save() + ds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_ckan') + ds_setting.dataset_service = ds + ds_setting.save() + + # Check as_engine + ret = DatasetServiceSetting.objects.get(name='primary_ckan').get_value(as_engine=True) + self.assertEqual('CKAN', ret.type) + + # Check as_enpoint + ret = DatasetServiceSetting.objects.get(name='primary_ckan').get_value(as_endpoint=True) + self.assertEqual('http://localhost/api/3/action/', ret) + + # Check as_public_endpoint + ret = DatasetServiceSetting.objects.get(name='primary_ckan').get_value(as_public_endpoint=True) + self.assertEqual('http://publichost/api/3/action/', ret) diff --git a/tests/unit_tests/test_tethys_apps/test_models/test_PersistentStoreConnectionSetting.py b/tests/unit_tests/test_tethys_apps/test_models/test_PersistentStoreConnectionSetting.py new file mode 100644 index 000000000..48f3d1738 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/test_PersistentStoreConnectionSetting.py @@ -0,0 +1,91 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_apps.models import TethysApp, PersistentStoreConnectionSetting, PersistentStoreService +from django.core.exceptions import ValidationError +from tethys_apps.exceptions import TethysAppSettingNotAssigned +from sqlalchemy.engine.base import Engine +from sqlalchemy.orm.session import sessionmaker + + +class PersistentStoreConnectionSettingTests(TethysTestCase): + def set_up(self): + self.test_app = TethysApp.objects.get(package='test_app') + + self.pss = PersistentStoreService( + name='test_ps', + host='localhost', + port='5432', + username='foo', + password='password' + ) + self.pss.save() + pass + + def tear_down(self): + self.pss.delete() + + def test_clean_empty_validation_error(self): + ps_cs_setting = self.test_app.settings_set.select_subclasses().get(name='primary') + ps_cs_setting.persistent_store_service = None + ps_cs_setting.save() + # Check ValidationError + self.assertRaises(ValidationError, PersistentStoreConnectionSetting.objects.get(name='primary').clean) + + def test_get_value(self): + ps_cs_setting = self.test_app.settings_set.select_subclasses().get(name='primary') + ps_cs_setting.persistent_store_service = self.pss + ps_cs_setting.save() + + # Execute + ret = PersistentStoreConnectionSetting.objects.get(name='primary').get_value() + + # Check if ret is an instance of PersistentStoreService + self.assertIsInstance(ret, PersistentStoreService) + self.assertEqual('test_ps', ret.name) + self.assertEqual('localhost', ret.host) + self.assertEqual(5432, ret.port) + self.assertEqual('foo', ret.username) + self.assertEqual('password', ret.password) + + def test_get_value_none(self): + ps_cs_setting = self.test_app.settings_set.select_subclasses().get(name='primary') + ps_cs_setting.persistent_store_service = None + ps_cs_setting.save() + + # Check TethysAppSettingNotAssigned + self.assertRaises(TethysAppSettingNotAssigned, PersistentStoreConnectionSetting.objects + .get(name='primary').get_value) + + def test_get_value_as_engine(self): + ps_cs_setting = self.test_app.settings_set.select_subclasses().get(name='primary') + ps_cs_setting.persistent_store_service = self.pss + ps_cs_setting.save() + + # Execute + ret = PersistentStoreConnectionSetting.objects.get(name='primary').get_value(as_engine=True) + + # Check if ret is an instance of sqlalchemy Engine + self.assertIsInstance(ret, Engine) + self.assertEqual('postgresql://foo:password@localhost:5432', str(ret.url)) + + def test_get_value_as_sessionmaker(self): + ps_cs_setting = self.test_app.settings_set.select_subclasses().get(name='primary') + ps_cs_setting.persistent_store_service = self.pss + ps_cs_setting.save() + + # Execute + ret = PersistentStoreConnectionSetting.objects.get(name='primary').get_value(as_sessionmaker=True) + + # Check if ret is an instance of sqlalchemy sessionmaker + self.assertIsInstance(ret, sessionmaker) + self.assertEqual('postgresql://foo:password@localhost:5432', str(ret.kw['bind'].url)) + + def test_get_value_as_url(self): + ps_cs_setting = self.test_app.settings_set.select_subclasses().get(name='primary') + ps_cs_setting.persistent_store_service = self.pss + ps_cs_setting.save() + + # Execute + ret = PersistentStoreConnectionSetting.objects.get(name='primary').get_value(as_url=True) + + # Check Url + self.assertEqual('postgresql://foo:password@localhost:5432', str(ret)) diff --git a/tests/unit_tests/test_tethys_apps/test_models/test_PersistentStoreDatabaseSetting.py b/tests/unit_tests/test_tethys_apps/test_models/test_PersistentStoreDatabaseSetting.py new file mode 100644 index 000000000..b10e8aaf5 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/test_PersistentStoreDatabaseSetting.py @@ -0,0 +1,389 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_apps.models import TethysApp, PersistentStoreDatabaseSetting, PersistentStoreService +from django.core.exceptions import ValidationError +from django.conf import settings +from tethys_apps.exceptions import TethysAppSettingNotAssigned, PersistentStorePermissionError,\ + PersistentStoreInitializerError +from sqlalchemy.engine.base import Engine +from sqlalchemy.orm import sessionmaker +import mock + + +class PersistentStoreDatabaseSettingTests(TethysTestCase): + def set_up(self): + self.test_app = TethysApp.objects.get(package='test_app') + + # Get default database connection if there is one + if 'default' in settings.DATABASES: + self.conn = settings.DATABASES['default'] + else: + self.conn = { + 'USER': 'tethys_super', + 'PASSWORD': 'pass', + 'HOST': 'localhost', + 'PORT': '5435' + } + + self.expected_url = 'postgresql://{}:{}@{}:{}'.format( + self.conn['USER'], self.conn['PASSWORD'], self.conn['HOST'], self.conn['PORT'] + ) + + self.pss = PersistentStoreService( + name='test_ps', + host=self.conn['HOST'], + port=self.conn['PORT'], + username=self.conn['USER'], + password=self.conn['PASSWORD'] + ) + + self.pss.save() + + def tear_down(self): + pass + + def test_clean_validation_error(self): + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = None + ps_ds_setting.save() + # Check ValidationError + self.assertRaises(ValidationError, self.test_app.settings_set.select_subclasses().get(name='spatial_db').clean) + + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.create_persistent_store_database') + def test_initialize(self, mock_create): + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = self.pss + ps_ds_setting.save() + + # Execute + self.test_app.settings_set.select_subclasses().get(name='spatial_db').initialize() + + mock_create.assert_called() + + @mock.patch('tethys_apps.models.is_testing_environment') + def test_get_namespaced_persistent_store_name(self, mock_ite): + mock_ite.return_value = False + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = self.pss + ps_ds_setting.save() + + # Execute + ret = self.test_app.settings_set.select_subclasses().get(name='spatial_db').\ + get_namespaced_persistent_store_name() + + # Check result + self.assertEqual('test_app_spatial_db', ret) + + @mock.patch('tethys_apps.models.is_testing_environment') + def test_get_namespaced_persistent_store_name_testing(self, mock_ite): + mock_ite.return_value = True + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = self.pss + ps_ds_setting.save() + + # Execute + ret = self.test_app.settings_set.select_subclasses().get(name='spatial_db').\ + get_namespaced_persistent_store_name() + + # Check result + self.assertEqual('test_app_tethys-testing_spatial_db', ret) + + def test_get_value(self): + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = self.pss + ps_ds_setting.save() + + # Execute + ret = self.test_app.settings_set.select_subclasses().get(name='spatial_db').get_value(with_db=True) + + # Check results + self.assertIsInstance(ret, PersistentStoreService) + self.assertEqual('test_ps', ret.name) + self.assertEqual(self.conn['HOST'], ret.host) + self.assertEqual(int(self.conn['PORT']), ret.port) + self.assertEqual(self.conn['USER'], ret.username) + self.assertEqual(self.conn['PASSWORD'], ret.password) + + def test_get_value_none(self): + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = None + ps_ds_setting.save() + + self.assertRaises(TethysAppSettingNotAssigned, PersistentStoreDatabaseSetting.objects + .get(name='spatial_db').get_value) + + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_namespaced_persistent_store_name') + def test_get_value_with_db(self, mock_gn): + mock_gn.return_value = 'test_database' + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = self.pss + ps_ds_setting.save() + + # Execute + ret = self.test_app.settings_set.select_subclasses().get(name='spatial_db').get_value(with_db=True) + + self.assertIsInstance(ret, PersistentStoreService) + self.assertEqual('test_database', ret.database) + + def test_get_value_as_engine(self): + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = self.pss + ps_ds_setting.save() + + # Execute + ret = self.test_app.settings_set.select_subclasses().get(name='spatial_db').get_value(as_engine=True) + + self.assertIsInstance(ret, Engine) + self.assertEqual(self.expected_url, str(ret.url)) + + def test_get_value_as_sessionmaker(self): + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = self.pss + ps_ds_setting.save() + + # Execute + ret = self.test_app.settings_set.select_subclasses().get(name='spatial_db').get_value(as_sessionmaker=True) + + self.assertIsInstance(ret, sessionmaker) + self.assertEqual(self.expected_url, str(ret.kw['bind'].url)) + + def test_get_value_as_url(self): + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = self.pss + ps_ds_setting.save() + + # Execute + ret = self.test_app.settings_set.select_subclasses().get(name='spatial_db').get_value(as_url=True) + + # check URL + self.assertEqual(self.expected_url, str(ret)) + + def test_persistent_store_database_exists(self): + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = self.pss + ps_ds_setting.save() + + ps_ds_setting.get_namespaced_persistent_store_name = mock.MagicMock(return_value='foo_bar') + ps_ds_setting.get_value = mock.MagicMock() + mock_engine = ps_ds_setting.get_value() + mock_db = mock.MagicMock() + mock_db.name = 'foo_bar' + mock_engine.connect().execute.return_value = [mock_db] + + # Execute + ret = ps_ds_setting.persistent_store_database_exists() + + self.assertTrue(ret) + + def test_persistent_store_database_exists_false(self): + ps_ds_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_ds_setting.persistent_store_service = self.pss + ps_ds_setting.save() + + ps_ds_setting.get_namespaced_persistent_store_name = mock.MagicMock(return_value='foo_bar') + ps_ds_setting.get_value = mock.MagicMock() + mock_engine = ps_ds_setting.get_value() + mock_engine.connect().execute.return_value = [] + + # Execute + ret = ps_ds_setting.persistent_store_database_exists() + + self.assertFalse(ret) + + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.persistent_store_database_exists') + def test_drop_persistent_store_database_not_exists(self, mock_psd): + mock_psd.return_value = False + + # Execute + ret = self.test_app.settings_set.select_subclasses().get(name='spatial_db').drop_persistent_store_database() + + self.assertIsNone(ret) + + @mock.patch('tethys_apps.models.logging') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_value') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.persistent_store_database_exists') + def test_drop_persistent_store_database(self, mock_psd, mock_get, mock_log): + mock_psd.return_value = True + + # Execute + self.test_app.settings_set.select_subclasses().get(name='spatial_db').drop_persistent_store_database() + + # Check + mock_log.getLogger().info.assert_called_with('Dropping database "spatial_db" for app "test_app"...') + mock_get().connect.assert_called() + rts_call_args = mock_get().connect().execute.call_args_list + self.assertEqual('commit', rts_call_args[0][0][0]) + self.assertEqual('DROP DATABASE IF EXISTS "test_app_tethys-testing_spatial_db"', rts_call_args[1][0][0]) + mock_get().connect().close.assert_called() + + @mock.patch('tethys_apps.models.logging') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_value') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.persistent_store_database_exists') + def test_drop_persistent_store_database_exception(self, mock_psd, mock_get, mock_log): + mock_psd.return_value = True + mock_get().connect().execute.side_effect = [Exception('Message: being accessed by other users'), + mock.MagicMock(), mock.MagicMock(), mock.MagicMock()] + + # Execute + self.test_app.settings_set.select_subclasses().get(name='spatial_db').drop_persistent_store_database() + + # Check + mock_log.getLogger().info.assert_called_with('Dropping database "spatial_db" for app "test_app"...') + mock_get().connect.assert_called() + rts_call_args = mock_get().connect().execute.call_args_list + self.assertEqual('commit', rts_call_args[0][0][0]) + self.assertIn('SELECT pg_terminate_backend(pg_stat_activity.pid)', rts_call_args[1][0][0]) + mock_get().connect().close.assert_called() + + @mock.patch('tethys_apps.models.logging') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_value') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.persistent_store_database_exists') + def test_drop_persistent_store_database_connection_exception(self, mock_psd, mock_get, mock_log): + mock_psd.return_value = True + mock_get().connect.side_effect = [Exception('Message: being accessed by other users'), + mock.MagicMock(), mock.MagicMock()] + # Execute + self.test_app.settings_set.select_subclasses().get(name='spatial_db').drop_persistent_store_database() + + # Check + mock_log.getLogger().info.assert_called_with('Dropping database "spatial_db" for app "test_app"...') + mock_get().connect.assert_called() + mock_get().connect().execute.assert_not_called() + mock_get().connect().close.assert_not_called() + + @mock.patch('tethys_apps.models.logging') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_value') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.persistent_store_database_exists') + def test_drop_persistent_store_database_exception_else(self, mock_psd, mock_get, _): + mock_psd.return_value = True + mock_get().connect().execute.side_effect = [Exception('Error Message'), mock.MagicMock()] + + # Execute + self.assertRaises(Exception, PersistentStoreDatabaseSetting.objects. + get(name='spatial_db').drop_persistent_store_database) + + # Check + mock_get().connect().close.assert_called() + + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.drop_persistent_store_database') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_namespaced_persistent_store_name') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.persistent_store_database_exists') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_value') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.initializer_function') + @mock.patch('tethys_apps.models.logging') + def test_create_persistent_store_database(self, mock_log, mock_init, mock_get, mock_ps_de, mock_gn, mock_drop): + # Mock Get Name + mock_gn.return_value = 'spatial_db' + + # Mock Drop Database + mock_drop.return_value = '' + + # Mock persistent_store_database_exists + mock_ps_de.return_value = True + + # Mock get_values + mock_url = mock.MagicMock(username='test_app') + mock_engine = mock.MagicMock() + mock_new_db_engine = mock.MagicMock() + mock_init_param = mock.MagicMock() + mock_get.side_effect = [mock_url, mock_engine, mock_new_db_engine, mock_init_param] + + # Execute + self.test_app.settings_set.select_subclasses().get(name='spatial_db')\ + .create_persistent_store_database(refresh=True, force_first_time=True) + + # Check mock called + rts_get_args = mock_log.getLogger().info.call_args_list + check_log1 = 'Creating database "spatial_db" for app "test_app"...' + check_log2 = 'Enabling PostGIS on database "spatial_db" for app "test_app"...' + check_log3 = 'Initializing database "spatial_db" for app "test_app" ' \ + 'with initializer "appsettings.model.init_spatial_db"...' + self.assertEqual(check_log1, rts_get_args[0][0][0]) + self.assertEqual(check_log2, rts_get_args[1][0][0]) + self.assertEqual(check_log3, rts_get_args[2][0][0]) + mock_init.assert_called() + + @mock.patch('sqlalchemy.exc') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.drop_persistent_store_database') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_namespaced_persistent_store_name') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.persistent_store_database_exists') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_value') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.initializer_function') + @mock.patch('tethys_apps.models.logging') + def test_create_persistent_store_database_perm_error(self, _, __, mock_get, mock_ps_de, mock_gn, mock_drop, mock_e): + # Mock Get Name + mock_gn.return_value = 'spatial_db' + + # Mock Drop Database + mock_drop.return_value = '' + + # Mock persistent_store_database_exists + mock_ps_de.return_value = True + + # Mock get_values + mock_url = mock.MagicMock(username='test_app') + mock_engine = mock.MagicMock() + mock_e.ProgrammingError = Exception + mock_engine.connect().execute.side_effect = [mock.MagicMock(), Exception] + mock_get.side_effect = [mock_url, mock_engine] + + # Execute + self.assertRaises(PersistentStorePermissionError, PersistentStoreDatabaseSetting + .objects.get(name='spatial_db').create_persistent_store_database, refresh=True) + + @mock.patch('sqlalchemy.exc') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.drop_persistent_store_database') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_namespaced_persistent_store_name') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.persistent_store_database_exists') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_value') + @mock.patch('tethys_apps.models.logging') + def test_create_persistent_store_database_ext_perm_error(self, _, mock_get, mock_ps_de, mock_gn, mock_drop, mock_e): + # Mock Get Name + mock_gn.return_value = 'spatial_db' + + # Mock Drop Database + mock_drop.return_value = '' + + # Mock persistent_store_database_exists + mock_ps_de.return_value = True + + # Mock get_values + mock_url = mock.MagicMock(username='test_app') + mock_engine = mock.MagicMock() + mock_e.ProgrammingError = Exception + mock_new_db_engine = mock.MagicMock() + mock_new_db_engine.connect().execute.side_effect = Exception + mock_get.side_effect = [mock_url, mock_engine, mock_new_db_engine] + + # Execute + self.assertRaises(PersistentStorePermissionError, PersistentStoreDatabaseSetting + .objects.get(name='spatial_db').create_persistent_store_database) + + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.drop_persistent_store_database') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_namespaced_persistent_store_name') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.persistent_store_database_exists') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.get_value') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting.initializer_function') + @mock.patch('tethys_apps.models.logging') + def test_create_persistent_store_database_exception(self, _, mock_init, mock_get, mock_ps_de, + mock_gn, mock_drop): + # Mock initializer_function + mock_init.side_effect = Exception('Initializer Error') + # Mock Get Name + mock_gn.return_value = 'spatial_db' + + # Mock Drop Database + mock_drop.return_value = '' + + # Mock persistent_store_database_exists + mock_ps_de.return_value = True + + # Mock get_values + mock_url = mock.MagicMock(username='test_app') + mock_engine = mock.MagicMock() + mock_new_db_engine = mock.MagicMock() + mock_init_param = mock.MagicMock() + mock_get.side_effect = [mock_url, mock_engine, mock_new_db_engine, mock_init_param] + + # Execute + self.assertRaises(PersistentStoreInitializerError, PersistentStoreDatabaseSetting + .objects.get(name='spatial_db').create_persistent_store_database) diff --git a/tests/unit_tests/test_tethys_apps/test_models/test_SpatialDatasetServiceSetting.py b/tests/unit_tests/test_tethys_apps/test_models/test_SpatialDatasetServiceSetting.py new file mode 100644 index 000000000..44445d399 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/test_SpatialDatasetServiceSetting.py @@ -0,0 +1,95 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_apps.models import TethysApp +from django.core.exceptions import ValidationError +from tethys_services.models import SpatialDatasetService + + +class SpatialDatasetServiceTests(TethysTestCase): + def set_up(self): + self.test_app = TethysApp.objects.get(package='test_app') + + pass + + def tear_down(self): + pass + + def test_clean(self): + sds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver') + sds_setting.spatial_dataset_service = None + sds_setting.save() + # Check ValidationError + self.assertRaises(ValidationError, self.test_app.settings_set.select_subclasses(). + get(name='primary_geoserver').clean) + + def test_get_value(self): + sds = SpatialDatasetService( + name='test_sds', + endpoint='http://localhost/geoserver/rest/', + public_endpoint='http://publichost/geoserver/rest/', + apikey='test_api', + username='foo', + password='password', + ) + sds.save() + sds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver') + sds_setting.spatial_dataset_service = sds + sds_setting.save() + + ret = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver').get_value() + + # Check result + self.assertEqual('test_sds', ret.name) + self.assertEqual('http://localhost/geoserver/rest/', ret.endpoint) + self.assertEqual('http://publichost/geoserver/rest/', ret.public_endpoint) + self.assertEqual('test_api', ret.apikey) + self.assertEqual('foo', ret.username) + self.assertEqual('password', ret.password) + + def test_get_value_none(self): + sds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver') + sds_setting.spatial_dataset_service = None + sds_setting.save() + + ret = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver').get_value() + self.assertIsNone(ret) + + def test_get_value_check_if(self): + sds = SpatialDatasetService( + name='test_sds', + endpoint='http://localhost/geoserver/rest/', + public_endpoint='http://publichost/geoserver/rest/', + apikey='test_api', + username='foo', + password='password', + ) + sds.save() + sds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver') + sds_setting.spatial_dataset_service = sds + sds_setting.save() + + # Check as_engine + ret = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver').get_value(as_engine=True) + # Check result + self.assertEqual('GEOSERVER', ret.type) + self.assertEqual('http://localhost/geoserver/rest/', ret.endpoint) + + # Check as wms + ret = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver').get_value(as_wms=True) + # Check result + self.assertEqual('http://localhost/geoserver/wms', ret) + + # Check as wfs + ret = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver').get_value(as_wfs=True) + # Check result + self.assertEqual('http://localhost/geoserver/ows', ret) + + # Check as_endpoint + ret = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver').get_value(as_endpoint=True) + # Check result + self.assertEqual('http://localhost/geoserver/rest/', ret) + + # Check as_endpoint + ret = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver').\ + get_value(as_public_endpoint=True) + # Check result + self.assertEqual('http://publichost/geoserver/rest/', ret) diff --git a/tests/unit_tests/test_tethys_apps/test_models/test_TethysApp.py b/tests/unit_tests/test_tethys_apps/test_models/test_TethysApp.py new file mode 100644 index 000000000..7942d2aa9 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/test_TethysApp.py @@ -0,0 +1,287 @@ +""" +******************************************************************************** +* Name: test_TethysApp +* Author: nswain +* Created On: August 15, 2018 +* Copyright: (c) Aquaveo 2018 +******************************************************************************** +""" +from tethys_sdk.testing import TethysTestCase +from tethys_apps.models import TethysApp, TethysAppSetting +from tethys_services.models import PersistentStoreService, SpatialDatasetService, DatasetService, WebProcessingService + + +class TethysAppTests(TethysTestCase): + + def set_up(self): + self.test_app = TethysApp.objects.get(package='test_app') + self.wps = WebProcessingService( + name='test_wps', + endpoint='http://localhost/wps/WebProcessingService', + username='foo', + password='password' + + ) + self.wps.save() + + self.sds = SpatialDatasetService( + name='test_sds', + endpoint='http://localhost/geoserver/rest/', + username='foo', + password='password' + ) + self.sds.save() + + self.ds = DatasetService( + name='test_ds', + endpoint='http://localhost/api/3/action/', + apikey='foo', + ) + self.ds.save() + + self.ps = PersistentStoreService( + name='test_ps', + host='localhost', + port='5432', + username='foo', + password='password' + ) + self.ps.save() + + def tear_down(self): + self.wps.delete() + self.ps.delete() + self.ds.delete() + self.sds.delete() + + def test_str(self): + ret = str(self.test_app) + self.assertEqual('Test App', ret) + + def test_add_settings(self): + new_setting = TethysAppSetting( + name='new_setting', + required=False + ) + + self.test_app.add_settings([new_setting]) + + app = TethysApp.objects.get(package='test_app') + settings = app.settings_set.filter(name='new_setting') + self.assertEqual(1, len(settings)) + + def test_add_settings_add_same_setting_twice(self): + new_setting = TethysAppSetting( + name='new_setting', + required=False + ) + new_setting_same_name = TethysAppSetting( + name='new_setting', + required=False + ) + + self.test_app.add_settings([new_setting, new_setting_same_name]) + + app = TethysApp.objects.get(package='test_app') + settings = app.settings_set.filter(name='new_setting') + self.assertEqual(1, len(settings)) + + def test_settings_prop(self): + ret = self.test_app.settings + self.assertEqual(12, len(ret)) + + for r in ret: + self.assertIsInstance(r, TethysAppSetting) + + def test_custom_settings_prop(self): + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = 'foo' + custom_setting.save() + + ret = self.test_app.custom_settings + + for r in ret: + self.assertIsInstance(r, TethysAppSetting) + if r.name == 'default_name': + self.assertEqual('foo', r.value) + + def test_dataset_service_settings_prop(self): + ds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_ckan') + ds_setting.dataset_service = self.ds + ds_setting.save() + + ret = self.test_app.dataset_service_settings + + for r in ret: + self.assertIsInstance(r, TethysAppSetting) + if r.name == 'primary_ckan': + self.assertEqual('test_ds', r.dataset_service.name) + self.assertEqual('foo', r.dataset_service.apikey) + self.assertEqual('http://localhost/api/3/action/', r.dataset_service.endpoint) + + def test_spatial_dataset_service_settings_prop(self): + sds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver') + sds_setting.spatial_dataset_service = self.sds + sds_setting.save() + + ret = self.test_app.spatial_dataset_service_settings + + for r in ret: + self.assertIsInstance(r, TethysAppSetting) + if r.name == 'primary_geoserver': + self.assertEqual('test_sds', r.spatial_dataset_service.name) + self.assertEqual('http://localhost/geoserver/rest/', r.spatial_dataset_service.endpoint) + self.assertEqual('foo', r.spatial_dataset_service.username) + self.assertEqual('password', r.spatial_dataset_service.password) + + def test_wps_services_settings_prop(self): + wps_setting = self.test_app.settings_set.select_subclasses().get(name='primary_52n') + wps_setting.web_processing_service = self.wps + wps_setting.save() + + ret = self.test_app.wps_services_settings + + for r in ret: + self.assertIsInstance(r, TethysAppSetting) + if r.name == 'primary_52n': + self.assertEqual('test_wps', r.web_processing_service.name) + self.assertEqual('http://localhost/wps/WebProcessingService', r.web_processing_service.endpoint) + self.assertEqual('foo', r.web_processing_service.username) + self.assertEqual('password', r.web_processing_service.password) + + def test_persistent_store_connection_settings_prop(self): + ps_setting = self.test_app.settings_set.select_subclasses().get(name='primary') + ps_setting.persistent_store_service = self.ps + ps_setting.save() + + ret = self.test_app.persistent_store_connection_settings + + for r in ret: + self.assertIsInstance(r, TethysAppSetting) + if r.name == 'primary': + self.assertEqual('test_ps', r.persistent_store_service.name) + self.assertEqual('localhost', r.persistent_store_service.host) + self.assertEqual(5432, r.persistent_store_service.port) + self.assertEqual('foo', r.persistent_store_service.username) + self.assertEqual('password', r.persistent_store_service.password) + + def test_persistent_store_database_settings_prop(self): + ps_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_setting.persistent_store_service = self.ps + ps_setting.save() + + ret = self.test_app.persistent_store_database_settings + + for r in ret: + self.assertIsInstance(r, TethysAppSetting) + if r.name == 'spatial_db': + self.assertEqual('test_ps', r.persistent_store_service.name) + self.assertEqual('localhost', r.persistent_store_service.host) + self.assertEqual(5432, r.persistent_store_service.port) + self.assertEqual('foo', r.persistent_store_service.username) + self.assertEqual('password', r.persistent_store_service.password) + + def test_configured_prop_required_and_set(self): + # See: test_app.app for expected settings configuration + # Set required settings + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = 'foo' + custom_setting.save() + + ds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_ckan') + ds_setting.dataset_service = self.ds + ds_setting.save() + + sds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver') + sds_setting.spatial_dataset_service = self.sds + sds_setting.save() + + wps_setting = self.test_app.settings_set.select_subclasses().get(name='primary_52n') + wps_setting.web_processing_service = self.wps + wps_setting.save() + + ps_setting = self.test_app.settings_set.select_subclasses().get(name='primary') + ps_setting.persistent_store_service = self.ps + ps_setting.save() + + ps_db_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + ps_db_setting.persistent_store_service = self.ps + ps_db_setting.save() + + ret = self.test_app.configured + + self.assertTrue(ret) + + def test_configured_prop_required_no_value(self): + # See: test_app.app for expected settings configuration + # Set required settings + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = '' # <-- NOT SET / NO VALUE + custom_setting.save() + + ds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_ckan') + ds_setting.dataset_service = self.ds + ds_setting.save() + + sds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver') + sds_setting.spatial_dataset_service = self.sds + sds_setting.save() + + wps_setting = self.test_app.settings_set.select_subclasses().get(name='primary_52n') + wps_setting.web_processing_service = self.wps + wps_setting.save() + + ps_setting = self.test_app.settings_set.select_subclasses().get(name='primary') + ps_setting.persistent_store_service = self.ps + ps_setting.save() + + psd_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + psd_setting.persistent_store_service = self.ps + psd_setting.save() + + ret = self.test_app.configured + self.assertFalse(ret) + + def test_configured_prop_not_assigned_exception(self): + # See: test_app.app for expected settings configuration + custom_setting = self.test_app.settings_set.select_subclasses().get(name='default_name') + custom_setting.value = '' + custom_setting.save() + + ds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_ckan') + ds_setting.dataset_service = None + ds_setting.save() + + sds_setting = self.test_app.settings_set.select_subclasses().get(name='primary_geoserver') + sds_setting.spatial_dataset_service = None + sds_setting.save() + + wps_setting = self.test_app.settings_set.select_subclasses().get(name='primary_52n') + wps_setting.web_processing_service = None + wps_setting.save() + + ps_setting = self.test_app.settings_set.select_subclasses().get(name='primary') + ps_setting.persistent_store_service = None + ps_setting.save() + + psd_setting = self.test_app.settings_set.select_subclasses().get(name='spatial_db') + psd_setting.persistent_store_service = None + psd_setting.save() + + ret = self.test_app.configured + + self.assertFalse(ret) + + +class TethysAppNoSettingsTests(TethysTestCase): + + def set_up(self): + self.test_app = TethysApp.objects.get(package='test_app') + + # See: test_app.app for expected settings configuration + for setting in self.test_app.settings_set.all(): + setting.delete() + + def test_configured_prop_no_settings(self): + ret = self.test_app.configured + self.assertTrue(ret) diff --git a/tests/unit_tests/test_tethys_apps/test_models/test_TethysAppSetting.py b/tests/unit_tests/test_tethys_apps/test_models/test_TethysAppSetting.py new file mode 100644 index 000000000..7525396d0 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/test_TethysAppSetting.py @@ -0,0 +1,30 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_apps.models import TethysAppSetting +import mock + + +class TethysAppSettingTests(TethysTestCase): + def set_up(self): + self.test_app_setting = TethysAppSetting.objects.get(name='primary_ckan') + + def tear_down(self): + pass + + def test_str(self): + ret = str(self.test_app_setting) + self.assertEqual('primary_ckan', ret) + + @mock.patch('tethys_apps.models.TethysFunctionExtractor') + def test_initializer_function_prop(self, mock_tfe): + mock_tfe.return_value = mock.MagicMock(function='test_function') + ret = self.test_app_setting.initializer_function + + self.assertEqual('test_function', ret) + + @mock.patch('tethys_apps.models.TethysAppSetting.initializer_function') + def test_initialize(self, mock_if): + self.test_app_setting.initialize() + mock_if.assert_called_with(False) + + def test_get_value(self): + self.assertRaises(NotImplementedError, self.test_app_setting.get_value, 'test') diff --git a/tests/unit_tests/test_tethys_apps/test_models/test_TethysExtension.py b/tests/unit_tests/test_tethys_apps/test_models/test_TethysExtension.py new file mode 100644 index 000000000..81b1639c9 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/test_TethysExtension.py @@ -0,0 +1,15 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_apps.models import TethysExtension + + +class TethysExtensionTests(TethysTestCase): + + def set_up(self): + self.test_ext = TethysExtension.objects.get(package='test_extension') + + def tear_down(self): + pass + + def test_str(self): + ret = str(self.test_ext) + self.assertEqual('Test Extension', ret) diff --git a/tests/unit_tests/test_tethys_apps/test_models/test_WebProcessingServiceSetting.py b/tests/unit_tests/test_tethys_apps/test_models/test_WebProcessingServiceSetting.py new file mode 100644 index 000000000..2c02785a3 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/test_WebProcessingServiceSetting.py @@ -0,0 +1,60 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_apps.models import TethysApp, WebProcessingServiceSetting +from django.core.exceptions import ValidationError +import mock + + +class WebProcessingServiceSettingTests(TethysTestCase): + def set_up(self): + self.test_app = TethysApp.objects.get(package='test_app') + + pass + + def tear_down(self): + pass + + def test_clean_empty_validation_error(self): + wps_setting = self.test_app.settings_set.select_subclasses().get(name='primary_52n') + wps_setting.web_processing_service = None + wps_setting.save() + # Check ValidationError + self.assertRaises(ValidationError, WebProcessingServiceSetting.objects.get(name='primary_52n').clean) + + def test_get_value_None(self): + wps_setting = self.test_app.settings_set.select_subclasses().get(name='primary_52n') + wps_setting.web_processing_service = None + wps_setting.save() + + ret = WebProcessingServiceSetting.objects.get(name='primary_52n').get_value() + self.assertIsNone(ret) + + @mock.patch('tethys_apps.models.WebProcessingServiceSetting.web_processing_service') + def test_get_value(self, mock_wps): + mock_wps.get_engine.return_value = 'test_wps_engine' + mock_wps.endpoint = 'test_endpoint' + mock_wps.public_endpoint = 'test_public_endpoint' + mock_wps.name = 'test_wps_name' + + ret = WebProcessingServiceSetting.objects.get(name='primary_52n').get_value() + self.assertEqual('test_wps_engine', ret.get_engine()) + self.assertEqual('test_wps_name', ret.name) + self.assertEqual('test_endpoint', ret.endpoint) + self.assertEqual('test_public_endpoint', ret.public_endpoint) + + @mock.patch('tethys_apps.models.WebProcessingServiceSetting.web_processing_service') + def test_get_value_check_if(self, mock_wps): + mock_wps.get_engine.return_value = 'test_wps_engine' + mock_wps.endpoint = 'test_endpoint' + mock_wps.public_endpoint = 'test_public_endpoint' + + # Check if as_engine + ret = WebProcessingServiceSetting.objects.get(name='primary_52n').get_value(as_engine=True) + self.assertEqual('test_wps_engine', ret) + + # Check if as_endpoint + ret = WebProcessingServiceSetting.objects.get(name='primary_52n').get_value(as_endpoint=True) + self.assertEqual('test_endpoint', ret) + + # Check if as_public_endpoint + ret = WebProcessingServiceSetting.objects.get(name='primary_52n').get_value(as_public_endpoint=True) + self.assertEqual('test_public_endpoint', ret) diff --git a/tests/unit_tests/test_tethys_apps/test_models/tests_models.py b/tests/unit_tests/test_tethys_apps/test_models/tests_models.py new file mode 100644 index 000000000..04bb0deb1 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_models/tests_models.py @@ -0,0 +1,38 @@ +""" +******************************************************************************** +* Name: tests_models.py +* Author: nswain +* Created On: August 29, 2018 +* Copyright: (c) Aquaveo 2018 +******************************************************************************** +""" +# import unittest +# import mock +# import tethys_apps.models +# from __builtin__ import __import__ as real_import +# +# +# def mock_import(name, globals={}, locals={}, fromlist=[], level=-1): +# if name == 'tethys_services.models' and len(fromlist) == 4: +# raise RuntimeError +# return real_import(name, globals, locals, fromlist, level) +# +# +# class ModelsTests(unittest.TestCase): +# def setUp(self): +# pass +# +# def tearDown(self): +# pass +# +# @mock.patch('__builtin__.__import__', side_effect=mock_import) +# @mock.patch('tethys_apps.models.log') +# def test_service_models_import_error(self, mock_log, _): +# # mock_log.exception.side_effect = SystemExit +# tethys_apps.models.logging = mock.MagicMock +# try: +# reload(tethys_apps.models) +# except SystemExit: +# pass +# +# # mock_log.exception.assert_called_with('An error occurred while trying to import tethys service models.') diff --git a/tests/unit_tests/test_tethys_apps/test_static_finders.py b/tests/unit_tests/test_tethys_apps/test_static_finders.py new file mode 100644 index 000000000..2ad749ae0 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_static_finders.py @@ -0,0 +1,66 @@ +import os +import unittest +from tethys_apps.static_finders import TethysStaticFinder + + +class TestTethysStaticFinder(unittest.TestCase): + def setUp(self): + self.src_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + self.root = os.path.join(self.src_dir, 'tethys_apps', 'tethysapp', 'test_app', 'public') + + def tearDown(self): + pass + + def test_init(self): + pass + + def test_find(self): + tethys_static_finder = TethysStaticFinder() + path = 'test_app/css/main.css' + ret = tethys_static_finder.find(path) + self.assertEqual(os.path.join(self.root, 'css/main.css'), ret) + + def test_find_all(self): + tethys_static_finder = TethysStaticFinder() + path = 'test_app/css/main.css' + ret = tethys_static_finder.find(path, all=True) + self.assertIn(os.path.join(self.root, 'css/main.css'), ret) + + def test_find_location_with_no_prefix(self): + prefix = None + path = 'css/main.css' + + tethys_static_finder = TethysStaticFinder() + ret = tethys_static_finder.find_location(self.root, path, prefix) + + self.assertEqual(os.path.join(self.root, path), ret) + + def test_find_location_with_prefix_not_in_path(self): + prefix = 'tethys_app' + path = 'css/main.css' + + tethys_static_finder = TethysStaticFinder() + ret = tethys_static_finder.find_location(self.root, path, prefix) + + self.assertIsNone(ret) + + def test_find_location_with_prefix_in_path(self): + prefix = 'tethys_app' + path = 'tethys_app/css/main.css' + + tethys_static_finder = TethysStaticFinder() + ret = tethys_static_finder.find_location(self.root, path, prefix) + + self.assertEqual(os.path.join(self.root, 'css/main.css'), ret) + + def test_list(self): + tethys_static_finder = TethysStaticFinder() + expected_ignore_patterns = '' + expected_app_paths = [] + for path, storage in tethys_static_finder.list(expected_ignore_patterns): + if 'test_app' in storage.location: + expected_app_paths.append(path) + + self.assertIn('js/main.js', expected_app_paths) + self.assertIn('images/icon.gif', expected_app_paths) + self.assertIn('css/main.css', expected_app_paths) diff --git a/tests/unit_tests/test_tethys_apps/test_template_loaders.py b/tests/unit_tests/test_tethys_apps/test_template_loaders.py new file mode 100644 index 000000000..145045261 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_template_loaders.py @@ -0,0 +1,76 @@ +import unittest +import mock +import errno + +from django.template import TemplateDoesNotExist +from tethys_apps.template_loaders import TethysTemplateLoader + + +class TestTethysTemplateLoader(unittest.TestCase): + def setUp(self): + self.mock_engine = mock.MagicMock() + + def tearDown(self): + pass + + @mock.patch('tethys_apps.template_loaders.io.open', new_callable=mock.mock_open) + @mock.patch('tethys_apps.template_loaders.BaseLoader') + def test_get_contents(self, _, mock_file): + handlers = (mock.mock_open(read_data='mytemplate').return_value, mock_file.return_value) + mock_file.side_effect = handlers + origin = mock.MagicMock(name='test_app/css/main.css') + + tethys_template_loader = TethysTemplateLoader(self.mock_engine) + + ret = tethys_template_loader.get_contents(origin) + self.assertIn('mytemplate', ret) + mock_file.assert_called_once() + + @mock.patch('tethys_apps.template_loaders.io.open') + @mock.patch('tethys_apps.template_loaders.BaseLoader') + def test_get_contents_io_error(self, _, mock_file): + mock_file.side_effect = IOError + origin = mock.MagicMock(name='test_app/css/main.css') + + tethys_template_loader = TethysTemplateLoader(self.mock_engine) + + self.assertRaises(IOError, tethys_template_loader.get_contents, origin) + mock_file.assert_called_once() + + @mock.patch('tethys_apps.template_loaders.io.open', side_effect=IOError(errno.ENOENT, 'foo')) + @mock.patch('tethys_apps.template_loaders.BaseLoader') + def test_get_contents_template_does_not_exist(self, _, mock_file): + origin = mock.MagicMock(name='test_app/css/main.css') + + tethys_template_loader = TethysTemplateLoader(self.mock_engine) + + self.assertRaises(TemplateDoesNotExist, tethys_template_loader.get_contents, origin) + mock_file.assert_called_once() + + @mock.patch('tethys_apps.template_loaders.BaseLoader') + @mock.patch('tethys_apps.template_loaders.get_directories_in_tethys') + def test_get_template_sources(self, mock_gdt, _): + tethys_template_loader = TethysTemplateLoader(self.mock_engine) + mock_gdt.return_value = ['/foo/template1'] + expected_template_name = 'foo' + + for origin in tethys_template_loader.get_template_sources(expected_template_name): + self.assertEquals('/foo/template1/foo', origin.name) + self.assertEquals('foo', origin.template_name) + self.assertTrue(isinstance(origin.loader, TethysTemplateLoader)) + + @mock.patch('tethys_apps.template_loaders.safe_join') + @mock.patch('tethys_apps.template_loaders.BaseLoader') + @mock.patch('tethys_apps.template_loaders.get_directories_in_tethys') + def test_get_template_sources_exception(self, mock_gdt, _, mock_safe_join): + from django.core.exceptions import SuspiciousFileOperation + + tethys_template_loader = TethysTemplateLoader(self.mock_engine) + mock_gdt.return_value = ['/foo/template1', '/foo/template2'] + mock_safe_join.side_effect = [SuspiciousFileOperation, '/foo/template2/foo'] + expected_template_name = 'foo' + + for origin in tethys_template_loader.get_template_sources(expected_template_name): + self.assertEquals('/foo/template2/foo', origin.name) + self.assertEquals('foo', origin.template_name) + self.assertTrue(isinstance(origin.loader, TethysTemplateLoader)) diff --git a/tests/unit_tests/test_tethys_apps/test_templatetags/__init__.py b/tests/unit_tests/test_tethys_apps/test_templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_apps/test_templatetags/test_tags.py b/tests/unit_tests/test_tethys_apps/test_templatetags/test_tags.py new file mode 100644 index 000000000..75dd696e9 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_templatetags/test_tags.py @@ -0,0 +1,26 @@ +import unittest +import mock +from tethys_apps.templatetags import tags as t + + +class TestTags(unittest.TestCase): + def setUp(self): + # app_list + self.app_names = ['app1', 'app2', 'app3', 'app4', 'app5', 'app6'] + self.tag_names = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6'] + self.mock_apps = [] + for i, ap in enumerate(self.app_names): + mock_app = mock.MagicMock(tags=','.join(self.tag_names[:i+1])) + mock_app.name = ap + self.mock_apps.append(mock_app) + + def tearDown(self): + pass + + def test_get_tags_from_apps(self): + ret_tag_list = t.get_tags_from_apps(self.mock_apps) + self.assertEqual(sorted(self.tag_names), sorted(ret_tag_list)) + + def test_get_tag_class(self): + ret_tag_list = t.get_tag_class(self.mock_apps[-1]) + self.assertEqual(' '.join(sorted(self.tag_names)), ret_tag_list) diff --git a/tests/unit_tests/test_tethys_apps/test_urls.py b/tests/unit_tests/test_tethys_apps/test_urls.py new file mode 100644 index 000000000..3903bce36 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_urls.py @@ -0,0 +1,43 @@ +from django.urls import reverse, resolve +from tethys_sdk.testing import TethysTestCase + + +class TestUrls(TethysTestCase): + + def set_up(self): + pass + + def tear_down(self): + pass + + def test_urls(self): + # This executes the code at the module level + url = reverse('app_library') + resolver = resolve(url) + self.assertEqual('/apps/', url) + self.assertEqual('library', resolver.func.__name__) + self.assertEqual('tethys_apps.views', resolver.func.__module__) + + url = reverse('send_beta_feedback') + resolver = resolve(url) + self.assertEqual('/apps/send-beta-feedback/', url) + self.assertEquals('send_beta_feedback_email', resolver.func.__name__) + self.assertEqual('tethys_apps.views', resolver.func.__module__) + + url = reverse('test_app:home', args=['foo', 'bar']) + resolver = resolve(url) + self.assertEqual('/apps/test-app/foo/bar/', url) + self.assertEquals('home', resolver.func.__name__) + self.assertEqual('tethys_apps.tethysapp.test_app.controllers', resolver.func.__module__) + + url = reverse('test_app:home', kwargs={'var1': 'foo', 'var2': 'bar'}) + resolver = resolve(url) + self.assertEqual('/apps/test-app/foo/bar/', url) + self.assertEquals('home', resolver.func.__name__) + self.assertEqual('tethys_apps.tethysapp.test_app.controllers', resolver.func.__module__) + + url = reverse('test_extension:home', args=['foo', 'bar']) + resolver = resolve(url) + self.assertEqual('/extensions/test-extension/foo/bar/', url) + self.assertEquals('home', resolver.func.__name__) + self.assertEqual('tethysext.test_extension.controllers', resolver.func.__module__) diff --git a/tests/unit_tests/test_tethys_apps/test_utilities.py b/tests/unit_tests/test_tethys_apps/test_utilities.py new file mode 100644 index 000000000..3f0e304d2 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_utilities.py @@ -0,0 +1,483 @@ +import unittest +import mock + +from tethys_apps import utilities + + +class TethysAppsUtilitiesTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_get_directories_in_tethys_templates(self): + # Get the templates directories for the test_app and test_extension + result = utilities.get_directories_in_tethys(('templates',)) + self.assertGreaterEqual(len(result), 2) + + test_app = False + test_ext = False + + for r in result: + if '/tethysapp/test_app/templates' in r: + test_app = True + if '/tethysext-test_extension/tethysext/test_extension/templates' in r: + test_ext = True + + self.assertTrue(test_app) + self.assertTrue(test_ext) + + def test_get_directories_in_tethys_templates_with_app_name(self): + # Get the templates directories for the test_app and test_extension + # Use the with_app_name argument, so that the app and extension names appear in the result + result = utilities.get_directories_in_tethys(('templates',), with_app_name=True) + self.assertGreaterEqual(len(result), 2) + self.assertEqual(2, len(result[0])) + self.assertEqual(2, len(result[1])) + + test_app = False + test_ext = False + + for r in result: + if 'test_app' in r and '/tethysapp/test_app/templates' in r[1]: + test_app = True + if 'test_extension' in r and '/tethysext-test_extension/tethysext/test_extension/templates' in r[1]: + test_ext = True + + self.assertTrue(test_app) + self.assertTrue(test_ext) + + @mock.patch('tethys_apps.utilities.SingletonHarvester') + def test_get_directories_in_tethys_templates_extension_import_error(self, mock_harvester): + # Mock the extension_modules variable with bad data, to throw an ImportError + mock_harvester().extension_modules = {'foo_invalid_foo': 'tethysext.foo_invalid_foo'} + + result = utilities.get_directories_in_tethys(('templates',)) + self.assertGreaterEqual(len(result), 1) + + test_app = False + test_ext = False + + for r in result: + if '/tethysapp/test_app/templates' in r: + test_app = True + if '/tethysext-test_extension/tethysext/test_extension/templates' in r: + test_ext = True + + self.assertTrue(test_app) + self.assertFalse(test_ext) + + def test_get_directories_in_tethys_foo(self): + # Get the foo directories for the test_app and test_extension + # foo doesn't exist + result = utilities.get_directories_in_tethys(('foo',)) + self.assertEqual(0, len(result)) + + def test_get_directories_in_tethys_foo_public(self): + # Get the foo and public directories for the test_app and test_extension + # foo doesn't exist, but public will + result = utilities.get_directories_in_tethys(('foo', 'public')) + self.assertGreaterEqual(len(result), 2) + + test_app = False + test_ext = False + + for r in result: + if '/tethysapp/test_app/public' in r: + test_app = True + if '/tethysext-test_extension/tethysext/test_extension/public' in r: + test_ext = True + + self.assertTrue(test_app) + self.assertTrue(test_ext) + + def test_get_active_app_none_none(self): + # Get the active TethysApp object, with a request of None and url of None + result = utilities.get_active_app(request=None, url=None) + self.assertEqual(None, result) + + # Try again with the defaults, which are a request of None and url of None + result = utilities.get_active_app() + self.assertEqual(None, result) + + @mock.patch('tethys_apps.models.TethysApp') + def test_get_active_app_request(self, mock_app): + # Mock up for TethysApp, and request + mock_app.objects.get.return_value = mock.MagicMock() + mock_request = mock.MagicMock() + mock_request.path = '/apps/foo/bar' + + # Result should be mock for mock_app.objects.get.return_value + result = utilities.get_active_app(request=mock_request) + self.assertEqual(mock_app.objects.get(), result) + + @mock.patch('tethys_apps.models.TethysApp') + def test_get_active_app_url(self, mock_app): + # Mock up for TethysApp + mock_app.objects.get.return_value = mock.MagicMock() + + # Result should be mock for mock_app.objects.get.return_value + result = utilities.get_active_app(url='/apps/foo/bar') + self.assertEqual(mock_app.objects.get(), result) + + @mock.patch('tethys_apps.models.TethysApp') + def test_get_active_app_request_bad_path(self, mock_app): + # Mock up for TethysApp + mock_app.objects.get.return_value = mock.MagicMock() + mock_request = mock.MagicMock() + # Path does not contain apps + mock_request.path = '/foo/bar' + + # Because 'app' not in request path, return None + result = utilities.get_active_app(request=mock_request) + self.assertEqual(None, result) + + @mock.patch('tethys_apps.utilities.tethys_log.warning') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_active_app_request_exception1(self, mock_app, mock_log_warning): + from django.core.exceptions import ObjectDoesNotExist + + # Mock up for TethysApp to raise exception, and request + mock_app.objects.get.side_effect = ObjectDoesNotExist + mock_request = mock.MagicMock() + mock_request.path = '/apps/foo/bar' + + # Result should be None due to the exception + result = utilities.get_active_app(request=mock_request) + self.assertEqual(None, result) + mock_log_warning.assert_called_once_with('Could not locate app with root url "foo".') + + @mock.patch('tethys_apps.utilities.tethys_log.warning') + @mock.patch('tethys_apps.models.TethysApp') + def test_get_active_app_request_exception2(self, mock_app, mock_log_warning): + from django.core.exceptions import MultipleObjectsReturned + + # Mock up for TethysApp to raise exception, and request + mock_app.objects.get.side_effect = MultipleObjectsReturned + mock_request = mock.MagicMock() + mock_request.path = '/apps/foo/bar' + + # Result should be None due to the exception + result = utilities.get_active_app(request=mock_request) + self.assertEqual(None, result) + mock_log_warning.assert_called_once_with('Multiple apps found with root url "foo".') + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_apps.models.TethysApp') + def test_create_ps_database_setting_app_does_not_exist(self, mock_app, mock_pretty_output): + from django.core.exceptions import ObjectDoesNotExist + + # Mock up for TethysApp to not exist + mock_app.objects.get.side_effect = ObjectDoesNotExist + mock_app_package = mock.MagicMock() + mock_name = mock.MagicMock() + + # ObjectDoesNotExist should be thrown, and False returned + result = utilities.create_ps_database_setting(app_package=mock_app_package, name=mock_name) + + self.assertEqual(False, result) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('A Tethys App with the name', po_call_args[0][0][0]) + self.assertIn('does not exist. Aborted.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting') + @mock.patch('tethys_apps.models.TethysApp') + def test_create_ps_database_setting_ps_database_setting_exists(self, mock_app, mock_ps_db_setting, + mock_pretty_output): + # Mock up for TethysApp and PersistentStoreDatabaseSetting to exist + mock_app.objects.get.return_value = mock.MagicMock() + mock_ps_db_setting.objects.get.return_value = mock.MagicMock() + mock_app_package = mock.MagicMock() + mock_name = mock.MagicMock() + + # PersistentStoreDatabaseSetting should exist, and False returned + result = utilities.create_ps_database_setting(app_package=mock_app_package, name=mock_name) + + self.assertEqual(False, result) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('A PersistentStoreDatabaseSetting with name', po_call_args[0][0][0]) + self.assertIn('already exists. Aborted.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.utilities.print') + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting') + @mock.patch('tethys_apps.models.TethysApp') + def test_create_ps_database_setting_ps_database_setting_exceptions(self, mock_app, mock_ps_db_setting, + mock_pretty_output, mock_print): + from django.core.exceptions import ObjectDoesNotExist + + # Mock up for TethysApp to exist and PersistentStoreDatabaseSetting to throw exceptions + mock_app.objects.get.return_value = mock.MagicMock() + mock_ps_db_setting.objects.get.side_effect = ObjectDoesNotExist + mock_ps_db_setting().save.side_effect = Exception('foo exception') + mock_app_package = mock.MagicMock() + mock_name = mock.MagicMock() + + # PersistentStoreDatabaseSetting should exist, and False returned + result = utilities.create_ps_database_setting(app_package=mock_app_package, name=mock_name) + + self.assertEqual(False, result) + mock_ps_db_setting.assert_called() + mock_ps_db_setting().save.assert_called() + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('The above error was encountered. Aborted.', po_call_args[0][0][0]) + rts_call_args = mock_print.call_args_list + self.assertIn('foo exception', rts_call_args[0][0][0].args[0]) + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting') + @mock.patch('tethys_apps.models.TethysApp') + def test_create_ps_database_setting_ps_database_savess(self, mock_app, mock_ps_db_setting, mock_pretty_output): + # Mock up for TethysApp to exist and PersistentStoreDatabaseSetting to not + mock_app.objects.get.return_value = mock.MagicMock() + mock_ps_db_setting.objects.get.return_value = False + mock_ps_db_setting().save.return_value = True + mock_app_package = mock.MagicMock() + mock_name = mock.MagicMock() + + # True should be returned + result = utilities.create_ps_database_setting(app_package=mock_app_package, name=mock_name) + + self.assertEqual(True, result) + mock_ps_db_setting.assert_called() + mock_ps_db_setting().save.assert_called() + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('PersistentStoreDatabaseSetting named', po_call_args[0][0][0]) + self.assertIn('created successfully!', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_apps.models.TethysApp') + def test_remove_ps_database_setting_app_not_exist(self, mock_app, mock_pretty_output): + from django.core.exceptions import ObjectDoesNotExist + + # Mock up for TethysApp to throw an exception + mock_app.objects.get.side_effect = ObjectDoesNotExist + mock_app_package = mock.MagicMock() + mock_name = mock.MagicMock() + + # An exception will be thrown and false returned + result = utilities.remove_ps_database_setting(app_package=mock_app_package, name=mock_name) + + self.assertEqual(False, result) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('A Tethys App with the name', po_call_args[0][0][0]) + self.assertIn('does not exist. Aborted.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting') + @mock.patch('tethys_apps.models.TethysApp') + def test_remove_ps_database_setting_psdbs_not_exist(self, mock_app, mock_ps_db_setting, mock_pretty_output): + from django.core.exceptions import ObjectDoesNotExist + + # Mock up for TethysApp and PersistentStoreDatabaseSetting to throw an exception + mock_app.objects.get.return_value = mock.MagicMock() + mock_ps_db_setting.objects.get.side_effect = ObjectDoesNotExist + mock_app_package = mock.MagicMock() + mock_name = mock.MagicMock() + + # An exception will be thrown and false returned + result = utilities.remove_ps_database_setting(app_package=mock_app_package, name=mock_name) + + self.assertEqual(False, result) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('An PersistentStoreDatabaseSetting with the name', po_call_args[0][0][0]) + self.assertIn(' for app ', po_call_args[0][0][0]) + self.assertIn('does not exist. Aborted.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting') + @mock.patch('tethys_apps.models.TethysApp') + def test_remove_ps_database_setting_force_delete(self, mock_app, mock_ps_db_setting, mock_pretty_output): + # Mock up for TethysApp and PersistentStoreDatabaseSetting + mock_app.objects.get.return_value = mock.MagicMock() + mock_ps_db_setting.objects.get.return_value = mock.MagicMock() + mock_ps_db_setting.objects.get().delete.return_value = True + mock_app_package = mock.MagicMock() + mock_name = mock.MagicMock() + + # Delete will be called and True returned + result = utilities.remove_ps_database_setting(app_package=mock_app_package, name=mock_name, force=True) + + self.assertEqual(True, result) + mock_ps_db_setting.objects.get().delete.assert_called_once() + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Successfully removed PersistentStoreDatabaseSetting with name', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.utilities.input') + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting') + @mock.patch('tethys_apps.models.TethysApp') + def test_remove_ps_database_setting_proceed_delete(self, mock_app, mock_ps_db_setting, mock_pretty_output, + mock_input): + # Mock up for TethysApp and PersistentStoreDatabaseSetting + mock_app.objects.get.return_value = mock.MagicMock() + mock_ps_db_setting.objects.get.return_value = mock.MagicMock() + mock_ps_db_setting.objects.get().delete.return_value = True + mock_input.side_effect = ['Y'] + mock_app_package = mock.MagicMock() + mock_name = mock.MagicMock() + + # Based on the raw_input, delete not called and None returned + result = utilities.remove_ps_database_setting(app_package=mock_app_package, name=mock_name) + + self.assertEqual(True, result) + mock_ps_db_setting.objects.get().delete.assert_called() + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('Successfully removed PersistentStoreDatabaseSetting with name', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.utilities.input') + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_apps.models.PersistentStoreDatabaseSetting') + @mock.patch('tethys_apps.models.TethysApp') + def test_remove_ps_database_setting_do_not_proceed(self, mock_app, mock_ps_db_setting, mock_pretty_output, + mock_input): + # Mock up for TethysApp and PersistentStoreDatabaseSetting + mock_app.objects.get.return_value = mock.MagicMock() + mock_ps_db_setting.objects.get.return_value = mock.MagicMock() + mock_ps_db_setting.objects.get().delete.return_value = True + mock_input.side_effect = ['foo', 'N'] + mock_app_package = mock.MagicMock() + mock_name = mock.MagicMock() + + # Based on the raw_input, delete not called and None returned + result = utilities.remove_ps_database_setting(app_package=mock_app_package, name=mock_name) + + self.assertEqual(None, result) + mock_ps_db_setting.objects.get().delete.assert_not_called() + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertEqual('Aborted. PersistentStoreDatabaseSetting not removed.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_services.models.SpatialDatasetService') + def test_link_service_to_app_setting_spatial_dss_does_not_exist(self, mock_service, mock_pretty_output): + from django.core.exceptions import ObjectDoesNotExist + + # Mock up the SpatialDatasetService to throw ObjectDoesNotExist + mock_service.objects.get.side_effect = ObjectDoesNotExist + + # Based on exception, False will be returned + result = utilities.link_service_to_app_setting(service_type='spatial', service_uid='123', + app_package='foo_app', setting_type='ds_spatial', + setting_uid='456') + + self.assertEqual(False, result) + mock_service.objects.get.assert_called_once_with(pk=123) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('with ID/Name', po_call_args[0][0][0]) + self.assertIn('does not exist.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_services.models.SpatialDatasetService') + @mock.patch('tethys_apps.models.TethysApp') + def test_link_service_to_app_setting_spatial_dss_value_error(self, mock_app, mock_service, mock_pretty_output): + from django.core.exceptions import ObjectDoesNotExist + + # Mock up TethysApp to throw ObjectDoesNotExist + mock_app.objects.get.side_effect = ObjectDoesNotExist + # Mock up the SpatialDatasetService to MagicMock + mock_service.objects.get.return_value = mock.MagicMock() + + # Based on ValueError exception casting to int, then TethysApp ObjectDoesNotExist False will be returned + result = utilities.link_service_to_app_setting(service_type='spatial', service_uid='foo_spatial_service', + app_package='foo_app', setting_type='ds_spatial', + setting_uid='456') + + self.assertEqual(False, result) + mock_service.objects.get.assert_called_once_with(name='foo_spatial_service') + mock_app.objects.get.assert_called_once_with(package='foo_app') + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('A Tethys App with the name', po_call_args[0][0][0]) + self.assertIn('does not exist. Aborted.', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_services.models.SpatialDatasetService') + @mock.patch('tethys_apps.models.TethysApp') + def test_link_service_to_app_setting_spatial_link_key_error(self, mock_app, mock_service, mock_pretty_output): + # Mock up TethysApp to MagicMock + mock_app.objects.get.return_value = mock.MagicMock() + # Mock up the SpatialDatasetService to MagicMock + mock_service.objects.get.return_value = mock.MagicMock() + + # Based on KeyError for invalid setting_type False will be returned + result = utilities.link_service_to_app_setting(service_type='spatial', service_uid='foo_spatial_service', + app_package='foo_app', setting_type='foo_invalid', + setting_uid='456') + + self.assertEqual(False, result) + mock_service.objects.get.assert_called_once_with(name='foo_spatial_service') + mock_app.objects.get.assert_called_once_with(package='foo_app') + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('The setting_type you specified ("foo_invalid") does not exist.', po_call_args[0][0][0]) + self.assertIn('Choose from: "ps_database|ps_connection|ds_spatial"', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_sdk.app_settings.SpatialDatasetServiceSetting') + @mock.patch('tethys_services.models.SpatialDatasetService') + @mock.patch('tethys_apps.models.TethysApp') + def test_link_service_to_app_setting_spatial_link_value_error_save(self, mock_app, mock_service, mock_setting, + mock_pretty_output): + # Mock up TethysApp to MagicMock + mock_app.objects.get.return_value = mock.MagicMock() + # Mock up the SpatialDatasetService to MagicMock + mock_service.objects.get.return_value = mock.MagicMock() + # Mock up the SpatialDatasetServiceSetting to MagicMock + mock_setting.objects.get.return_value = mock.MagicMock() + mock_setting.objects.get().save.return_value = True + + # True will be returned, mocked save will be called + result = utilities.link_service_to_app_setting(service_type='spatial', service_uid='foo_spatial_service', + app_package='foo_app', setting_type='ds_spatial', + setting_uid='foo_456') + + self.assertEqual(True, result) + mock_service.objects.get.assert_called_once_with(name='foo_spatial_service') + mock_app.objects.get.assert_called_once_with(package='foo_app') + mock_setting.objects.get.assert_called() + mock_setting.objects.get().save.assert_called_once() + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('was successfully linked to', po_call_args[0][0][0]) + + @mock.patch('tethys_apps.cli.cli_colors.pretty_output') + @mock.patch('tethys_sdk.app_settings.SpatialDatasetServiceSetting') + @mock.patch('tethys_services.models.SpatialDatasetService') + @mock.patch('tethys_apps.models.TethysApp') + def test_link_service_to_app_setting_spatial_link_does_not_exist(self, mock_app, mock_service, mock_setting, + mock_pretty_output): + from django.core.exceptions import ObjectDoesNotExist + + # Mock up TethysApp to MagicMock + mock_app.objects.get.return_value = mock.MagicMock() + # Mock up the SpatialDatasetService to MagicMock + mock_service.objects.get.return_value = mock.MagicMock() + # Mock up the SpatialDatasetServiceSetting to MagicMock + mock_setting.objects.get.side_effect = ObjectDoesNotExist + + # Based on KeyError for invalid setting_type False will be returned + result = utilities.link_service_to_app_setting(service_type='spatial', service_uid='foo_spatial_service', + app_package='foo_app', setting_type='ds_spatial', + setting_uid='456') + + self.assertEqual(False, result) + mock_service.objects.get.assert_called_once_with(name='foo_spatial_service') + mock_app.objects.get.assert_called_once_with(package='foo_app') + mock_setting.objects.get.assert_called() + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('with ID/Name', po_call_args[0][0][0]) + self.assertIn('does not exist.', po_call_args[0][0][0]) diff --git a/tests/unit_tests/test_tethys_apps/test_views.py b/tests/unit_tests/test_tethys_apps/test_views.py new file mode 100644 index 000000000..becd8cf96 --- /dev/null +++ b/tests/unit_tests/test_tethys_apps/test_views.py @@ -0,0 +1,230 @@ +import unittest +import mock + +from tethys_apps.views import library, handoff_capabilities, handoff, send_beta_feedback_email, update_job_status + + +class TethysAppsViewsTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.views.render') + @mock.patch('tethys_apps.views.TethysApp') + def test_library(self, mock_tethys_app, mock_render): + mock_request = mock.MagicMock() + mock_request.user.is_staff = True + mock_app1 = mock.MagicMock() + mock_app1.configured = True + mock_app2 = mock.MagicMock() + mock_app2.configured = False + mock_tethys_app.objects.all.return_value = [mock_app1, mock_app2] + mock_render.return_value = True + + ret = library(mock_request) + self.assertEqual(ret, mock_render.return_value) + mock_tethys_app.objects.all.assert_called_once() + mock_render.assert_called_once_with(mock_request, 'tethys_apps/app_library.html', + {'apps': {'configured': [mock_app1], 'unconfigured': [mock_app2]}}) + + @mock.patch('tethys_apps.views.render') + @mock.patch('tethys_apps.views.TethysApp') + def test_library_no_staff(self, mock_tethys_app, mock_render): + mock_request = mock.MagicMock() + mock_request.user.is_staff = False + mock_app1 = mock.MagicMock() + mock_app1.configured = True + mock_app2 = mock.MagicMock() + mock_app2.configured = False + mock_tethys_app.objects.all.return_value = [mock_app1, mock_app2] + mock_render.return_value = True + + ret = library(mock_request) + self.assertEqual(ret, mock_render.return_value) + mock_tethys_app.objects.all.assert_called_once() + mock_render.assert_called_once_with(mock_request, 'tethys_apps/app_library.html', + {'apps': {'configured': [mock_app1], 'unconfigured': []}}) + + @mock.patch('tethys_apps.views.HttpResponse') + @mock.patch('tethys_apps.views.TethysAppBase') + def test_handoff_capabilities(self, mock_app_base, mock_http_response): + mock_request = mock.MagicMock() + mock_app_name = 'foo-app' + mock_manager = mock.MagicMock() + mock_handlers = mock.MagicMock() + mock_app_base.get_handoff_manager.return_value = mock_manager + mock_manager.get_capabilities.return_value = mock_handlers + mock_http_response.return_value = True + + ret = handoff_capabilities(mock_request, mock_app_name) + self.assertTrue(ret) + mock_app_base.get_handoff_manager.assert_called_once() + mock_manager.get_capabilities.assert_called_once_with('foo_app', external_only=True, jsonify=True) + mock_http_response.assert_called_once_with(mock_handlers, content_type='application/javascript') + + @mock.patch('tethys_apps.views.TethysAppBase') + def test_handoff(self, mock_app_base): + mock_request = mock.MagicMock() + mock_request_dict = mock.MagicMock() + mock_request.GET.dict.return_value = mock_request_dict + mock_app_name = 'foo-app' + mock_handler_name = 'foo_handler' + mock_manager = mock.MagicMock() + mock_app_base.get_handoff_manager.return_value = mock_manager + mock_manager.handoff.return_value = True + + ret = handoff(mock_request, mock_app_name, mock_handler_name) + self.assertTrue(ret) + mock_app_base.get_handoff_manager.assert_called_once() + mock_manager.handoff.assert_called_once_with(mock_request, mock_handler_name, 'foo_app') + mock_request.GET.dict.assert_called_once() + + @mock.patch('tethys_apps.views.JsonResponse') + @mock.patch('tethys_apps.views.get_active_app') + def test_send_beta_feedback_email_app_none(self, mock_get_active_app, mock_json_response): + mock_request = mock.MagicMock() + mock_post = mock.MagicMock() + mock_request.POST = mock_post + mock_post.get.return_value = 'http://foo/beta' + mock_get_active_app.return_value = None + mock_json_response.return_value = True + + ret = send_beta_feedback_email(mock_request) + self.assertTrue(ret) + mock_post.get.assert_called_once_with('betaFormUrl') + mock_get_active_app.assert_called_once_with(url='http://foo/beta') + mock_json_response.assert_called_once_with({'success': False, + 'error': 'App not found or feedback_emails not defined in app.py'}) + + @mock.patch('tethys_apps.views.send_mail') + @mock.patch('tethys_apps.views.JsonResponse') + @mock.patch('tethys_apps.views.get_active_app') + def test_send_beta_feedback_email_send_mail(self, mock_get_active_app, mock_json_response, mock_send_mail): + mock_request = mock.MagicMock() + mock_post = mock.MagicMock() + mock_app = mock.MagicMock() + mock_app.feedback_emails = 'foo@feedback.foo' + mock_app.name = 'foo_name' + mock_request.POST = mock_post + mock_post.get.side_effect = ['http://foo/beta', 'foo_betaUser', 'foo_betaSubmitLocalTime', + 'foo_betaSubmitUTCOffset', 'foo_betaFormUrl', 'foo_betaFormUserAgent', + 'foo_betaFormVendor', 'foo_betaUserComments'] + mock_get_active_app.return_value = mock_app + mock_json_response.return_value = True + mock_send_mail.return_value = True + + ret = send_beta_feedback_email(mock_request) + self.assertTrue(ret) + mock_post.get.assert_any_call('betaFormUrl') + mock_get_active_app.assert_called_once_with(url='http://foo/beta') + mock_post.get.assert_any_call('betaUser') + mock_post.get.assert_any_call('betaSubmitLocalTime') + mock_post.get.assert_any_call('betaSubmitUTCOffset') + mock_post.get.assert_any_call('betaFormUrl') + mock_post.get.assert_any_call('betaFormUserAgent') + mock_post.get.assert_any_call('betaFormVendor') + mock_post.get.assert_called_with('betaUserComments') + expected_subject = 'User Feedback for {0}'.format(mock_app.name.encode('utf-8')) + expected_message = 'User: {0}\n'\ + 'User Local Time: {1}\n'\ + 'UTC Offset in Hours: {2}\n'\ + 'App URL: {3}\n'\ + 'User Agent: {4}\n'\ + 'Vendor: {5}\n'\ + 'Comments:\n' \ + '{6}'.\ + format('foo_betaUser', + 'foo_betaSubmitLocalTime', + 'foo_betaSubmitUTCOffset', + 'foo_betaFormUrl', + 'foo_betaFormUserAgent', + 'foo_betaFormVendor', + 'foo_betaUserComments' + ) + mock_send_mail.assert_called_once_with(expected_subject, expected_message, from_email=None, + recipient_list=mock_app.feedback_emails) + mock_json_response.assert_called_once_with({'success': True, + 'result': 'Emails sent to specified developers'}) + + @mock.patch('tethys_apps.views.send_mail') + @mock.patch('tethys_apps.views.JsonResponse') + @mock.patch('tethys_apps.views.get_active_app') + def test_send_beta_feedback_email_send_mail_exception(self, mock_get_active_app, mock_json_response, + mock_send_mail): + mock_request = mock.MagicMock() + mock_post = mock.MagicMock() + mock_app = mock.MagicMock() + mock_app.feedback_emails = 'foo@feedback.foo' + mock_app.name = 'foo_name' + mock_request.POST = mock_post + mock_post.get.side_effect = ['http://foo/beta', 'foo_betaUser', 'foo_betaSubmitLocalTime', + 'foo_betaSubmitUTCOffset', 'foo_betaFormUrl', 'foo_betaFormUserAgent', + 'foo_betaFormVendor', 'foo_betaUserComments'] + mock_get_active_app.return_value = mock_app + mock_json_response.return_value = False + mock_send_mail.side_effect = Exception('foo_error') + + ret = send_beta_feedback_email(mock_request) + self.assertFalse(ret) + mock_post.get.assert_any_call('betaFormUrl') + mock_get_active_app.assert_called_once_with(url='http://foo/beta') + mock_post.get.assert_any_call('betaUser') + mock_post.get.assert_any_call('betaSubmitLocalTime') + mock_post.get.assert_any_call('betaSubmitUTCOffset') + mock_post.get.assert_any_call('betaFormUrl') + mock_post.get.assert_any_call('betaFormUserAgent') + mock_post.get.assert_any_call('betaFormVendor') + mock_post.get.assert_called_with('betaUserComments') + expected_subject = 'User Feedback for {0}'.format(mock_app.name.encode('utf-8')) + expected_message = 'User: {0}\n' \ + 'User Local Time: {1}\n' \ + 'UTC Offset in Hours: {2}\n' \ + 'App URL: {3}\n' \ + 'User Agent: {4}\n' \ + 'Vendor: {5}\n' \ + 'Comments:\n' \ + '{6}'. \ + format('foo_betaUser', + 'foo_betaSubmitLocalTime', + 'foo_betaSubmitUTCOffset', + 'foo_betaFormUrl', + 'foo_betaFormUserAgent', + 'foo_betaFormVendor', + 'foo_betaUserComments' + ) + mock_send_mail.assert_called_once_with(expected_subject, expected_message, from_email=None, + recipient_list=mock_app.feedback_emails) + mock_json_response.assert_called_once_with({'success': False, + 'error': 'Failed to send email: foo_error'}) + + @mock.patch('tethys_apps.views.JsonResponse') + @mock.patch('tethys_apps.views.TethysJob') + def test_update_job_status(self, mock_tethysjob, mock_json_response): + mock_request = mock.MagicMock() + mock_job_id = mock.MagicMock() + mock_job1 = mock.MagicMock() + mock_job1.status = True + mock_job2 = mock.MagicMock() + mock_tethysjob.objects.filter.return_value = [mock_job1, mock_job2] + mock_json_response.return_value = True + + ret = update_job_status(mock_request, mock_job_id) + self.assertTrue(ret) + mock_tethysjob.objects.filter.assert_called_once_with(id=mock_job_id) + mock_json_response.assert_called_once_with({'success': True}) + + @mock.patch('tethys_apps.views.JsonResponse') + @mock.patch('tethys_apps.views.TethysJob') + def test_update_job_statusException(self, mock_tethysjob, mock_json_response): + mock_request = mock.MagicMock() + mock_job_id = mock.MagicMock() + mock_tethysjob.objects.filter.side_effect = Exception + mock_json_response.return_value = False + + ret = update_job_status(mock_request, mock_job_id) + self.assertFalse(ret) + mock_tethysjob.objects.filter.assert_called_once_with(id=mock_job_id) + mock_json_response.assert_called_once_with({'success': False}) diff --git a/tests/unit_tests/test_tethys_compute/test_admin.py b/tests/unit_tests/test_tethys_compute/test_admin.py new file mode 100644 index 000000000..ace0ca0de --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_admin.py @@ -0,0 +1,48 @@ +import unittest +import mock + +from tethys_compute.admin import SchedulerAdmin, JobAdmin + + +class TestTethysComputeAdmin(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_scheduler_admin(self): + mock_admin = mock.MagicMock() + mock_admin2 = mock.MagicMock() + + sa = SchedulerAdmin(mock_admin, mock_admin2) + self.assertListEqual(['name', 'host', 'username', 'password', 'private_key_path', 'private_key_pass'], + sa.list_display) + + def test_job_admin(self): + mock_admin = mock.MagicMock() + mock_admin2 = mock.MagicMock() + + ja = JobAdmin(mock_admin, mock_admin2) + self.assertListEqual(['name', 'description', 'label', 'user', 'creation_time', 'execute_time', + 'completion_time', 'status'], ja.list_display) + self.assertEquals(('name',), ja.list_display_links) + + def test_job_admin_has_add_permission(self): + mock_admin = mock.MagicMock() + mock_admin2 = mock.MagicMock() + mock_request = mock.MagicMock() + + ja = JobAdmin(mock_admin, mock_admin2) + self.assertFalse(ja.has_add_permission(mock_request)) + + def test_admin_site_register(self): + from django.contrib import admin + from tethys_compute.models import Scheduler, TethysJob + registry = admin.site._registry + self.assertIn(Scheduler, registry) + self.assertIsInstance(registry[Scheduler], SchedulerAdmin) + + self.assertIn(TethysJob, registry) + self.assertIsInstance(registry[TethysJob], JobAdmin) diff --git a/tests/unit_tests/test_tethys_compute/test_apps.py b/tests/unit_tests/test_tethys_compute/test_apps.py new file mode 100644 index 000000000..bb898f6a5 --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_apps.py @@ -0,0 +1,22 @@ +import unittest + +from django.apps import apps +from tethys_compute.apps import TethysComputeConfig + + +class TethysComputeConfigAppsTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_TethysComputeConfig(self): + app_config = apps.get_app_config('tethys_compute') + name = app_config.name + verbose_name = app_config.verbose_name + + self.assertEqual('tethys_compute', name) + self.assertEqual('Tethys Compute', verbose_name) + self.assertTrue(isinstance(app_config, TethysComputeConfig)) diff --git a/tests/unit_tests/test_tethys_compute/test_job_manager.py b/tests/unit_tests/test_tethys_compute/test_job_manager.py index 7d3530d4a..a71cdd8ef 100644 --- a/tests/unit_tests/test_tethys_compute/test_job_manager.py +++ b/tests/unit_tests/test_tethys_compute/test_job_manager.py @@ -1,38 +1,438 @@ -from django.test import TestCase -from tethys_apps.base import TethysAppBase -from tethys_compute.job_manager import JobManager, TethysJob, CondorJob -from django.contrib.auth.models import User +import unittest +import mock -def echo(arg): - return arg +from tethys_compute.job_manager import JobManager, JobTemplate, BasicJobTemplate, CondorJobTemplate,\ + CondorJobDescription, CondorWorkflowTemplate, CondorWorkflowNodeBaseTemplate, CondorWorkflowJobTemplate +from tethys_compute.models import (TethysJob, + BasicJob, + CondorJob, + CondorWorkflow, + CondorWorkflowJobNode) -class TestApp(TethysAppBase): - def job_templates(self): - return [] -class TethysJobTestCase(TestCase): +class TestJobManager(unittest.TestCase): + def setUp(self): - self.app = TestApp() - self.job_manager = JobManager(self.app) - self.user = User.objects.create_user('user', 'user@example.com', 'pass') + pass + + def tearDown(self): + pass + + def test_JobManager_init(self): + mock_app = mock.MagicMock() + mock_app.package = 'test_label' + mock_app.get_app_workspace.return_value = 'test_app_workspace' + + mock_template1 = mock.MagicMock() + mock_template1.name = 'template_1' + mock_template2 = mock.MagicMock() + mock_template2.name = 'template_2' + + mock_app.job_templates.return_value = [mock_template1, mock_template2] + + # Execute + ret = JobManager(mock_app) + + # Check Result + self.assertEqual(mock_app, ret.app) + self.assertEqual('test_label', ret.label) + self.assertEqual('test_app_workspace', ret.app_workspace) + self.assertEqual(mock_template1, ret.job_templates['template_1']) + self.assertEqual(mock_template2, ret.job_templates['template_2']) + + @mock.patch('tethys_compute.job_manager.print') + @mock.patch('tethys_compute.job_manager.warnings') + @mock.patch('tethys_compute.job_manager.JobManager.old_create_job') + def test_JobManager_create_job_template(self, mock_ocj, mock_warn, mock_print): + mock_app = mock.MagicMock() + mock_app.package = 'test_label' + mock_app.get_app_workspace.return_value = 'test_app_workspace' + mock_user_workspace = mock.MagicMock() + + mock_app.get_user_workspace.return_value = mock_user_workspace + mock_app.get_user_workspace().path = 'test_user_workspace' + + mock_template1 = mock.MagicMock() + mock_template1.name = 'template_1' + mock_template2 = mock.MagicMock() + mock_template2.name = 'template_2' + + mock_app.job_templates.return_value = [mock_template1, mock_template2] + + mock_ocj.return_value = 'old_create_job_return_value' + # Execute + ret_jm = JobManager(mock_app) + ret_job = ret_jm.create_job(name='test_name', user='test_user', template_name='template_1') + + # Check result + self.assertEqual('old_create_job_return_value', ret_job) + + mock_ocj.assert_called_with('test_name', 'test_user', 'template_1') + + # Check if warning message is called + check_msg = 'The job template "{0}" was used in the "{1}" app. Using job templates is now deprecated.'.format( + 'template_1', 'test_label' + ) + rts_call_args = mock_warn.warn.call_args_list + self.assertEqual(check_msg, rts_call_args[0][0][0]) + mock_print.assert_called_with(check_msg) + + @mock.patch('tethys_compute.job_manager.CondorJob') + def test_JobManager_create_job_template_none(self, mock_cj): + mock_app = mock.MagicMock() + mock_app.package = 'test_label' + mock_app.get_app_workspace.return_value = 'test_app_workspace' + mock_user_workspace = mock.MagicMock() + + mock_app.get_user_workspace.return_value = mock_user_workspace + mock_app.get_user_workspace().path = 'test_user_workspace' + + with mock.patch.dict('tethys_compute.job_manager.JOB_TYPES', {'CONDOR': mock_cj}): + # Execute + ret_jm = JobManager(mock_app) + ret_jm.create_job(name='test_name', user='test_user', job_type='CONDOR') + mock_cj.assert_called_with(label='test_label', name='test_name', user='test_user', + workspace='test_user_workspace') + + def test_old_create_job(self): + mock_app = mock.MagicMock() + mock_app.package = 'test_label' + mock_app.get_app_workspace.return_value = 'test_app_workspace' + mock_user_workspace = mock.MagicMock() + + mock_app.get_user_workspace.return_value = mock_user_workspace + mock_app.get_user_workspace().path = 'test_user_workspace' + + mock_template1 = mock.MagicMock() + mock_template1.name = 'template_1' + mock_template2 = mock.MagicMock() + mock_template2.name = 'template_2' + + mock_app.job_templates.return_value = [mock_template1, mock_template2] + + # Execute + ret_jm = JobManager(mock_app) + ret_jm.old_create_job(name='test_name', user='test_user', template_name='template_1') + mock_template1.create_job.assert_called_with(app_workspace='test_app_workspace', label='test_label', + name='test_name', user='test_user', + user_workspace=mock_user_workspace, + workspace='test_user_workspace') + + def test_old_create_job_key_error(self): + mock_app = mock.MagicMock() + + mock_name = 'foo' + mock_user = 'foo_user' + mock_template_name = 'bar' + mock_app.package = 'test_app_name' + + mgr = JobManager(mock_app) + self.assertRaises(KeyError, mgr.old_create_job, name=mock_name, user=mock_user, + template_name=mock_template_name) + + @mock.patch('tethys_compute.job_manager.TethysJob') + def test_JobManager_list_job(self, mock_tethys_job): + mock_args = mock.MagicMock() + mock_jobs = mock.MagicMock() + mock_tethys_job.objects.filter().order_by().select_subclasses.return_value = mock_jobs + + mock_user = 'foo_user' + + mgr = JobManager(mock_args) + ret = mgr.list_jobs(user=mock_user) + + self.assertEquals(ret, mock_jobs) + mock_tethys_job.objects.filter().order_by().select_subclasses.assert_called_once() + + @mock.patch('tethys_compute.job_manager.TethysJob') + def test_JobManager_get_job(self, mock_tethys_job): + mock_args = mock.MagicMock() + mock_app_package = mock.MagicMock() + mock_args.package = mock_app_package + mock_jobs = mock.MagicMock() + mock_tethys_job.objects.get_subclass.return_value = mock_jobs + + mock_job_id = 'fooid' + mock_user = 'bar' + + mgr = JobManager(mock_args) + ret = mgr.get_job(job_id=mock_job_id, user=mock_user) + + self.assertEquals(ret, mock_jobs) + mock_tethys_job.objects.get_subclass.assert_called_once_with(id='fooid', label=mock_app_package, user='bar') + + @mock.patch('tethys_compute.job_manager.TethysJob') + def test_JobManager_get_job_dne(self, mock_tethys_job): + mock_args = mock.MagicMock() + mock_app_package = mock.MagicMock() + mock_args.package = mock_app_package + mock_tethys_job.DoesNotExist = TethysJob.DoesNotExist # Restore original exception + mock_tethys_job.objects.get_subclass.side_effect = TethysJob.DoesNotExist + + mock_job_id = 'fooid' + mock_user = 'bar' + + mgr = JobManager(mock_args) + ret = mgr.get_job(job_id=mock_job_id, user=mock_user) + + self.assertEquals(ret, None) + mock_tethys_job.objects.get_subclass.assert_called_once_with(id='fooid', label=mock_app_package, user='bar') + + def test_JobManager_get_job_status_callback_url(self): + mock_args = mock.MagicMock() + mock_request = mock.MagicMock() + mock_job_id = 'foo' + + mgr = JobManager(mock_args) + mgr.get_job_status_callback_url(mock_request, mock_job_id) + mock_request.build_absolute_uri.assert_called_once_with(u'/update-job-status/foo/') + + def test_JobManager_replace_workspaces(self): + mock_app_workspace = mock.MagicMock() + mock_user_workspace = mock.MagicMock() + mock_app_workspace.path = 'replace_app' + mock_user_workspace.path = 'replace_user' + + mock_parameters = {str: '/foo/app/$(APP_WORKSPACE)/foo', + dict: {'foo': '/foo/user/$(USER_WORKSPACE)/foo'}, + list: ['/foo/app/$(APP_WORKSPACE)/foo', '/foo/user/$(USER_WORKSPACE)/foo'], + tuple: ('/foo/app/$(APP_WORKSPACE)/foo', '/foo/user/$(USER_WORKSPACE)/foo'), + int: 1 + } + + expected = {str: '/foo/app/replace_app/foo', + dict: {'foo': '/foo/user/replace_user/foo'}, + list: ['/foo/app/replace_app/foo', '/foo/user/replace_user/foo'], + tuple: ('/foo/app/replace_app/foo', '/foo/user/replace_user/foo'), + int: 1 + } + + ret = JobManager._replace_workspaces(mock_parameters, mock_app_workspace, mock_user_workspace) + self.assertEquals(ret, expected) + + # JobTemplate + + def test_JobTemplate_init(self): + mock_name = mock.MagicMock() + mock_type = BasicJob + mock_parameters = {list: ['/foo/app/workspace', '/foo/user/workspace']} + + ret = JobTemplate(name=mock_name, type=mock_type, parameters=mock_parameters) + self.assertEquals(mock_name, ret.name) + self.assertEquals(BasicJob, ret.type) + self.assertEquals(mock_parameters, ret.parameters) + + @mock.patch('tethys_compute.job_manager.JobManager._replace_workspaces') + def test_JobTemplate_create_job(self, mock_replace_workspaces): + mock_name = mock.MagicMock() + mock_type = BasicJob + mock_app_workspace = '/foo/APP_WORKSPACE' + mock_user_workspace = '/foo/APP_WORKSPACE' + mock_parameters = {list: ['/foo/app/workspace', '/foo/user/workspace']} + + ret = JobTemplate(name=mock_name, type=mock_type, parameters=mock_parameters) + ret2 = ret.create_job(mock_app_workspace, mock_user_workspace) + mock_replace_workspaces.assert_called_once() + self.assertTrue(isinstance(ret2, TethysJob)) + + # BasicJobTemplate + + def test_BasicJobTemplate_init(self): + mock_name = mock.MagicMock() + mock_parameters = {list: ['/foo/app/workspace', '/foo/user/workspace']} + + ret = BasicJobTemplate(name=mock_name, parameters=mock_parameters) + self.assertEquals(mock_name, ret.name) + self.assertEquals(BasicJob, ret.type) + self.assertEquals(mock_parameters, ret.parameters) + + def test_BasicJobTemplate_process_parameters(self): + mock_name = mock.MagicMock() + mock_parameters = {list: ['/foo/app/workspace', '/foo/user/workspace']} + + ret = BasicJobTemplate(name=mock_name, parameters=mock_parameters) + ret.process_parameters() + self.assertEquals(mock_name, ret.name) + self.assertEquals(BasicJob, ret.type) + self.assertEquals(mock_parameters, ret.parameters) + + # CondorJobTemplate + + def test_CondorJobTemplate_init_job_description(self): + mock_scheduler = mock.MagicMock() + mock_jd = mock.MagicMock() + mock_jd.remote_input_files = 'test_input' + mock_jd.condorpy_template_name = 'test_template' + mock_jd.attributes = 'test_attributes' + mock_param = {'workspace': 'test_workspace'} + + ret = CondorJobTemplate(name='test_name', other_params=mock_param, job_description=mock_jd, + scheduler=mock_scheduler) + + # Check result + self.assertEqual(ret.type, CondorJob) + self.assertEqual('test_name', ret.name) + self.assertEqual('test_template', ret.parameters['condorpy_template_name']) + self.assertEqual('test_template', ret.parameters['condorpy_template_name']) + self.assertEqual(mock_param, ret.parameters['other_params']) + self.assertEqual(mock_scheduler, ret.parameters['scheduler']) + self.assertEqual('test_attributes', ret.parameters['attributes']) + + def test_CondorJobTemplate_process_parameters(self): + mock_name = mock.MagicMock() + mock_job_description = mock.MagicMock() + mock_scheduler = mock.MagicMock() + mock_parameters = {list: ['/foo/app/workspace', '/foo/user/workspace'], + 'scheduler': mock_scheduler, + 'remote_input_files': mock_job_description.remote_input_files, + 'attributes': mock_job_description.attributes} + + ret = CondorJobTemplate(name=mock_name, parameters=mock_parameters, job_description=mock_job_description, + scheduler=mock_scheduler) + + self.assertIsNone(ret.process_parameters()) + + @mock.patch('tethys_compute.job_manager.CondorJob') + def test_CondorJobDescription_init(self, mock_job): + ret = CondorJobDescription(condorpy_template_name='temp1', remote_input_files='rm_files', name='foo') + self.assertEqual('rm_files', ret.remote_input_files) + self.assertEqual('temp1', ret.condorpy_template_name) + self.assertEqual('foo', ret.attributes['name']) + + @mock.patch('tethys_compute.job_manager.JobManager._replace_workspaces') + def test_CondorJobDescription_process_attributes(self, mock_jm): + + ret_cd = CondorJobDescription(condorpy_template_name='temp1', remote_input_files='rm_files', name='foo') + + mock_app_workspace = mock.MagicMock(path='/foo/app/workspacee') + mock_user_workspace = mock.MagicMock(path='/foo/user/workspacee') + before_dict = self.__dict__ + + after_dict = before_dict + after_dict['foo'] = 'bar' + mock_jm.return_value = after_dict + ret_cd.process_attributes(app_workspace=mock_app_workspace, user_workspace=mock_user_workspace) + + self.assertEqual(after_dict, ret_cd.__dict__) + + # CondorWorkflowTemplate + + def test_CondorWorkflowTemplate_init(self): + input_name = 'foo' + input_parameters = {'param1': 'inputparam1'} + input_jobs = ['job1', 'job2', 'job3'] + input_max_jobs = 10 + input_config = 'test_path_config_file' + + ret = CondorWorkflowTemplate(name=input_name, parameters=input_parameters, jobs=input_jobs, + max_jobs=input_max_jobs, config=input_config, additional_param='param1') + + self.assertEquals(input_name, ret.name) + self.assertEquals(input_parameters, ret.parameters) + self.assertEquals(set(input_jobs), ret.node_templates) + self.assertEquals(input_max_jobs, ret.parameters['max_jobs']) + self.assertEquals(input_config, ret.parameters['config']) + self.assertEquals('param1', ret.parameters['additional_param']) + + @mock.patch('tethys_compute.job_manager.JobTemplate.create_job') + def test_CondorWorkflowTemplate_create_job(self, mock_save): + input_name = 'foo' + input_parameters = {'param1': 'inputparam1'} + mock_job1 = mock.MagicMock() + mock_node_parent = mock.MagicMock() + mock_node_parent.create_node.return_value = 'add_parent_code_line' + mock_job1.parameters = {'parents': [mock_node_parent]} + mock_node1 = mock.MagicMock() + mock_job1.create_node.return_value = mock_node1 + + input_jobs = [mock_job1] + input_max_jobs = 10 + input_config = 'test_path_config_file' + + ret = CondorWorkflowTemplate(name=input_name, parameters=input_parameters, jobs=input_jobs, + max_jobs=input_max_jobs, config=input_config, additional_param='param1') + + app_workspace = '/foo/APP_WORKSPACE' + user_workspace = '/foo/USER_WORKSPACE' + + # call Execute + ret.create_job(app_workspace=app_workspace, user_workspace=user_workspace) + + # Check called + mock_node1.add_parent.assert_called_with('add_parent_code_line') + + def test_CondorWorkflowNodeBaseTemplate_init(self): + mock_name = mock.MagicMock() + mock_type = CondorWorkflowJobNode + mock_parameters = {} + + ret = CondorWorkflowNodeBaseTemplate(name=mock_name, type=mock_type, parameters=mock_parameters) + self.assertEquals(mock_name, ret.name) + self.assertEquals(CondorWorkflowJobNode, ret.type) + self.assertEquals(mock_parameters, ret.parameters) + + @mock.patch('tethys_compute.job_manager.issubclass') + def test_CondorWorkflowNodeBaseTemplate_add_dependency(self, mock_issubclass): + mock_name = mock.MagicMock() + mock_type = CondorWorkflowJobNode + mock_parameters = {} + mock_dependency = mock.MagicMock() + dep_set = set() + dep_set.add(mock_dependency) + mock_issubclass.return_value = True + + ret = CondorWorkflowNodeBaseTemplate(name=mock_name, type=mock_type, parameters=mock_parameters) + ret.add_dependency(mock_dependency) + self.assertEquals(dep_set, ret.dependencies) - def test_create_job(self): - job = self.job_manager.create_empty_job('job', self.user, CondorJob) - self.assertIsInstance(job, CondorJob, 'Empty job is not an instance of CondorJob') - self.assertIsInstance(job, TethysJob, 'Empty job is not an instance of TethysJob') + @mock.patch('tethys_compute.job_manager.CondorWorkflowNode.save') + @mock.patch('tethys_compute.job_manager.JobManager._replace_workspaces') + def test_CondorWorkflowNodeBaseTemplate_create_node(self, mock_replace, mock_node_save): + mock_name = mock.MagicMock() + mock_type = CondorWorkflowJobNode + mock_job1 = mock.MagicMock() + mock_job_parent = mock.MagicMock() + mock_job1.parameters = {'parents': [mock_job_parent]} + mock_parameters = {'parents': [mock_job_parent]} + mock_app_workspace = '/foo/APP_WORKSPACE' + mock_user_workspace = '/foo/USER_WORKSPACE' + mock_workflow = mock.MagicMock() + mock_workflow.__class__ = CondorWorkflow + mock_node_save.return_value = True + mock_replace.return_value = {'parents': [mock_job_parent], + 'num_jobs': 1, + 'remote_input_files': []} - self.assertIsInstance(job.extended_properties, dict) + ret = CondorWorkflowNodeBaseTemplate(name=mock_name, type=mock_type, parameters=mock_parameters) + node = ret.create_node(mock_workflow, mock_app_workspace, mock_user_workspace) - job.extended_properties['property'] = 'value' + self.assertTrue(isinstance(node, CondorWorkflowJobNode)) + self.assertEquals(mock_parameters, ret.parameters) + self.assertEquals('JOB', node.type) + self.assertEquals('', node.workspace) + # CondorWorkflowJobTemplate - job.save() + def test_CondorWorkflowJobTemplate_init(self): + mock_jd = mock.MagicMock() + mock_jd.remote_input_files = 'test_input' + mock_jd.condorpy_template_name = 'test_template' + mock_jd.attributes = 'test_attributes' - self.assertDictEqual(job.extended_properties, {'property': 'value'}) + ret = CondorWorkflowJobTemplate(name='test_name', job_description=mock_jd, other_params='test_kwargs') + # Check result + self.assertEqual(ret.type, CondorWorkflowJobNode) + self.assertEqual('test_name', ret.name) + self.assertEqual('test_template', ret.parameters['condorpy_template_name']) + self.assertEqual('test_attributes', ret.parameters['attributes']) + self.assertEqual('test_kwargs', ret.parameters['other_params']) - job.process_results = echo + def test_CondorWorkflowJobTemplate_process_parameters(self): + mock_jd = mock.MagicMock() + mock_jd.remote_input_files = 'test_input' + mock_jd.condorpy_template_name = 'test_template' + mock_jd.attributes = 'test_attributes' - job.save() + ret = CondorWorkflowJobTemplate(name='test_name', job_description=mock_jd, other_params='test_kwargs') - self.assertEqual(job.process_results('test'), 'test') - self.assertTrue(hasattr(job.process_results, '__call__')) \ No newline at end of file + self.assertIsNone(ret.process_parameters()) diff --git a/tests/unit_tests/test_tethys_compute/test_models.py b/tests/unit_tests/test_tethys_compute/test_models.py deleted file mode 100644 index 43e770091..000000000 --- a/tests/unit_tests/test_tethys_compute/test_models.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.test import TestCase -from tethys_compute.models import TethysJob - -class TethysJobTestCase(TestCase): - def setUp(self): - job = TethysJob() - - def test_job(self): - pass \ No newline at end of file diff --git a/tests/unit_tests/test_tethys_compute/test_models/__init__.py b/tests/unit_tests/test_tethys_compute/test_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_BasicJob.py b/tests/unit_tests/test_tethys_compute/test_models/test_BasicJob.py new file mode 100644 index 000000000..be251c5e9 --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_models/test_BasicJob.py @@ -0,0 +1,42 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_compute.models import BasicJob +from django.contrib.auth.models import User + + +class CondorBaseTest(TethysTestCase): + def set_up(self): + self.user = User.objects.create_user('tethys_super', 'user@example.com', 'pass') + self.basic_job = BasicJob( + name='test_basicjob', + description='test_description', + user=self.user, + label='test_label' + ) + self.basic_job.save() + + def tear_down(self): + self.basic_job.delete() + + def test_execute(self): + ret = BasicJob.objects.get(name='test_basicjob')._execute() + self.assertIsNone(ret) + + def test__update_status(self): + ret = BasicJob.objects.get(name='test_basicjob')._update_status() + self.assertIsNone(ret) + + def test_process_results(self): + ret = BasicJob.objects.get(name='test_basicjob')._process_results() + self.assertIsNone(ret) + + def test_stop(self): + ret = BasicJob.objects.get(name='test_basicjob').stop() + self.assertIsNone(ret) + + def test_pause(self): + ret = BasicJob.objects.get(name='test_basicjob').pause() + self.assertIsNone(ret) + + def test_resume(self): + ret = BasicJob.objects.get(name='test_basicjob').resume() + self.assertIsNone(ret) diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_CondorBase.py b/tests/unit_tests/test_tethys_compute/test_models/test_CondorBase.py new file mode 100644 index 000000000..d559093d5 --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_models/test_CondorBase.py @@ -0,0 +1,173 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_compute.models import Scheduler, CondorBase +from django.contrib.auth.models import User +from datetime import datetime, timedelta +from django.utils import timezone +import mock + + +class CondorBaseTest(TethysTestCase): + def set_up(self): + self.user = User.objects.create_user('tethys_super', 'user@example.com', 'pass') + + self.scheduler = Scheduler( + name='test_scheduler', + host='localhost', + username='tethys_super', + password='pass', + private_key_path='test_path', + private_key_pass='test_pass' + ) + self.scheduler.save() + + self.condorbase = CondorBase( + name='test_condorbase', + description='test_description', + user=self.user, + label='test_label', + cluster_id='1', + remote_id='test_machine', + scheduler=self.scheduler + ) + self.condorbase.save() + + self.condorbase_exe = CondorBase( + name='test_condorbase_exe', + description='test_description', + user=self.user, + label='test_label', + execute_time=timezone.now(), + cluster_id='1', + remote_id='test_machine', + scheduler=self.scheduler + ) + self.condorbase_exe.save() + + def tear_down(self): + self.scheduler.delete() + self.condorbase.delete() + self.condorbase_exe.delete() + + @mock.patch('tethys_compute.models.CondorBase._condor_object') + def test_condor_object_pro(self, mock_co): + ret = CondorBase.objects.get(name='test_condorbase') + mock_co.return_value = ret + + ret.condor_object + + # Check result + self.assertEqual(mock_co, ret.condor_object) + self.assertEqual(1, ret.condor_object._cluster_id) + self.assertEqual('test_machine', ret.condor_object._remote_id) + mock_co.set_scheduler.assert_called_with('localhost', 'tethys_super', 'pass', 'test_path', 'test_pass') + + def test_condor_obj_abs(self): + ret = CondorBase.objects.get(name='test_condorbase')._condor_object() + + # Check result. + self.assertIsNone(ret) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_statuses_prop(self, mock_co): + mock_co.statuses = 'test_statuses' + + condor_obj = CondorBase.objects.get(name='test_condorbase') + + # to set updated inside if statement = False + d = datetime.now() - timedelta(days=1) + condor_obj._last_status_update = d + + # Execute + ret = condor_obj.statuses + + # Check result + self.assertEqual('test_statuses', ret) + + # to set updated inside if statement = True + d = datetime.now() + condor_obj._last_status_update = d + + mock_co.statuses = 'test_statuses2' + ret = condor_obj.statuses + + # Check result, should not set statuses from condor_object again. Same ret as previous. + self.assertEqual('test_statuses', ret) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_execute_abs(self, mock_co): + mock_co.submit.return_value = 111 + + # Execute + CondorBase.objects.get(name='test_condorbase')._execute() + + ret = CondorBase.objects.get(name='test_condorbase') + + # Check result + self.assertEqual(111, ret.cluster_id) + + def test_update_status_not_execute_time(self): + ret = CondorBase.objects.get(name='test_condorbase')._update_status() + + # Check result + self.assertEqual('PEN', ret) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_update_status(self, mock_co): + mock_co.status = 'Various' + mock_co.statuses = {'Unexpanded': '', 'Idle': '', 'Running': ''} + CondorBase.objects.get(name='test_condorbase_exe')._update_status() + + ret = CondorBase.objects.get(name='test_condorbase_exe')._status + + # Check result + self.assertEqual('VCP', ret) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_update_status_exception(self, mock_co): + mock_co.status = 'Various' + mock_co.statuses = {} + CondorBase.objects.get(name='test_condorbase_exe')._update_status() + + ret = CondorBase.objects.get(name='test_condorbase_exe')._status + + # Check result + self.assertEqual('ERR', ret) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_process_results(self, mock_co): + CondorBase.objects.get(name='test_condorbase_exe')._process_results() + + # Check result + mock_co.sync_remote_output.assert_called() + mock_co.close_remote.assert_called() + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_stop(self, mock_co): + CondorBase.objects.get(name='test_condorbase_exe').stop() + + # Check result + mock_co.remove.assert_called() + + def test_pause(self): + ret = CondorBase.objects.get(name='test_condorbase_exe').pause() + + # Check result + self.assertIsNone(ret) + + def test_resume(self): + ret = CondorBase.objects.get(name='test_condorbase_exe').resume() + + # Check result + self.assertIsNone(ret) + + @mock.patch('tethys_compute.models.CondorBase._condor_object') + def test_update_database_fields(self, mock_co): + mock_co._remote_id = 'test_update_remote_id' + ret = CondorBase.objects.get(name='test_condorbase_exe') + + # _condor_object is an abstract method returning a condorpyjob or condorpyworkflow. + # We'll test condor_object.remote_id in condorpyjob test + ret.update_database_fields() + + # Check result + self.assertEqual('test_update_remote_id', ret.remote_id) diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_CondorJob.py b/tests/unit_tests/test_tethys_compute/test_models/test_CondorJob.py new file mode 100644 index 000000000..98d4477a0 --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_models/test_CondorJob.py @@ -0,0 +1,110 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_compute.models import TethysJob, CondorJob, Scheduler, CondorBase, CondorPyJob +from django.contrib.auth.models import User +import mock +import os +import shutil +import os.path + + +class CondorJobTest(TethysTestCase): + def set_up(self): + self.user = User.objects.create_user('tethys_super', 'user@example.com', 'pass') + + self.scheduler = Scheduler( + name='test_scheduler', + host='localhost', + username='tethys_super', + password='pass', + ) + self.scheduler.save() + + path = os.path.dirname(__file__) + self.workspace_dir = os.path.join(path, 'workspace') + + self.condorjob = CondorJob( + name='test condorbase', + description='test_description', + user=self.user, + label='test_label', + cluster_id='1', + remote_id='test_machine', + workspace=self.workspace_dir, + scheduler=self.scheduler, + condorpyjob_id='99', + _attributes={'foo': 'bar'}, + _remote_input_files=['test_file1.txt', 'test_file2.txt'], + ) + self.condorjob.save() + + self.id_val = TethysJob.objects.get(name='test condorbase').id + + def tear_down(self): + self.scheduler.delete() + if self.condorjob.condorpyjob_ptr_id == 99: + self.condorjob.delete() + + if os.path.exists(self.workspace_dir): + shutil.rmtree(self.workspace_dir) + + def test_condor_object_prop(self): + condorpy_job = self.condorjob._condor_object + + # Check result + self.assertEqual('test_condorbase', condorpy_job.name) + self.assertEqual('test_condorbase', condorpy_job.attributes['job_name']) + self.assertEqual('bar', condorpy_job.attributes['foo']) + self.assertIn('test_file1.txt', condorpy_job.remote_input_files) + self.assertIn('test_file2.txt', condorpy_job.remote_input_files) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_execute(self, mock_cos): + # TODO: Check if we can mock this or we can provide an executable. + # Mock condor_object.submit() + mock_cos.submit.return_value = 111 + self.condorjob._execute(queue=2) + + # Check result + self.assertEqual(111, self.condorjob.cluster_id) + self.assertEqual(2, self.condorjob.num_jobs) + + @mock.patch('tethys_compute.models.CondorPyJob.update_database_fields') + @mock.patch('tethys_compute.models.CondorBase.update_database_fields') + def test_update_database_fields(self, mock_cb_update, mock_cj_update): + # Mock condor_object.submit() + self.condorjob.update_database_fields() + + # Check result + mock_cb_update.assert_called() + mock_cj_update.assert_called() + + def test_condor_job_pre_save(self): + # Check if CondorBase is updated + self.assertIsInstance(CondorBase.objects.get(tethysjob_ptr_id=self.id_val), CondorBase) + + # Check if CondorPyJob is updated + self.assertIsInstance(CondorPyJob.objects.get(condorpyjob_id=99), CondorPyJob) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_condor_job_pre_delete(self, mock_co): + if not os.path.exists(self.workspace_dir): + os.makedirs(self.workspace_dir) + file_path = os.path.join(self.workspace_dir, 'test_file.txt') + open(file_path, 'a').close() + + self.condorjob.delete() + + # Check if close_remote is called + mock_co.close_remote.assert_called() + + # Check if file has been removed + self.assertFalse(os.path.isfile(file_path)) + + @mock.patch('tethys_compute.models.log') + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_condor_job_pre_delete_exception(self, mock_co, mock_log): + mock_co.close_remote.side_effect = Exception('test error') + self.condorjob.delete() + + # Check if close_remote is called + mock_log.exception.assert_called_with('test error') diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_CondorPyJob.py b/tests/unit_tests/test_tethys_compute/test_models/test_CondorPyJob.py new file mode 100644 index 000000000..ef2c6dff6 --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_models/test_CondorPyJob.py @@ -0,0 +1,143 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_compute.models import Scheduler, CondorPyJob, CondorJob +from condorpy import Templates +from django.contrib.auth.models import User +from condorpy import Job +import mock + + +class CondorPyJobTest(TethysTestCase): + def set_up(self): + self.condor_py = CondorPyJob( + condorpyjob_id='99', + _attributes={'foo': 'bar'}, + _remote_input_files=['test_file1.txt', 'test_file2.txt'], + ) + + self.condor_py.save() + + user = User.objects.create_user('tethys_super', 'user@example.com', 'pass') + + scheduler = Scheduler( + name='test_scheduler', + host='localhost', + username='tethys_super', + password='pass', + ) + + self.condorjob = CondorJob( + name='test condorbase', + description='test_description', + user=user, + label='test_label', + workspace='test_workspace', + scheduler=scheduler, + condorpyjob_id='98', + ) + + def tear_down(self): + self.condor_py.delete() + + def test_init(self): + ret = CondorPyJob(_attributes={'foo': 'bar'}, + condorpy_template_name='vanilla_base', + ) + # Check result + # Instance of CondorPyJob + self.assertIsInstance(ret, CondorPyJob) + # Check return vanilla Django base + self.assertEqual('vanilla', ret.attributes['universe']) + + def test_get_condorpy_template(self): + ret = CondorPyJob.get_condorpy_template('vanilla_base') + + # Check result + self.assertEqual(ret, Templates.vanilla_base) + + def test_get_condorpy_template_default(self): + ret = CondorPyJob.get_condorpy_template(None) + + # Check result + self.assertEqual(ret, Templates.base) + + def test_get_condorpy_template_no_template(self): + ret = CondorPyJob.get_condorpy_template('test') + + # Check result + self.assertEqual(ret, Templates.base) + + def test_condorpy_job(self): + ret = self.condorjob.condorpy_job + + # Check result for Django Job + self.assertIsInstance(ret, Job) + self.assertEqual('test_condorbase', ret.name) + self.assertEqual('test_workspace', ret._cwd) + self.assertEqual('test_condorbase', ret.attributes['job_name']) + self.assertEqual(1, ret.num_jobs) + + def test_attributes(self): + ret = CondorPyJob.objects.get(condorpyjob_id='99').attributes + + self.assertEqual({'foo': 'bar'}, ret) + + @mock.patch('tethys_compute.models.CondorPyJob.condorpy_job') + def test_set_attributes(self, mock_ca): + set_attributes = {'baz': 'qux'} + ret = CondorPyJob.objects.get(condorpyjob_id='99') + + ret.attributes = set_attributes + + # Mock setter + mock_ca._attributes = set_attributes + + self.assertEqual({'baz': 'qux'}, ret.attributes) + + @mock.patch('tethys_compute.models.CondorPyJob.condorpy_job') + def test_numjobs(self, mock_cj): + num_job = 5 + ret = CondorPyJob.objects.get(condorpyjob_id='99') + ret.num_jobs = num_job + + # Mock setter + mock_cj.numb_jobs = num_job + + # self.assertEqual(5, ret) + self.assertEqual(num_job, ret.num_jobs) + + @mock.patch('tethys_compute.models.CondorPyJob.condorpy_job') + def test_remote_input_files(self, mock_cj): + ret = CondorPyJob.objects.get(condorpyjob_id='99') + ret.remote_input_files = ['test_newfile1.txt'] + + # Mock setter + mock_cj.remote_input_files = ['test_newfile1.txt'] + + # Check result + self.assertEqual(['test_newfile1.txt'], ret.remote_input_files) + + def test_initial_dir(self): + ret = self.condorjob.initial_dir + + # Check result + self.assertEqual('test_workspace/.', ret) + + def test_set_and_get_attribute(self): + self.condorjob.set_attribute('test', 'value') + + ret = self.condorjob.get_attribute('test') + + # Check Result + self.assertEqual('value', ret) + + def test_update_database_fields(self): + ret = self.condorjob + + # Before Update + self.assertFalse(ret.attributes) + + # Execute + ret.update_database_fields() + + # Check after update + self.assertEqual('test_condorbase', ret.attributes['job_name']) diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_CondorPyWorkflow.py b/tests/unit_tests/test_tethys_compute/test_models/test_CondorPyWorkflow.py new file mode 100644 index 000000000..d8a9f3d2c --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_models/test_CondorPyWorkflow.py @@ -0,0 +1,165 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_compute.models import CondorPyWorkflow, CondorWorkflow, Scheduler, CondorWorkflowJobNode +from django.contrib.auth.models import User +import mock +import os +import os.path + + +class CondorPyWorkflowTest(TethysTestCase): + def set_up(self): + path = os.path.dirname(__file__) + self.workspace_dir = os.path.join(path, 'workspace') + + self.user = User.objects.create_user('tethys_super', 'user@example.com', 'pass') + + self.scheduler = Scheduler( + name='test_scheduler', + host='localhost', + username='tethys_super', + password='pass', + private_key_path='test_path', + private_key_pass='test_pass' + ) + self.scheduler.save() + + self.condorworkflow = CondorWorkflow( + _max_jobs={'foo': 10}, + _config='test_config', + name='test name', + workspace=self.workspace_dir, + user=self.user, + scheduler=self.scheduler, + ) + self.condorworkflow.save() + + self.id_value = CondorWorkflow.objects.get(name='test name').condorpyworkflow_ptr_id + self.condorpyworkflow = CondorPyWorkflow.objects.get(condorpyworkflow_id=self.id_value) + + self.condorworkflowjobnode_a = CondorWorkflowJobNode( + name='Job1_a', + workflow=self.condorpyworkflow, + _attributes={'foo': 'one'}, + _num_jobs=1, + _remote_input_files=['test1.txt'], + ) + + self.condorworkflowjobnode_a.save() + + self.condorworkflowjobnode_a1 = CondorWorkflowJobNode( + name='Job1_a1', + workflow=self.condorpyworkflow, + _attributes={'foo': 'one'}, + _num_jobs=1, + _remote_input_files=['test1.txt'], + ) + + self.condorworkflowjobnode_a1.save() + + # Django model many to many relationship add method + # self.condorworkflowjobnode.parent_nodes.add(self.condorworkflowjobnode_job) + + def tear_down(self): + self.scheduler.delete() + self.condorworkflow.delete() + self.condorworkflowjobnode_a.delete() + self.condorworkflowjobnode_a1.delete() + + # pass + + def test_condorpy_workflow_prop(self): + ret = self.condorworkflow.condorpy_workflow + + # Check Result + self.assertEqual('', repr(ret)) + self.assertEqual(self.workspace_dir, ret._cwd) + self.assertEqual('test_config', ret.config) + + @mock.patch('tethys_compute.models.Workflow') + def test_max_jobs(self, mock_wf): + max_jobs = {'foo': 5} + self.condorpyworkflow.name = 'test_name' + self.condorpyworkflow.workspace = 'test_dict' + self.condorpyworkflow.max_jobs = max_jobs + + ret = self.condorpyworkflow.max_jobs + + # Check result + self.assertEqual(5, ret['foo']) + mock_wf.assert_called_with(config='test_config', max_jobs={'foo': 10}, + name='test_name', working_directory='test_dict') + + @mock.patch('tethys_compute.models.CondorPyWorkflow.condorpy_workflow') + def test_config(self, mock_cw): + test_config_value = 'test_config2' + + # Mock condorpy_workflow.config = test_config_value. We have already tested condorpy_workflow. + mock_cw.config = test_config_value + + # Setter + self.condorpyworkflow.config = test_config_value + + # Property + ret = self.condorpyworkflow.config + + # Check result + self.assertEqual('test_config2', ret) + + def test_nodes(self): + ret = self.condorworkflow.nodes + # Check result after loading nodes + # self.assertEqual('Node_1', ret[0].name) + + # Check result in CondorPyWorkflow object + self.assertEqual({'foo': 10}, ret[0].workflow.max_jobs) + self.assertEqual('test_config', ret[0].workflow.config) + + def test_load_nodes(self): + # Before load nodes. Set should be empty + ret_before = self.condorworkflow.condorpy_workflow.node_set + list_before = [] + + list_after = [] + for e in ret_before: + list_before.append(e) + + # Check list_before is empty + self.assertFalse(list_before) + + # Add parent + self.condorworkflowjobnode_a1.add_parent(self.condorworkflowjobnode_a) + + # Execute load nodes + self.condorworkflow.load_nodes() + + # After load nodes, Set should have two elements. One parent and one child + ret_after = self.condorworkflow.condorpy_workflow.node_set + # Convert to list for checking result + for e in ret_after: + list_after.append(e) + + # Check list_after is not empty + self.assertTrue(list_after) + + # sort list and compare result + list_after.sort(key=lambda node: node.job.name) + self.assertEqual('Job1_a', list_after[0].job.name) + self.assertEqual('Job1_a1', list_after[1].job.name) + + def test_add_max_jobs_throttle(self): + # Set max_jobs + self.condorworkflow.add_max_jobs_throttle('foo1', 20) + + # Get return value + ret = self.condorworkflow.condorpy_workflow + + # Check result + self.assertEqual(20, ret.max_jobs['foo1']) + + @mock.patch('tethys_compute.models.CondorWorkflowJobNode.update_database_fields') + def test_update_database_fields(self, mock_update): + # Set attribute for node + self.condorpyworkflow.update_database_fields() + + # Check if mock is called twice for node and child node + self.assertTrue(mock_update.call_count == 2) diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_CondorWorkflow.py b/tests/unit_tests/test_tethys_compute/test_models/test_CondorWorkflow.py new file mode 100644 index 000000000..781a8b95d --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_models/test_CondorWorkflow.py @@ -0,0 +1,150 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_compute.models import CondorPyWorkflow, CondorWorkflow, Scheduler, CondorWorkflowJobNode +from django.contrib.auth.models import User +import mock +import os +import shutil +import os.path + + +class CondorWorkflowTest(TethysTestCase): + def set_up(self): + path = os.path.dirname(__file__) + self.workspace_dir = os.path.join(path, 'workspace') + self.user = User.objects.create_user('tethys_super', 'user@example.com', 'pass') + + self.scheduler = Scheduler( + name='test_scheduler', + host='localhost', + username='tethys_super', + password='pass', + private_key_path='test_path', + private_key_pass='test_pass' + ) + self.scheduler.save() + + self.condorworkflow = CondorWorkflow( + _max_jobs={'foo': 10}, + _config='test_config', + name='test name', + workspace=self.workspace_dir, + user=self.user, + scheduler=self.scheduler, + ) + self.condorworkflow.save() + + self.id_value = CondorWorkflow.objects.get(name='test name').condorpyworkflow_ptr_id + self.condorpyworkflow = CondorPyWorkflow.objects.get(condorpyworkflow_id=self.id_value) + + self.condorworkflowjobnode_child = CondorWorkflowJobNode( + name='Node_child', + workflow=self.condorpyworkflow, + _attributes={'test': 'one'}, + _num_jobs=1, + _remote_input_files=['test1.txt'], + ) + self.condorworkflowjobnode_child.save() + + # self.child_id = CondorWorkflowJobNode.objects.get(name='Node_child').id + + self.condorworkflowjobnode = CondorWorkflowJobNode( + name='Node_1', + workflow=self.condorpyworkflow, + _attributes={'test': 'one'}, + _num_jobs=1, + _remote_input_files=['test1.txt'], + ) + self.condorworkflowjobnode.save() + + # Django model many to many relationship add method + self.condorworkflowjobnode.parent_nodes.add(self.condorworkflowjobnode_child) + + self.condorbase_id = CondorWorkflow.objects.get(name='test name').condorbase_ptr_id + self.condorpyworkflow_id = CondorWorkflow.objects.get(name='test name').condorpyworkflow_ptr_id + + def tear_down(self): + self.scheduler.delete() + + if self.condorworkflow.condorbase_ptr_id == self.condorbase_id: + self.condorworkflow.delete() + + if os.path.exists(self.workspace_dir): + shutil.rmtree(self.workspace_dir) + + def test_condor_object_prop(self): + ret = self.condorworkflow._condor_object + + # Check workflow return + self.assertEqual({'foo': 10}, ret.max_jobs) + self.assertEqual('test_config', ret.config) + self.assertEqual('', repr(ret)) + + @mock.patch('tethys_compute.models.CondorPyWorkflow.load_nodes') + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_execute(self, mock_co, mock_ln): + # Mock submit to return a 111 cluster id + mock_co.submit.return_value = 111 + + # Execute + self.condorworkflow._execute(options=['foo']) + + # We already tested load_nodes in CondorPyWorkflow, just mocked to make sure it's called here. + mock_ln.assert_called() + mock_co.submit.assert_called_with(options=['foo']) + + # Check cluster_id from _execute in condorbase + self.assertEqual(111, self.condorworkflow.cluster_id) + + def test_get_job(self): + ret = self.condorworkflow.get_job(job_name='Node_1') + + # Check result + self.assertIsInstance(ret, CondorWorkflowJobNode) + self.assertEqual('Node_1', ret.name) + + def test_get_job_does_not_exist(self): + ret = self.condorworkflow.get_job(job_name='Node_2') + # Check result + self.assertIsNone(ret) + + @mock.patch('tethys_compute.models.CondorBase.update_database_fields') + @mock.patch('tethys_compute.models.CondorPyWorkflow.update_database_fields') + def test_update_database_fieds(self, mock_pw_update, mock_ba_update): + # Execute + self.condorworkflow.update_database_fields() + + # Check if mock is called + mock_pw_update.assert_called() + mock_ba_update.assert_called() + + @mock.patch('tethys_compute.models.CondorWorkflow.update_database_fields') + def test_condor_workflow_presave(self, mock_update): + # Excute + self.condorworkflow.save() + + # Check if update_database_fields is called + mock_update.assert_called() + + @mock.patch('tethys_compute.models.CondorWorkflow.condor_object') + def test_condor_job_pre_delete(self, mock_co): + if not os.path.exists(self.workspace_dir): + os.makedirs(self.workspace_dir) + file_path = os.path.join(self.workspace_dir, 'test_file.txt') + open(file_path, 'a').close() + + self.condorworkflow.delete() + + # Check if close_remote is called + mock_co.close_remote.assert_called() + + # Check if file has been removed + self.assertFalse(os.path.isfile(file_path)) + + @mock.patch('tethys_compute.models.log') + @mock.patch('tethys_compute.models.CondorWorkflow.condor_object') + def test_condor_job_pre_delete_exception(self, mock_co, mock_log): + mock_co.close_remote.side_effect = Exception('test error') + self.condorworkflow.delete() + + # Check if close_remote is called + mock_log.exception.assert_called_with('test error') diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_CondorWorkflowJobNode.py b/tests/unit_tests/test_tethys_compute/test_models/test_CondorWorkflowJobNode.py new file mode 100644 index 000000000..3679d5572 --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_models/test_CondorWorkflowJobNode.py @@ -0,0 +1,88 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_compute.models import CondorPyWorkflow, CondorWorkflow, CondorWorkflowJobNode, TethysJob +from django.contrib.auth.models import User +import mock +import os +import os.path + + +class CondorPyWorkflowJobNodeTest(TethysTestCase): + def set_up(self): + path = os.path.dirname(__file__) + self.workspace_dir = os.path.join(path, 'workspace') + + self.user = User.objects.create_user('tethys_super', 'user@example.com', 'pass') + + self.condorworkflow = CondorWorkflow( + _max_jobs={'foo': 10}, + _config='test_config', + name='foo{id}', + workspace=self.workspace_dir, + user=self.user, + ) + self.condorworkflow.save() + + # To have a flow Node, we need to have a Condor Job which requires a CondorBase which requires a TethysJob + self.id_value = CondorWorkflow.objects.get(name='foo{id}').condorpyworkflow_ptr_id + self.condorpyworkflow = CondorPyWorkflow.objects.get(condorpyworkflow_id=self.id_value) + + self.condorworkflowjobnode = CondorWorkflowJobNode( + name='Job1_NodeA', + workflow=self.condorpyworkflow, + _attributes={'test': 'one'}, + _num_jobs=1, + _remote_input_files=['test1.txt'], + ) + self.condorworkflowjobnode.save() + + def tear_down(self): + self.condorworkflow.delete() + self.condorworkflowjobnode.delete() + + def test_type_prop(self): + self.assertEqual('JOB', self.condorworkflowjobnode.type) + + def test_workspace_prop(self): + self.assertEqual('', self.condorworkflowjobnode.workspace) + + @mock.patch('tethys_compute.models.CondorPyJob.condorpy_job') + def test_job_prop(self, mock_cpj): + # Condorpy_job Prop is already tested in CondorPyJob Test case + self.assertEqual(mock_cpj, self.condorworkflowjobnode.job) + + @mock.patch('tethys_compute.models.CondorWorkflowNode.update_database_fields') + @mock.patch('tethys_compute.models.CondorPyJob.update_database_fields') + def test_update_database_fields(self, mock_pj_update, mock_wfn_update): + # Execute + self.condorworkflowjobnode.update_database_fields() + + # Check result + mock_pj_update.assert_called_once() + mock_wfn_update.assert_called_once() + + @mock.patch('tethys_compute.models.CondorWorkflowNode.update_database_fields') + @mock.patch('tethys_compute.models.CondorPyJob.update_database_fields') + def test_receiver_pre_save(self, mock_pj_update, mock_wfn_update): + self.condorworkflowjobnode.save() + + # Check result + mock_pj_update.assert_called_once() + mock_wfn_update.assert_called_once() + + def test_job_post_save(self): + # get the job + tethys_job = TethysJob.objects.get(name='foo{id}') + id_val = tethys_job.id + + # Run save to activate post save + tethys_job.save() + + # Set up new name + new_name = 'foo{id}'.format(id=id_val) + + # Get same tethys job with new name + tethys_job = TethysJob.objects.get(name=new_name) + + # Check results + self.assertIsInstance(tethys_job, TethysJob) + self.assertEqual(new_name, tethys_job.name) diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_Scheduler.py b/tests/unit_tests/test_tethys_compute/test_models/test_Scheduler.py new file mode 100644 index 000000000..e5be2aab5 --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_models/test_Scheduler.py @@ -0,0 +1,30 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_compute.models import Scheduler + + +class SchedulerTest(TethysTestCase): + def set_up(self): + self.scheduler = Scheduler( + name='test_scheduler', + host='localhost', + username='tethys_super', + password='pass', + private_key_path='test_path', + private_key_pass='test_pass' + ) + self.scheduler.save() + + def tear_down(self): + self.scheduler.delete() + + def test_Scheduler(self): + ret = Scheduler.objects.get(name='test_scheduler') + + # Check result + self.assertIsInstance(ret, Scheduler) + self.assertEqual('test_scheduler', ret.name) + self.assertEqual('localhost', ret.host) + self.assertEqual('tethys_super', ret.username) + self.assertEqual('pass', ret.password) + self.assertEqual('test_path', ret.private_key_path) + self.assertEqual('test_pass', ret.private_key_pass) diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_TethysJob.py b/tests/unit_tests/test_tethys_compute/test_models/test_TethysJob.py new file mode 100644 index 000000000..2876e33ce --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_models/test_TethysJob.py @@ -0,0 +1,243 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_compute.models import TethysJob, CondorBase, Scheduler +from django.contrib.auth.models import User +from datetime import datetime, timedelta +from pytz import timezone +from django.utils import timezone as dt +import mock + + +def test_function(): + pass + + +class TethysJobTest(TethysTestCase): + def set_up(self): + self.tz = timezone('America/Denver') + + self.user = User.objects.create_user('tethys_super', 'user@example.com', 'pass') + + self.scheduler = Scheduler( + name='test_scheduler', + host='localhost', + username='tethys_super', + password='pass', + private_key_path='test_path', + private_key_pass='test_pass' + ) + + self.scheduler.save() + + self.tethysjob = TethysJob( + name='test_tethysjob', + description='test_description', + user=self.user, + label='test_label', + ) + self.tethysjob.save() + + self.tethysjob_execute_time = TethysJob( + name='test_tethysjob_execute_time', + description='test_description', + user=self.user, + label='test_label', + execute_time=datetime(year=2018, month=1, day=1, tzinfo=self.tz), + completion_time=datetime(year=2018, month=1, day=1, hour=1, tzinfo=self.tz), + _status='VAR', + _process_results_function=test_function + + ) + self.tethysjob_execute_time.save() + + def tear_down(self): + self.tethysjob.delete() + self.tethysjob_execute_time.delete() + self.scheduler.delete() + + def test_update_status_interval_prop(self): + ret = TethysJob.objects.get(name='test_tethysjob').update_status_interval + + # Check result + self.assertIsInstance(ret, timedelta) + self.assertEqual(timedelta(0, 10), ret) + + def test_last_status_update_prop(self): + ret = TethysJob.objects.get(name='test_tethysjob') + check_date = datetime(year=2018, month=1, day=1, tzinfo=self.tz) + ret._last_status_update = check_date + + # Check result + self.assertEqual(check_date, ret.last_status_update) + + def test_status_prop(self): + ret = TethysJob.objects.get(name='test_tethysjob').status + + # Check result + self.assertEqual('Pending', ret) + + def test_run_time_execute_time_prop(self): + ret = TethysJob.objects.get(name='test_tethysjob_execute_time').run_time + + # Check result + self.assertIsInstance(ret, timedelta) + self.assertEqual(timedelta(0, 3600), ret) + + # TODO: How to get to inside the if self.completion_time and self.execute_time: statement + + def test_execute(self): + ret_old = TethysJob.objects.get(name='test_tethysjob_execute_time') + TethysJob.objects.get(name='test_tethysjob_execute_time').execute() + ret_new = TethysJob.objects.get(name='test_tethysjob_execute_time') + + self.assertNotEqual(ret_old.execute_time, ret_new.execute_time) + self.assertEqual('Various', ret_old.status) + self.assertEqual('Pending', ret_new.status) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_update_status_run(self, mock_co): + tethysjob = CondorBase( + name='test_tethysjob', + description='test_description', + user=self.user, + label='test_label', + scheduler=self.scheduler, + execute_time=dt.now(), + ) + + mock_co.status = 'Running' + tethysjob.update_status() + + # Check result + self.assertIsNotNone(tethysjob.last_status_update) + self.assertIsInstance(tethysjob.last_status_update, datetime) + self.assertIsNotNone(tethysjob.start_time) + self.assertIsInstance(tethysjob.start_time, datetime) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_update_status_com(self, mock_co): + tethysjob = CondorBase( + name='test_tethysjob', + description='test_description', + user=self.user, + label='test_label', + scheduler=self.scheduler, + execute_time=dt.now(), + ) + + mock_co.status = 'Completed' + tethysjob.update_status() + + # Check result + self.assertIsNotNone(tethysjob.last_status_update) + self.assertIsInstance(tethysjob.last_status_update, datetime) + self.assertIsNotNone(tethysjob.completion_time) + self.assertIsInstance(tethysjob.completion_time, datetime) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_update_status_vcp(self, mock_co): + tethysjob = CondorBase( + name='test_tethysjob', + description='test_description', + user=self.user, + label='test_label', + scheduler=self.scheduler, + execute_time=dt.now(), + ) + + mock_co.status = 'Various-Complete' + tethysjob.update_status() + + # Check result + self.assertIsNotNone(tethysjob.last_status_update) + self.assertIsInstance(tethysjob.last_status_update, datetime) + self.assertIsNotNone(tethysjob.completion_time) + self.assertIsInstance(tethysjob.completion_time, datetime) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_update_status_err(self, mock_co): + tethysjob = CondorBase( + name='test_tethysjob', + description='test_description', + user=self.user, + label='test_label', + scheduler=self.scheduler, + execute_time=dt.now(), + ) + + mock_co.status = 'Held' + tethysjob.update_status() + + # Check result + self.assertIsNotNone(tethysjob.last_status_update) + self.assertIsInstance(tethysjob.last_status_update, datetime) + self.assertIsNotNone(tethysjob.completion_time) + self.assertIsInstance(tethysjob.completion_time, datetime) + + @mock.patch('tethys_compute.models.CondorBase.condor_object') + def test_update_status_abt(self, mock_co): + tethysjob = CondorBase( + name='test_tethysjob', + description='test_description', + user=self.user, + label='test_label', + scheduler=self.scheduler, + execute_time=dt.now(), + ) + + mock_co.status = 'Removed' + tethysjob.update_status() + + # Check result + self.assertIsNotNone(tethysjob.last_status_update) + self.assertIsInstance(tethysjob.last_status_update, datetime) + self.assertIsNotNone(tethysjob.completion_time) + self.assertIsInstance(tethysjob.completion_time, datetime) + + @mock.patch('tethys_compute.models.TethysFunctionExtractor') + def test_process_results_function(self, mock_tfe): + mock_tfe().valid = True + mock_tfe().function = 'test_function_return' + + # Setter + TethysJob.objects.get(name='test_tethysjob_execute_time').process_results_function = test_function + + # Property + ret = TethysJob.objects.get(name='test_tethysjob_execute_time').process_results_function + + # Check result + self.assertEqual(ret, 'test_function_return') + mock_tfe.assert_called_with(str(test_function), None) + + def test_process_results(self): + ret = TethysJob.objects.get(name='test_tethysjob') + + ret.process_results('test', name='test_name') + + # Check result + self.assertIsInstance(ret.completion_time, datetime) + self.assertIsNotNone(ret.completion_time) + + def test_abs_method(self): + # Execute + ret = TethysJob.objects.get(name='test_tethysjob')._execute() + + # Check result + self.assertIsNone(ret) + + # Update Status + ret = TethysJob.objects.get(name='test_tethysjob')._update_status() + + # Check result + self.assertIsNone(ret) + + # Execute + ret = TethysJob.objects.get(name='test_tethysjob')._process_results() + + # Check result + self.assertIsNone(ret) + + self.assertRaises(NotImplementedError, TethysJob.objects.get(name='test_tethysjob').stop) + + self.assertRaises(NotImplementedError, TethysJob.objects.get(name='test_tethysjob').pause) + + self.assertRaises(NotImplementedError, TethysJob.objects.get(name='test_tethysjob').resume) diff --git a/tests/unit_tests/test_tethys_compute/test_models/test_WorkflowNode.py b/tests/unit_tests/test_tethys_compute/test_models/test_WorkflowNode.py new file mode 100644 index 000000000..64b1368b2 --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_models/test_WorkflowNode.py @@ -0,0 +1,114 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_compute.models import CondorPyWorkflow, CondorWorkflow, Scheduler, CondorWorkflowNode, \ + CondorWorkflowJobNode +from django.contrib.auth.models import User +from condorpy import Job +import mock +import os +import os.path + + +class CondorPyWorkflowNodeTest(TethysTestCase): + def set_up(self): + path = os.path.dirname(__file__) + self.workspace_dir = os.path.join(path, 'workspace') + + self.user = User.objects.create_user('tethys_super', 'user@example.com', 'pass') + + self.scheduler = Scheduler( + name='test_scheduler', + host='localhost', + username='tethys_super', + password='pass', + private_key_path='test_path', + private_key_pass='test_pass' + ) + self.scheduler.save() + + self.condorworkflow = CondorWorkflow( + _max_jobs={'foo': 10}, + _config='test_config', + name='test name', + workspace=self.workspace_dir, + user=self.user, + scheduler=self.scheduler, + ) + self.condorworkflow.save() + + self.id_value = CondorWorkflow.objects.get(name='test name').condorpyworkflow_ptr_id + self.condorpyworkflow = CondorPyWorkflow.objects.get(condorpyworkflow_id=self.id_value) + + # One node can have many children nodes + self.condorworkflowjobnode_child = CondorWorkflowJobNode( + name='Job1', + workflow=self.condorpyworkflow, + _attributes={'test': 'one'}, + _num_jobs=1, + _remote_input_files=['test1.txt'], + ) + self.condorworkflowjobnode_child.save() + + # One node can have many children nodes + self.condorworkflowjobnode_child2 = CondorWorkflowJobNode( + name='Job2', + workflow=self.condorpyworkflow, + _attributes={'test': 'one'}, + _num_jobs=1, + _remote_input_files=['test1.txt'], + ) + self.condorworkflowjobnode_child2.save() + + self.condorworkflownode = CondorWorkflowNode( + name='test_condorworkflownode', + workflow=self.condorpyworkflow, + ) + self.condorworkflownode.save() + + def tear_down(self): + self.condorworkflow.delete() + self.condorworkflowjobnode_child.delete() + self.condorworkflowjobnode_child2.delete() + + def test_type_abs_prop(self): + ret = self.condorworkflownode.type() + + # Check result + self.assertIsNone(ret) + + def test_job_abs_prop(self): + ret = self.condorworkflownode.job() + + # Check result + self.assertIsNone(ret) + + @mock.patch('tethys_compute.models.CondorWorkflowNode.job') + def test_condorpy_node(self, mock_job): + mock_job_return = Job(name='test_job', + attributes={'foo': 'bar'}, + num_jobs=1, + remote_input_files=['test_file.txt'], + working_directory=self.workspace_dir) + mock_job.return_value = mock_job_return + + self.condorworkflownode.job = mock_job_return + ret = self.condorworkflownode.condorpy_node + + # Check result + self.assertEqual('', repr(ret)) + + def test_add_parents_and_parents_prop(self): + # Add parent should add parent to condorwoflownode + self.condorworkflownode.add_parent(self.condorworkflowjobnode_child) + self.condorworkflownode.add_parent(self.condorworkflowjobnode_child2) + + # Get this Parent Nodes here + ret = self.condorworkflownode.parents + + # Check result + self.assertIsInstance(ret[0], CondorWorkflowJobNode) + self.assertEqual('Job1', ret[0].name) + self.assertIsInstance(ret[1], CondorWorkflowJobNode) + self.assertEqual('Job2', ret[1].name) + + def test_update_database_fields(self): + self.assertIsNone(self.condorworkflownode.update_database_fields()) diff --git a/tests/unit_tests/test_tethys_compute/test_scheduler_manager.py b/tests/unit_tests/test_tethys_compute/test_scheduler_manager.py index e69de29bb..154ae8065 100644 --- a/tests/unit_tests/test_tethys_compute/test_scheduler_manager.py +++ b/tests/unit_tests/test_tethys_compute/test_scheduler_manager.py @@ -0,0 +1,54 @@ +import unittest +import mock + +from tethys_compute.scheduler_manager import list_schedulers, get_scheduler, create_scheduler + + +class SchedulerManagerTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_compute.scheduler_manager.Scheduler') + def test_list_schedulers(self, mock_scheduler): + mock_scheduler.objects.all.return_value = ['foo'] + ret = list_schedulers() + self.assertListEqual(['foo'], ret) + mock_scheduler.objects.all.assert_called() + + @mock.patch('tethys_compute.scheduler_manager.Scheduler') + def test_get_scheduler(self, mock_scheduler): + mock_filter_sche = mock.MagicMock() + mock_filter_foo = mock.MagicMock() + mock_filter_bar = mock.MagicMock() + + def my_filter(name): + if name == 'foo': + return [mock_filter_foo] + elif name == 'bar': + return [mock_filter_bar] + else: + return [mock_filter_sche] + + mock_scheduler.objects.filter.side_effect = my_filter + + self.assertEquals(mock_filter_foo, get_scheduler('foo')) + self.assertEquals(mock_filter_bar, get_scheduler('bar')) + self.assertEquals(mock_filter_sche, get_scheduler('asdf')) + mock_scheduler.objects.filter.assert_any_call(name='foo') + mock_scheduler.objects.filter.assert_any_call(name='bar') + mock_scheduler.objects.filter.assert_any_call(name='asdf') + + @mock.patch('tethys_compute.scheduler_manager.Scheduler') + def test_create_scheduler(self, mock_scheduler): + name = 'foo' + host = 'localhost' + mock_sch = mock.MagicMock() + + mock_scheduler.return_value = mock_sch + + self.assertEquals(mock_sch, create_scheduler(name, host)) + mock_scheduler.assert_called_once_with('foo', 'localhost', None, None, None, None) diff --git a/tests/unit_tests/test_tethys_compute/test_utilities.py b/tests/unit_tests/test_tethys_compute/test_utilities.py new file mode 100644 index 000000000..a81fcc40c --- /dev/null +++ b/tests/unit_tests/test_tethys_compute/test_utilities.py @@ -0,0 +1,251 @@ +from django.test import TestCase +from captcha.models import CaptchaStore +from django.contrib.auth.models import User +import mock +from tethys_compute.utilities import ListField, DictionaryField, Creator + + +def test_fun(): + return 'test' + + +class TestObject(object): + test_name = 'test' + + +class TethysComputeUtilitiesTests(TestCase): + + def setUp(self): + CaptchaStore.generate_key() + self.hashkey = CaptchaStore.objects.all()[0].hashkey + self.response = CaptchaStore.objects.all()[0].response + self.user = User.objects.create_user(username='user_exist', + email='foo_exist@aquaveo.com', + password='glass_onion') + + def tearDown(self): + pass + + def test_Creator_init(self): + mock_obj = mock.MagicMock() + ret = Creator(mock_obj) + self.assertEqual(mock_obj, ret.field) + + def test_Creator_get_set(self): + mock_field = mock.MagicMock() + mock_field.name = 'test_name' + + mock_field.to_python.return_value = 'test_name_value' + + # Set Object Value + TestObject.test_name = 'test2' + + ret = Creator(mock_field) + + ret_value = ret.__get__(TestObject) + + self.assertEqual('test2', ret_value) + + # Object is None + ret_value = ret.__get__(None) + + # Check result + self.assertEqual(ret_value, ret) + + # ListField + + def test_ListField(self): + ret = ListField() + self.assertEqual('List object', ret.description) + + def test_list_field_get_internal_type(self): + ret = ListField() + self.assertEqual('TextField', ret.get_internal_type()) + + def test_list_field_to_python_none(self): + ret = ListField() + self.assertIsNone(ret.to_python(value=None)) + + def test_list_field_to_python_empty_str(self): + ret = ListField() + self.assertListEqual([], ret.to_python(value="")) + + @mock.patch('tethys_compute.utilities.json.loads') + def test_list_field_to_python_str(self, mock_jl): + ret = ListField() + ret.to_python(value='foo') + mock_jl.assert_called_with('foo') + + @mock.patch('tethys_compute.utilities.json.loads') + def test_list_field_to_python_str_ValueError(self, mock_jl): + ret = ListField() + mock_jl.side_effect = ValueError + self.assertRaises(ValueError, ret.to_python, value='foo') + + def test_list_field_to_python_list(self): + ret = ListField() + input_value = ['foo', 'bar'] + output = ret.to_python(value=input_value) + self.assertListEqual(input_value, output) + + def test_list_field_to_python_dict(self): + ret = ListField() + input_value = {'name': 'bar'} + output = ret.to_python(value=input_value) + self.assertListEqual([], output) + + @mock.patch('tethys_compute.utilities.ListField.to_python') + def test_list_field_from_db_value(self, mock_tp): + ret = ListField() + ret.from_db_value(value='foo', expression='exp', connection='con', context='ctx') + mock_tp.assert_called_with('foo') + + def test_list_field_get_prep_value(self): + ret = ListField() + self.assertEqual('', ret.get_prep_value(value='')) + + def test_list_field_get_prep_value_str(self): + ret = ListField() + self.assertEqual('foo', ret.get_prep_value(value='foo')) + + @mock.patch('tethys_compute.utilities.json.dumps') + def test_list_field_get_prep_value_list(self, mock_jd): + ret = ListField() + input_value = ['foo', 'bar'] + ret.get_prep_value(value=input_value) + mock_jd.assert_called_with(input_value) + + @mock.patch('tethys_compute.utilities.ListField.get_prep_value') + @mock.patch('tethys_compute.utilities.ListField._get_val_from_obj') + def test_list_field_value_to_string(self, mock_gpvo, mock_gpv): + ret = ListField() + + output = mock_gpvo.return_value + + mock_obj = mock.MagicMock() + + ret.value_to_string(obj=mock_obj) + + mock_gpvo.assert_called_with(mock_obj) + + mock_gpv.assert_called_with(output) + + @mock.patch('tethys_compute.utilities.ListField.get_prep_value') + @mock.patch('django.db.models.fields.Field.clean') + def test_list_field_clean(self, mock_sc, mock_gpv): + ret = ListField() + input_value = 'foo' + input_model_instance = mock.MagicMock() + + output = mock_sc.return_value + + ret.clean(value=input_value, model_instance=input_model_instance) + + mock_sc.assert_called_with(input_value, input_model_instance) + + mock_gpv.assert_called_with(output) + + @mock.patch('django.db.models.fields.Field.formfield') + def test_list_field_formfield(self, mock_ff): + ret = ListField() + ret.formfield(additional='test2') + mock_ff.assert_called_once() + + # DictionaryField + + def test_DictionaryField(self): + ret = DictionaryField() + self.assertEqual('Dictionary object', ret.description) + + def test_dictionary_field_get_internal_type(self): + ret = DictionaryField() + self.assertEqual('TextField', ret.get_internal_type()) + + def test_dictionary_field_to_python_none(self): + ret = DictionaryField() + self.assertIsNone(ret.to_python(value=None)) + + def test_dictionary_field_to_python_empty_str(self): + ret = DictionaryField() + self.assertDictEqual({}, ret.to_python(value="")) + + @mock.patch('tethys_compute.utilities.json.loads') + def test_dictionary_field_to_python_str(self, mock_jl): + ret = DictionaryField() + ret.to_python(value='foo') + mock_jl.assert_called_with('foo') + + @mock.patch('tethys_compute.utilities.json.loads') + def test_dictionary_field_to_python_str_value_error(self, mock_jl): + ret = DictionaryField() + mock_jl.side_effect = ValueError + self.assertRaises(ValueError, ret.to_python, value='foo') + + def test_dictionary_field_to_python_dict(self): + ret = DictionaryField() + input_dict = {'name': 'foo', 'extra': 'bar'} + res = ret.to_python(value=input_dict) + self.assertDictEqual(input_dict, res) + + def test_dictionary_field_to_python_empty_dict(self): + ret = DictionaryField() + input_value = ['test1', 'test2'] + res = ret.to_python(value=input_value) + self.assertDictEqual({}, res) + + @mock.patch('tethys_compute.utilities.DictionaryField.to_python') + def test_dictionary_field_from_db_value(self, mock_tp): + ret = DictionaryField() + ret.from_db_value(value='foo', expression='exp', connection='con', context='ctx') + mock_tp.assert_called_with('foo') + + def test_dictionary_field_get_prep_value(self): + ret = DictionaryField() + self.assertEqual('', ret.get_prep_value(value='')) + + def test_dictionary_field_get_prep_value_str(self): + ret = DictionaryField() + self.assertEqual('foo', ret.get_prep_value(value='foo')) + + @mock.patch('tethys_compute.utilities.json.dumps') + def test_dictionary_field_get_prep_value_list(self, mock_jd): + ret = DictionaryField() + input_value = ['foo', 'bar'] + ret.get_prep_value(value=input_value) + mock_jd.assert_called_with(input_value) + + @mock.patch('tethys_compute.utilities.DictionaryField.get_prep_value') + @mock.patch('tethys_compute.utilities.DictionaryField._get_val_from_obj') + def test_dictionary_field_value_to_string(self, mock_gpvo, mock_gpv): + ret = DictionaryField() + + output = mock_gpvo.return_value + + mock_obj = mock.MagicMock() + + ret.value_to_string(obj=mock_obj) + + mock_gpvo.assert_called_with(mock_obj) + + mock_gpv.assert_called_with(output) + + @mock.patch('tethys_compute.utilities.DictionaryField.get_prep_value') + @mock.patch('django.db.models.fields.Field.clean') + def test_dictionary_field_clean(self, mock_sc, mock_gpv): + ret = DictionaryField() + input_value = 'foo' + input_model_instance = mock.MagicMock() + + output = mock_sc.return_value + + ret.clean(value=input_value, model_instance=input_model_instance) + + mock_sc.assert_called_with(input_value, input_model_instance) + + mock_gpv.assert_called_with(output) + + @mock.patch('django.db.models.fields.Field.formfield') + def test_dictionary_field_formfield(self, mock_ff): + ret = DictionaryField() + ret.formfield(additional='test2') + mock_ff.assert_called_once() diff --git a/tests/unit_tests/test_tethys_config/test_admin.py b/tests/unit_tests/test_tethys_config/test_admin.py new file mode 100644 index 000000000..af3bd293b --- /dev/null +++ b/tests/unit_tests/test_tethys_config/test_admin.py @@ -0,0 +1,51 @@ +import unittest +import mock + +from tethys_config.models import SettingsCategory, Setting +from tethys_config.admin import SettingInline, SettingCategoryAdmin + + +class TethysConfigAdminTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_SettingInline(self): + expected_fields = ('name', 'content', 'date_modified') + expected_readonly_fields = ('name', 'date_modified') + ret = SettingInline(mock.MagicMock(), mock.MagicMock()) + + self.assertEquals(expected_fields, ret.fields) + self.assertEquals(expected_readonly_fields, ret.readonly_fields) + self.assertEquals(Setting, ret.model) + self.assertEquals(0, ret.extra) + self.assertIsNotNone(ret.formfield_overrides) + + def test_SettingCategoryAdmin(self): + expected_fields = ('name',) + expected_readonly_fields = ('name',) + expected_inlines = [SettingInline] + ret = SettingCategoryAdmin(mock.MagicMock(), mock.MagicMock()) + + self.assertEquals(expected_fields, ret.fields) + self.assertEquals(expected_readonly_fields, ret.readonly_fields) + self.assertEquals(expected_inlines, ret.inlines) + + def test_has_delete_permission(self): + mock_request = mock.MagicMock() + ret = SettingCategoryAdmin(mock.MagicMock(), mock.MagicMock()) + self.assertFalse(ret.has_delete_permission(mock_request)) + + def test_has_add_permission(self): + mock_request = mock.MagicMock() + ret = SettingCategoryAdmin(mock.MagicMock(), mock.MagicMock()) + self.assertFalse(ret.has_add_permission(mock_request)) + + def test_admin_site_register(self): + from django.contrib import admin + registry = admin.site._registry + self.assertIn(SettingsCategory, registry) + self.assertIsInstance(registry[SettingsCategory], SettingCategoryAdmin) diff --git a/tests/unit_tests/test_tethys_config/test_apps.py b/tests/unit_tests/test_tethys_config/test_apps.py new file mode 100644 index 000000000..e7c88489a --- /dev/null +++ b/tests/unit_tests/test_tethys_config/test_apps.py @@ -0,0 +1,22 @@ +import unittest + +from django.apps import apps +from tethys_config.apps import TethysPortalConfig + + +class TethysConfigAppsTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_TethysPortalConfig(self): + app_config = apps.get_app_config('tethys_config') + name = app_config.name + verbose_name = app_config.verbose_name + + self.assertEqual('tethys_config', name) + self.assertEqual('Tethys Portal', verbose_name) + self.assertTrue(isinstance(app_config, TethysPortalConfig)) diff --git a/tests/unit_tests/test_tethys_config/test_context_processors.py b/tests/unit_tests/test_tethys_config/test_context_processors.py new file mode 100644 index 000000000..8cd9da31f --- /dev/null +++ b/tests/unit_tests/test_tethys_config/test_context_processors.py @@ -0,0 +1,45 @@ +import unittest +import mock + +from tethys_config.context_processors import tethys_global_settings_context + + +class TestTethysConfigContextProcessors(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('termsandconditions.models.TermsAndConditions') + @mock.patch('tethys_config.models.Setting') + def test_tethys_global_settings_context(self, mock_setting, mock_terms): + mock_request = mock.MagicMock() + mock_setting.as_dict.return_value = dict() + mock_terms.get_active_terms_list.return_value = ['active_terms'] + mock_terms.get_active_list.return_value = ['active_list'] + + ret = tethys_global_settings_context(mock_request) + + mock_setting.as_dict.assert_called_once() + mock_terms.get_active_terms_list.assert_called_once() + mock_terms.get_active_list.assert_not_called() + + self.assertEquals({'site_globals': {'documents': ['active_terms']}}, ret) + + @mock.patch('termsandconditions.models.TermsAndConditions') + @mock.patch('tethys_config.models.Setting') + def test_tethys_global_settings_context_exception(self, mock_setting, mock_terms): + mock_request = mock.MagicMock() + mock_setting.as_dict.return_value = dict() + mock_terms.get_active_terms_list.side_effect = AttributeError + mock_terms.get_active_list.return_value = ['active_list'] + + ret = tethys_global_settings_context(mock_request) + + mock_setting.as_dict.assert_called_once() + mock_terms.get_active_terms_list.assert_called_once() + mock_terms.get_active_list.assert_called_once_with(as_dict=False) + + self.assertEquals({'site_globals': {'documents': ['active_list']}}, ret) diff --git a/tests/unit_tests/test_tethys_config/test_init.py b/tests/unit_tests/test_tethys_config/test_init.py new file mode 100644 index 000000000..944120e1b --- /dev/null +++ b/tests/unit_tests/test_tethys_config/test_init.py @@ -0,0 +1,142 @@ +import unittest +import mock + +from tethys_config.init import initial_settings, reverse_init + + +class TestInit(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_config.init.timezone.now') + @mock.patch('tethys_config.init.SettingsCategory') + def test_initial_settings(self, mock_settings, mock_now): + mock_apps = mock.MagicMock() + mock_schema_editor = mock.MagicMock() + + initial_settings(apps=mock_apps, schema_editor=mock_schema_editor) + + mock_settings.assert_any_call(name='General Settings') + mock_settings(name='General Settings').save.assert_called() + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Favicon", + content="/tethys_portal/images/" + "default_favicon.png", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Brand Text", + content="Tethys Portal", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Brand Image", + content="/tethys_portal/images/" + "tethys-logo-75.png", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Brand Image Height", content="", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Brand Image Width", content="", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Brand Image Padding", + content="", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Apps Library Title", + content="Apps Library", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Primary Color", + content="#0a62a9", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Secondary Color", + content="#1b95dc", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Background Color", content="", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Primary Text Color", content="", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Primary Text Hover Color", + content="", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Secondary Text Color", + content="", + date_modified=mock_now.return_value) + mock_settings(name='General Settings').setting_set.create.assert_any_call(name="Secondary Text Hover Color", + content="", + date_modified=mock_now.return_value) + + # Home page settings + mock_settings.assert_any_call(name='Home Page') + mock_settings(name='Home Page').save.assert_called() + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Hero Text", + content="Welcome to Tethys Portal,\nthe hub " + "for your apps.", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Blurb Text", + content="Tethys Portal is designed to be " + "customizable, so that you can host " + "apps for your\norganization. You " + "can change everything on this page " + "from the Home Page settings.", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Feature 1 Heading", + content="Feature 1", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Feature 1 Body", + content="Use these features to brag about " + "all of the things users can do " + "with your instance of Tethys " + "Portal.", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Feature 1 Image", + content="/tethys_portal/images/" + "placeholder.gif", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Feature 2 Heading", + content="Feature 2", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Feature 2 Body", + content="Describe the apps and tools that " + "your Tethys Portal provides and " + "add custom pictures to each " + "feature as a finishing touch.", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Feature 2 Image", + content="/tethys_portal/images/" + "placeholder.gif", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Feature 3 Heading", + content="Feature 3", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Feature 3 Body", + content="You can change the color theme and " + "branding of your Tethys Portal in " + "a jiffy. Visit the Site Admin " + "settings from the user menu and " + "select General Settings.", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Feature 3 Image", + content="/tethys_portal/images/" + "placeholder.gif", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Call to Action", + content="Ready to get started?", + date_modified=mock_now.return_value) + mock_settings(name='Home Page').setting_set.create.assert_any_call(name="Call to Action Button", + content="Start Using Tethys!", + date_modified=mock_now.return_value) + + @mock.patch('tethys_config.init.Setting') + @mock.patch('tethys_config.init.SettingsCategory') + def test_reverse_init(self, mock_categories, mock_settings): + mock_apps = mock.MagicMock + mock_schema_editor = mock.MagicMock() + mock_cat = mock.MagicMock() + mock_set = mock.MagicMock() + mock_categories.objects.all.return_value = [mock_cat] + mock_settings.objects.all.return_value = [mock_set] + + reverse_init(apps=mock_apps, schema_editor=mock_schema_editor) + + mock_categories.objects.all.assert_called_once() + mock_settings.objects.all.assert_called_once() + mock_cat.delete.assert_called_once() + mock_set.delete.assert_called_once() diff --git a/tests/unit_tests/test_tethys_config/test_models/__init__.py b/tests/unit_tests/test_tethys_config/test_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_config/test_models/test_Setting.py b/tests/unit_tests/test_tethys_config/test_models/test_Setting.py new file mode 100644 index 000000000..ac6d4af08 --- /dev/null +++ b/tests/unit_tests/test_tethys_config/test_models/test_Setting.py @@ -0,0 +1,29 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_config.models import Setting + + +class SettingTest(TethysTestCase): + def set_up(self): + pass + + def tear_down(self): + pass + + def test_Setting_unicode(self): + set_title = Setting.objects.get(name='Site Title') + + # Check result + self.assertEqual('Site Title', str(set_title)) + + def test_Setting_str(self): + set_title = Setting.objects.get(name='Site Title') + + # Check result + self.assertEqual('Site Title', str(set_title)) + + def test_Setting_as_dict(self): + set_all = Setting.as_dict() + + # Check result + self.assertIsInstance(set_all, dict) + self.assertIn('site_title', set_all) diff --git a/tests/unit_tests/test_tethys_config/test_models/test_SettingsCategory.py b/tests/unit_tests/test_tethys_config/test_models/test_SettingsCategory.py new file mode 100644 index 000000000..7fbd4a14e --- /dev/null +++ b/tests/unit_tests/test_tethys_config/test_models/test_SettingsCategory.py @@ -0,0 +1,19 @@ +from tethys_sdk.testing import TethysTestCase +from tethys_config.models import SettingsCategory + + +class SettingsCategoryTest(TethysTestCase): + def set_up(self): + self.sc_gen = SettingsCategory.objects.get(name='General Settings') + self.sc_home = SettingsCategory.objects.get(name='Home Page') + + def tear_down(self): + pass + + def test_Settings_Category_unicode(self): + self.assertEqual('General Settings', str(self.sc_gen)) + self.assertEqual('Home Page', str(self.sc_home)) + + def test_Settings_Category_str(self): + self.assertEqual('General Settings', str(self.sc_gen)) + self.assertEqual('Home Page', str(self.sc_home)) diff --git a/tests/unit_tests/test_tethys_gizmos/__init__.py b/tests/unit_tests/test_tethys_gizmos/__init__.py new file mode 100644 index 000000000..fc1fe764a --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/__init__.py @@ -0,0 +1,8 @@ +""" +******************************************************************************** +* Name: __init__.py +* Author: nswain +* Created On: July 23, 2018 +* Copyright: (c) Aquaveo 2018 +******************************************************************************** +""" diff --git a/tests/unit_tests/test_tethys_gizmos/test_context_processors.py b/tests/unit_tests/test_tethys_gizmos/test_context_processors.py new file mode 100644 index 000000000..ff8ee2515 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_context_processors.py @@ -0,0 +1,16 @@ +import unittest +import tethys_gizmos.context_processors as gizmos_context_processor + + +class TestContextProcessor(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_tethys_gizmos_context(self): + result = gizmos_context_processor.tethys_gizmos_context('request') + + # Check Result + self.assertEqual({'gizmos_rendered': []}, result) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/__init__.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/__init__.py new file mode 100644 index 000000000..fc1fe764a --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/__init__.py @@ -0,0 +1,8 @@ +""" +******************************************************************************** +* Name: __init__.py +* Author: nswain +* Created On: July 23, 2018 +* Copyright: (c) Aquaveo 2018 +******************************************************************************** +""" diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_base.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_base.py new file mode 100644 index 000000000..7e1cd8e7e --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_base.py @@ -0,0 +1,59 @@ +""" +******************************************************************************** +* Name: base.py +* Author: nswain +* Created On: July 23, 2018 +* Copyright: (c) Aquaveo 2018 +******************************************************************************** +""" +import unittest +import tethys_gizmos.gizmo_options.base as basetest + + +class TestTethysGizmosBase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_TethysGizmoOptions(self): + test_dict = 'key1="value with spaces" key2="value_with_no_spaces"' + test_class = 'Map Type' + + result = basetest.TethysGizmoOptions(test_dict, test_class) + + self.assertIsInstance(result['attributes'], dict) + self.assertEqual('value with spaces', result['attributes']['key1']) + self.assertEqual('value_with_no_spaces', result['attributes']['key2']) + self.assertEqual('Map Type', result['classes']) + + def test_get_tethys_gizmos_js(self): + result = basetest.TethysGizmoOptions.get_tethys_gizmos_js() + self.assertIn('tethys_gizmos.js', result[0]) + self.assertNotIn('.css', result[0]) + + def test_get_tethys_gizmos_css(self): + result = basetest.TethysGizmoOptions.get_tethys_gizmos_css() + self.assertIn('tethys_gizmos.css', result[0]) + self.assertNotIn('.js', result[0]) + + def test_get_vendor_js(self): + result = basetest.TethysGizmoOptions.get_vendor_js() + self.assertFalse(result) + + def test_get_gizmo_js(self): + result = basetest.TethysGizmoOptions.get_gizmo_js() + self.assertFalse(result) + + def test_get_vendor_css(self): + result = basetest.TethysGizmoOptions.get_vendor_css() + self.assertFalse(result) + + def test_get_gizmo_css(self): + result = basetest.TethysGizmoOptions.get_gizmo_css() + self.assertFalse(result) + + def test_SecondaryGizmoOptions(self): + result = basetest.SecondaryGizmoOptions() + self.assertFalse(result) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_bokeh_view.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_bokeh_view.py new file mode 100644 index 000000000..bba71f653 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_bokeh_view.py @@ -0,0 +1,31 @@ +import unittest +import tethys_gizmos.gizmo_options.bokeh_view as bokeh_view +from bokeh.plotting import figure + + +class TestBokehView(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_BokehView(self): + plot = figure(plot_height=300) + plot.circle([1, 2], [3, 4]) + attr = {'title': 'test title', 'description': 'test attributes'} + result = bokeh_view.BokehView(plot, attributes=attr) + + self.assertIn('test attributes', result['attributes']['description']) + self.assertIn('Circle', result['script']) + + def test_get_vendor_css(self): + result = bokeh_view.BokehView.get_vendor_css() + + self.assertIn('.css', result[0]) + self.assertNotIn('.js', result[0]) + + def test_get_vendor_js(self): + result = bokeh_view.BokehView.get_vendor_js() + self.assertIn('.js', result[0]) + self.assertNotIn('.css', result[0]) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_button.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_button.py new file mode 100644 index 000000000..89ec8f967 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_button.py @@ -0,0 +1,38 @@ +import unittest +import tethys_gizmos.gizmo_options.button as gizmo_button + + +class TestButton(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_ButtonGroup(self): + buttons = [{'display_text': 'Add', 'style': 'success'}, + {'display_text': 'Delete', 'style': 'danger'}] + result = gizmo_button.ButtonGroup(buttons) + + self.assertIn(buttons[0], result['buttons']) + self.assertIn(buttons[1], result['buttons']) + + def test_Button(self): + display_text = 'Add' + name = 'Aquaveo' + style = 'success' + icon = 'glyphicon glyphicon-globe' + href = 'linktest' + attr = {'title': 'test title', 'description': 'test attributes'} + test_class = 'Test Class' + result = gizmo_button.Button(display_text=display_text, name=name, style=style, + icon=icon, href=href, submit=True, disabled=False, + attributes=attr, classes=test_class) + + self.assertEqual(display_text, result['display_text']) + self.assertEqual(name, result['name']) + self.assertEqual(style, result['style']) + self.assertEqual(icon, result['icon']) + self.assertEqual(href, result['href']) + self.assertEqual(attr, result['attributes']) + self.assertEqual(test_class, result['classes']) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_datatable_view.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_datatable_view.py new file mode 100644 index 000000000..dd6b7936e --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_datatable_view.py @@ -0,0 +1,41 @@ +import unittest +import tethys_gizmos.gizmo_options.datatable_view as gizmo_datatable_view + + +class TestDatatableView(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_DataTableView(self): + column_names = ['Name', 'Age', 'Job'] + datatable_options = {'rows': [['Bill', '30', 'contractor'], ['Fred', '18', 'programmer']]} + rows = 2 + result = gizmo_datatable_view.DataTableView(rows=rows, column_names=column_names, + datatable_options=datatable_options) + # Check Result + self.assertEqual(rows, result['rows']) + self.assertEqual(column_names, result['column_names']) + self.assertIn(datatable_options['rows'][0][0], result['datatable_options']['datatable_options']) + self.assertIn(datatable_options['rows'][0][1], result['datatable_options']['datatable_options']) + self.assertIn(datatable_options['rows'][0][2], result['datatable_options']['datatable_options']) + self.assertIn(datatable_options['rows'][1][0], result['datatable_options']['datatable_options']) + self.assertIn(datatable_options['rows'][1][1], result['datatable_options']['datatable_options']) + self.assertIn(datatable_options['rows'][1][2], result['datatable_options']['datatable_options']) + + result = gizmo_datatable_view.DataTableView.get_vendor_css() + # Check Result + self.assertIn('.css', result[0]) + self.assertNotIn('.js', result[0]) + + result = gizmo_datatable_view.DataTableView.get_vendor_js() + # Check Result + self.assertIn('.js', result[0]) + self.assertNotIn('.css', result[0]) + + result = gizmo_datatable_view.DataTableView.get_gizmo_js() + # Check Result + self.assertIn('.js', result[0]) + self.assertNotIn('.css', result[0]) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_date_picker.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_date_picker.py new file mode 100644 index 000000000..897c54425 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_date_picker.py @@ -0,0 +1,42 @@ +import unittest +import tethys_gizmos.gizmo_options.date_picker as gizmo_date_picker + + +class TestButton(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_DatePicker(self): + name = 'Date Picker' + display_text = 'Unit Test' + autoclose = True + calendar_weeks = True + clear_button = True + days_of_week_disabled = '6' + min_view_mode = 'days' + + result = gizmo_date_picker.DatePicker(name=name, display_text=display_text, autoclose=autoclose, + calendar_weeks=calendar_weeks, clear_button=clear_button, + days_of_week_disabled=days_of_week_disabled, min_view_mode=min_view_mode) + + # Check Result + self.assertIn(name, result['name']) + self.assertIn(display_text, result['display_text']) + self.assertTrue(result['autoclose']) + self.assertTrue(result['calendar_weeks']) + self.assertTrue(result['clear_button']) + self.assertIn(days_of_week_disabled, result['days_of_week_disabled']) + self.assertIn(min_view_mode, result['min_view_mode']) + + result = gizmo_date_picker.DatePicker.get_vendor_css() + + # Check Result + self.assertIn('.css', result[0]) + self.assertNotIn('.js', result[0]) + + result = gizmo_date_picker.DatePicker.get_vendor_js() + self.assertIn('.js', result[0]) + self.assertNotIn('.css', result[0]) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_esri_map.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_esri_map.py new file mode 100644 index 000000000..2acab3e65 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_esri_map.py @@ -0,0 +1,51 @@ +import unittest +import tethys_gizmos.gizmo_options.esri_map as gizmo_esri_map + + +class TestESRI(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_ESRIMap(self): + layers = ['layer1', 'layer2'] + basemap = 'Aerial' + result = gizmo_esri_map.ESRIMap(basemap=basemap, layers=layers) + # Check Result + self.assertIn(basemap, result['basemap']) + self.assertEqual(layers, result['layers']) + + result = gizmo_esri_map.ESRIMap.get_vendor_js() + # Check Result + self.assertIn('js', result[0]) + self.assertNotIn('css', result[0]) + + result = gizmo_esri_map.ESRIMap.get_gizmo_js() + # Check Result + self.assertIn('.js', result[0]) + self.assertNotIn('.css', result[0]) + + result = gizmo_esri_map.ESRIMap.get_vendor_css() + # Check Result + self.assertIn('.css', result[0]) + self.assertNotIn('.js', result[0]) + + def test_EMView(self): + center = ['40.276039', '-111.651120'] + zoom = 4 + + result = gizmo_esri_map.EMView(center=center, zoom=zoom) + # Check Result + self.assertEqual(zoom, result['zoom']) + self.assertEqual(center, result['center']) + + def test_EMLayer(self): + type = 'ImageryLayer' + url = 'www.aquaveo.com' + + result = gizmo_esri_map.EMLayer(type=type, url=url) + # Check Result + self.assertEqual(type, result['type']) + self.assertEqual(url, result['url']) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_google_map_view.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_google_map_view.py new file mode 100644 index 000000000..cc47ae487 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_google_map_view.py @@ -0,0 +1,47 @@ +import unittest +import tethys_gizmos.gizmo_options.google_map_view as gizmo_google_map_view + + +class TestGoogleMapView(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_GoogleMapView(self): + height = '600px' + width = '80%' + maps_api_key = 'api-key' + reference_kml_action = 'gizmos:get_kml' + drawing_types_enabled = ['POLYGONS', 'POINTS', 'POLYLINES'] + initial_drawing_mode = 'POINTS' + output_format = 'WKT' + result = gizmo_google_map_view.GoogleMapView(height=height, width=width, maps_api_key=maps_api_key, + reference_kml_action=reference_kml_action, + output_format=output_format, + drawing_types_enabled=drawing_types_enabled, + initial_drawing_mode=initial_drawing_mode) + # Check Result + self.assertIn(height, result['height']) + self.assertIn(width, result['width']) + self.assertIn(maps_api_key, result['maps_api_key']) + self.assertIn(reference_kml_action, result['reference_kml_action']) + self.assertEqual(drawing_types_enabled, result['drawing_types_enabled']) + self.assertIn(initial_drawing_mode, result['initial_drawing_mode']) + self.assertIn(output_format, result['output_format']) + + result = gizmo_google_map_view.GoogleMapView.get_vendor_js() + # Check Result + self.assertIn('.js', result[0]) + self.assertNotIn('.css', result[0]) + + result = gizmo_google_map_view.GoogleMapView.get_vendor_css() + # Check Result + self.assertIn('.css', result[0]) + self.assertNotIn('.js', result[0]) + + result = gizmo_google_map_view.GoogleMapView.get_gizmo_js() + # Check Result + self.assertIn('.js', result[0]) + self.assertNotIn('.css', result[0]) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_jobs_table.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_jobs_table.py new file mode 100644 index 000000000..2f5ccddd3 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_jobs_table.py @@ -0,0 +1,79 @@ +import unittest +import tethys_gizmos.gizmo_options.jobs_table as gizmo_jobs_table +import mock + + +class JobObject(object): + def __init__(self, id, name, description, creation_time, run_time): + self.id = id + self.name = name + self.description = description + self.creation_time = creation_time + self.run_time = run_time + + +class TestJobsTable(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_gizmos.gizmo_options.jobs_table.JobsTable.set_rows_and_columns') + def test_JobsTable_init(self, mock_set): + job1 = JobObject(1, 'name1', 'des1', 1, 1) + job2 = JobObject(2, 'name2', 'des2', 2, 2) + jobs = [job1, job2] + column_fields = ['id', 'name', 'description', 'creation_time', 'run_time'] + + ret = gizmo_jobs_table.JobsTable(jobs=jobs, column_fields=column_fields) + + mock_set.assert_called_with(jobs, ['id', 'name', 'description', 'creation_time', 'run_time']) + self.assertTrue(ret.status_actions) + self.assertTrue(ret.run) + self.assertTrue(ret.delete) + self.assertTrue(ret.delay_loading_status) + self.assertFalse(ret.hover) + self.assertFalse(ret.bordered) + self.assertFalse(ret.striped) + self.assertFalse(ret.condensed) + self.assertFalse(ret.attributes) + self.assertEqual('', ret.results_url) + self.assertEqual('', ret.classes) + self.assertEqual(5000, ret.refresh_interval) + + def test_set_rows_and_columns(self): + job1 = JobObject(1, 'name1', 'des1', 1, 1) + job2 = JobObject(2, 'name2', 'des2', 2, 2) + jobs = [job1, job2] + column_fields = ['id', 'name', 'description', 'creation_time', 'run_time'] + + # This set_rows_and_columns method is called at the init + result = gizmo_jobs_table.JobsTable(jobs=jobs, column_fields=column_fields) + self.assertIn(job1.id, result['rows'][0]) + self.assertIn(job1.name, result['rows'][0]) + self.assertIn(job1.description, result['rows'][0]) + self.assertIn(job1.creation_time, result['rows'][0]) + self.assertIn(job1.run_time, result['rows'][0]) + self.assertIn(job2.id, result['rows'][1]) + self.assertIn(job2.name, result['rows'][1]) + self.assertIn(job2.description, result['rows'][1]) + self.assertIn(job2.creation_time, result['rows'][1]) + self.assertIn(job2.run_time, result['rows'][1]) + self.assertTrue(result['status_actions']) + + @mock.patch('tethys_gizmos.gizmo_options.jobs_table.log.warning') + def test_set_rows_and_columns_warning(self, mock_log): + job1 = JobObject(1, 'name1', 'des1', 1, 1) + jobs = [job1] + column_name = 'not_exist' + column_fields = [column_name] + + gizmo_jobs_table.JobsTable(jobs=jobs, column_fields=column_fields) + + mock_log.assert_called_with('Column %s was not added because the %s has no attribute %s.', + 'Not Exist', str(job1), column_name) + + def test_get_gizmo_js(self): + self.assertIn('jobs_table.js', gizmo_jobs_table.JobsTable.get_gizmo_js()[0]) + self.assertNotIn('.css', gizmo_jobs_table.JobsTable.get_gizmo_js()[0]) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_map_view.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_map_view.py new file mode 100644 index 000000000..d13fb176d --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_map_view.py @@ -0,0 +1,210 @@ +import unittest +import tethys_gizmos.gizmo_options.map_view as gizmo_map_view +import mock + + +class MockObject(object): + def __init__(self, debug=True): + self.DEBUG = debug + + +class TestMapView(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_MapView(self): + height = '500px' + width = '90%' + basemap = 'Aerial' + controls = ['ZoomSlider', 'Rotate', 'FullScreen', 'ScaleLine'] + + result = gizmo_map_view.MapView(height=height, width=width, basemap=basemap, controls=controls) + + # Check Result + self.assertIn(height, result['height']) + self.assertIn(width, result['width']) + self.assertIn(basemap, result['basemap']) + self.assertEqual(controls, result['controls']) + + self.assertIn('.js', gizmo_map_view.MapView.get_vendor_js()[0]) + self.assertIn('.js', gizmo_map_view.MapView.get_gizmo_js()[0]) + self.assertIn('.css', gizmo_map_view.MapView.get_vendor_css()[0]) + self.assertIn('.css', gizmo_map_view.MapView.get_gizmo_css()[0]) + + @mock.patch('tethys_gizmos.gizmo_options.map_view.settings') + def test_MapView_debug(self, mock_settings): + ms = mock_settings() + ms.return_value = MockObject() + gizmo_map_view.MapView.ol_version = '4.6.5' + self.assertIn('-debug.js', gizmo_map_view.MapView.get_vendor_js()[0]) + + def test_MVView(self): + projection = 'EPSG:4326' + center = [-100, 40] + zoom = 10 + maxZoom = 20 + minZoom = 2 + + result = gizmo_map_view.MVView(projection=projection, center=center, zoom=zoom, + maxZoom=maxZoom, minZoom=minZoom) + + # Check result + self.assertIn(projection, result['projection']) + self.assertEqual(center, result['center']) + self.assertEqual(zoom, result['zoom']) + self.assertEqual(maxZoom, result['maxZoom']) + self.assertEqual(minZoom, result['minZoom']) + + def test_MVDraw(self): + controls = ['Modify', 'Delete', 'Move', 'Point', 'LineString', 'Polygon', 'Box'] + initial = 'Point' + output_format = 'GeoJSON' + fill_color = 'rgba(255,255,255,0.2)' + line_color = '#663399' + point_color = '#663399' + + result = gizmo_map_view.MVDraw(controls=controls, initial=initial, output_format=output_format, + line_color=line_color, fill_color=fill_color, point_color=point_color) + + # Check result + self.assertEqual(controls, result['controls']) + self.assertEqual(initial, result['initial']) + self.assertEqual(output_format, result['output_format']) + self.assertEqual(fill_color, result['fill_color']) + self.assertEqual(line_color, result['line_color']) + self.assertEqual(point_color, result['point_color']) + + def test_MVDraw_no_ini(self): + controls = ['Modify'] + + # Raise Error if Initial is not in Controls list + self.assertRaises(ValueError, gizmo_map_view.MVDraw, controls=controls, initial='Point') + + def test_MVLayer(self): + source = 'KML' + legend_title = 'Park City Watershed' + options = {'url': '/static/tethys_gizmos/data/model.kml'} + + result = gizmo_map_view.MVLayer(source=source, legend_title=legend_title, options=options) + + # Check Result + self.assertEqual(source, result['source']) + self.assertEqual(legend_title, result['legend_title']) + self.assertEqual(options, result['options']) + + @mock.patch('tethys_gizmos.gizmo_options.map_view.log.warning') + def test_MVLayer_warning(self, mock_log): + source = 'KML' + legend_title = 'Park City Watershed' + options = {'url': '/static/tethys_gizmos/data/model.kml'} + feature_selection = True + + gizmo_map_view.MVLayer(source=source, legend_title=legend_title, options=options, + feature_selection=feature_selection) + + mock_log.assert_called_with("geometry_attribute not defined -using default value 'the_geom'") + + def test_MVLegendClass(self): + # Point + type_value = 'point' + value = 'Point Legend' + fill = '#00ff00' + + result = gizmo_map_view.MVLegendClass(type=type_value, value=value, fill=fill) + + # Check Result + self.assertEqual(value, result['value']) + self.assertEqual(fill, result['fill']) + self.assertEqual(value, result['value']) + + # Point with No Fill + self.assertRaises(ValueError, gizmo_map_view.MVLegendClass, type=type_value, value=value) + + # Not Valid Type + self.assertRaises(ValueError, gizmo_map_view.MVLegendClass, type='points', value=value) + + # Line + type_value = 'line' + value = 'Line Legend' + stroke = '#00ff01' + result = gizmo_map_view.MVLegendClass(type=type_value, value=value, stroke=stroke) + + # Check Result + self.assertEqual(type_value, result['type']) + self.assertEqual(stroke, result['stroke']) + self.assertEqual(value, result['value']) + + # Line with No Stroke + self.assertRaises(ValueError, gizmo_map_view.MVLegendClass, type=type_value, value=value) + + # Polygon + type_value = 'polygon' + value = 'Polygon Legend' + fill = '#00ff00' + stroke = '#00ff01' + + result = gizmo_map_view.MVLegendClass(type=type_value, value=value, stroke=stroke, fill=fill) + + # Check Result + self.assertEqual(type_value, result['type']) + self.assertEqual(stroke, result['stroke']) + self.assertEqual(fill, result['fill']) + self.assertEqual(value, result['value']) + + # Polygon with no Stroke + result = gizmo_map_view.MVLegendClass(type=type_value, value=value, fill=fill) + + # Check Result + self.assertEqual(type_value, result['type']) + self.assertEqual(fill, result['fill']) + self.assertEqual(fill, result['line']) + + # Polygon with no fill + self.assertRaises(ValueError, gizmo_map_view.MVLegendClass, type=type_value, value=value) + + # Raster + type_value = 'raster' + value = 'Raster Legend' + ramp = ['#00ff00', '#00ff01', '#00ff02'] + + result = gizmo_map_view.MVLegendClass(type=type_value, value=value, ramp=ramp) + + # Check Result + self.assertEqual(type_value, result['type']) + self.assertEqual(ramp, result['ramp']) + + # Raster without ramp + self.assertRaises(ValueError, gizmo_map_view.MVLegendClass, type=type_value, value=value) + + def test_MVLegendImageClass(self): + value = 'image legend' + image_url = 'www.aquaveo.com/image.png' + + result = gizmo_map_view.MVLegendImageClass(value=value, image_url=image_url) + + # Check Result + self.assertEqual(value, result['value']) + self.assertEqual(image_url, result['image_url']) + + def test_MVLegendGeoServerImageClass(self): + value = 'Cities' + geoserver_url = 'http://localhost:8181/geoserver' + style = 'green' + layer = 'rivers' + width = 20 + height = 10 + + image_url = "{0}/wms?REQUEST=GetLegendGraphic&VERSION=1.0.0&" \ + "STYLE={1}&FORMAT=image/png&WIDTH={2}&HEIGHT={3}&" \ + "LEGEND_OPTIONS=forceRule:true&" \ + "LAYER={4}".format(geoserver_url, style, width, height, layer) + + result = gizmo_map_view.MVLegendGeoServerImageClass(value=value, geoserver_url=geoserver_url, + style=style, layer=layer, width=width, height=height) + + # Check Result + self.assertEqual(value, result['value']) + self.assertEqual(image_url, result['image_url']) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_message_box.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_message_box.py new file mode 100644 index 000000000..275c0b529 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_message_box.py @@ -0,0 +1,20 @@ +import unittest +import tethys_gizmos.gizmo_options.message_box as gizmo_message_box + + +class TestMessageBox(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_MessageBox(self): + name = 'MB Name' + title = 'MB Title' + + result = gizmo_message_box.MessageBox(name=name, title=title) + + # Check Result + self.assertEqual(name, result['name']) + self.assertEqual(title, result['title']) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_plot_view.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_plot_view.py new file mode 100644 index 000000000..f835bde41 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_plot_view.py @@ -0,0 +1,259 @@ +import unittest +import tethys_gizmos.gizmo_options.plot_view as gizmo_plot_view +import datetime + + +class TestPlotView(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_PlotViewBase(self): + engine = 'highcharts' + result = gizmo_plot_view.PlotViewBase(engine=engine) + + # Check Result + self.assertEqual(engine, result['engine']) + + # Engine is not d3 or hightcharts + self.assertRaises(ValueError, gizmo_plot_view.PlotViewBase, engine='d2') + + # Check Get Method + self.assertIn('.js', gizmo_plot_view.PlotViewBase.get_vendor_js()[0]) + self.assertNotIn('.css', gizmo_plot_view.PlotViewBase.get_vendor_js()[0]) + + self.assertIn('.js', gizmo_plot_view.PlotViewBase.get_gizmo_js()[0]) + self.assertNotIn('.css', gizmo_plot_view.PlotViewBase.get_gizmo_js()[0]) + + self.assertIn('.css', gizmo_plot_view.PlotViewBase.get_gizmo_css()[0]) + self.assertNotIn('.js', gizmo_plot_view.PlotViewBase.get_gizmo_css()[0]) + + def test_PlotObject(self): + chart = 'test chart' + xAxis = 'Distance' + yAxis = 'Time' + title = 'Title' + subtitle = 'Subtitle' + tooltip_format = 'Format' + custom = {'key1': 'value1', 'key2': 'value2'} + result = gizmo_plot_view.PlotObject(chart=chart, xAxis=xAxis, yAxis=yAxis, title=title, + subtitle=subtitle, tooltip_format=tooltip_format, custom=custom) + # Check Result + self.assertEqual(chart, result['chart']) + self.assertEqual(xAxis, result['xAxis']) + self.assertEqual(yAxis, result['yAxis']) + self.assertEqual(title, result['title']['text']) + self.assertEqual(subtitle, result['subtitle']['text']) + self.assertEqual(tooltip_format, result['tooltip']) + self.assertIn(custom['key1'], result['custom']['key1']) + self.assertIn(custom['key2'], result['custom']['key2']) + + def test_LinePlot(self): + series = [ + { + 'name': 'Air Temp', + 'color': '#0066ff', + 'marker': {'enabled': False}, + 'data': [ + [0, 5], [10, -70], + [20, -86.5], [30, -66.5], + [40, -32.1], + [50, -12.5], [60, -47.7], + [70, -85.7], [80, -106.5] + ] + }, + { + 'name': 'Water Temp', + 'color': '#ff6600', + 'data': [ + [0, 15], [10, -50], + [20, -56.5], [30, -46.5], + [40, -22.1], + [50, -2.5], [60, -27.7], + [70, -55.7], [80, -76.5] + ] + } + ] + + result = gizmo_plot_view.LinePlot(series=series) + # Check result + self.assertEqual(series[0]['color'], result['plot_object']['series'][0]['color']) + self.assertEqual(series[0]['name'], result['plot_object']['series'][0]['name']) + self.assertEqual(series[0]['data'], result['plot_object']['series'][0]['data']) + + x_axis_title = 'Distance' + x_axis_units = 'm' + y_axis_title = 'Time' + y_axis_units = 's' + + result = gizmo_plot_view.LinePlot(series=series, spline=True, x_axis_title=x_axis_title, + y_axis_title=y_axis_title, x_axis_units=x_axis_units, + y_axis_units=y_axis_units) + + # Check result + x_text = '{0} ({1})'.format(x_axis_title, x_axis_units) + y_text = '{0} ({1})'.format(y_axis_title, y_axis_units) + self.assertEqual(x_text, result['plot_object']['xAxis']['title']['text']) + self.assertEqual(y_text, result['plot_object']['yAxis']['title']['text']) + + def test_PolarPlot(self): + series = [ + { + 'name': 'Park City', + 'data': [0.2, 0.5, 0.1, 0.8, 0.2, 0.6, 0.8, 0.3], + 'pointPlacement': 'on' + }, + { + 'name': 'Little Dell', + 'data': [0.8, 0.3, 0.2, 0.5, 0.1, 0.8, 0.2, 0.6], + 'pointPlacement': 'on' + } + ] + + result = gizmo_plot_view.PolarPlot(series=series) + # Check result + self.assertEqual(series[0]['pointPlacement'], result['plot_object']['series'][0]['pointPlacement']) + self.assertEqual(series[0]['name'], result['plot_object']['series'][0]['name']) + self.assertEqual(series[0]['data'], result['plot_object']['series'][0]['data']) + + def test_ScatterPlot(self): + male_dataset = { + 'name': 'Male', + 'color': '#0066ff', + 'data': [ + [174.0, 65.6], [175.3, 71.8], [193.5, 80.7], [186.5, 72.6] + ] + } + + female_dataset = { + 'name': 'Female', + 'color': '#ff6600', + 'data': [ + [161.2, 51.6], [167.5, 59.0], [159.5, 49.2], [157.0, 63.0] + ] + } + + series = [male_dataset, female_dataset] + x_axis_title = 'Distance' + x_axis_units = 'm' + y_axis_title = 'Time' + y_axis_units = 's' + + result = gizmo_plot_view.ScatterPlot(series=series, x_axis_title=x_axis_title, + y_axis_title=y_axis_title, x_axis_units=x_axis_units, + y_axis_units=y_axis_units) + + # Check result + x_text = '{0} ({1})'.format(x_axis_title, x_axis_units) + y_text = '{0} ({1})'.format(y_axis_title, y_axis_units) + self.assertEqual(x_text, result['plot_object']['xAxis']['title']['text']) + self.assertEqual(y_text, result['plot_object']['yAxis']['title']['text']) + self.assertEqual(series[0]['color'], result['plot_object']['series'][0]['color']) + self.assertEqual(series[0]['name'], result['plot_object']['series'][0]['name']) + self.assertEqual(series[0]['data'], result['plot_object']['series'][0]['data']) + self.assertEqual(series[1]['color'], result['plot_object']['series'][1]['color']) + self.assertEqual(series[1]['name'], result['plot_object']['series'][1]['name']) + self.assertEqual(series[1]['data'], result['plot_object']['series'][1]['data']) + + def test_PiePlot(self): + series = [ + { + 'name': 'Park City', + 'data': [0.2, 0.5, 0.1, 0.8, 0.2, 0.6, 0.8, 0.3] + }, + ] + + result = gizmo_plot_view.PiePlot(series=series) + + # Check result + self.assertEqual(series[0]['name'], result['plot_object']['series'][0]['name']) + self.assertEqual(series[0]['data'], result['plot_object']['series'][0]['data']) + + def test_BarPlot(self): + series = [{ + 'name': "Year 1800", + 'data': [100, 31, 635, 203, 275, 487, 872, 671, 736, 568, 487, 432] + }, { + 'name': "Year 1900", + 'data': [133, 200, 947, 408, 682, 328, 917, 171, 482, 140, 176, 237] + } + ] + + result = gizmo_plot_view.BarPlot(series=series) + # Check result + self.assertEqual(series[0]['name'], result['plot_object']['series'][0]['name']) + self.assertEqual(series[0]['data'], result['plot_object']['series'][0]['data']) + + axis_title = 'Population' + axis_units = 'Millions' + result = gizmo_plot_view.BarPlot(series=series, horizontal=True, axis_title=axis_title, + axis_units=axis_units) + # Check result + y_text = '{0} ({1})'.format(axis_title, axis_units) + self.assertEqual(y_text, result['plot_object']['yAxis']['title']['text']) + self.assertEqual(series[0]['name'], result['plot_object']['series'][0]['name']) + self.assertEqual(series[0]['data'], result['plot_object']['series'][0]['data']) + + def test_TimeSeries(self): + series = [{ + 'name': 'Winter 2007-2008', + 'data': [ + ['12/02/2008', 0.8], + ['12/09/2008', 0.6] + ] + }] + + result = gizmo_plot_view.TimeSeries(series=series) + # Check result + self.assertEqual(series[0]['name'], result['plot_object']['series'][0]['name']) + self.assertEqual(series[0]['data'], result['plot_object']['series'][0]['data']) + + def test_AreaRange(self): + averages = [ + [datetime.date(2009, 7, 1), 21.5], [datetime.date(2009, 7, 2), 22.1], [datetime.date(2009, 7, 3), 23] + ] + ranges = [ + [datetime.date(2009, 7, 1), 14.3, 27.7], [datetime.date(2009, 7, 2), 14.5, 27.8], + [datetime.date(2009, 7, 3), 15.5, 29.6] + ] + series = [{ + 'name': 'Temperature', + 'data': averages, + 'zIndex': 1, + 'marker': { + 'lineWidth': 2, + } + }, { + 'name': 'Range', + 'data': ranges, + 'type': 'arearange', + 'lineWidth': 0, + 'linkedTo': ':previous', + 'fillOpacity': 0.3, + 'zIndex': 0 + }] + result = gizmo_plot_view.AreaRange(series=series) + # Check result + self.assertEqual(series[0]['name'], result['plot_object']['series'][0]['name']) + self.assertEqual(series[0]['data'], result['plot_object']['series'][0]['data']) + + def test_HeatMap(self): + sales_data = [ + [0, 0, 10], [0, 1, 19], [0, 2, 8], [0, 3, 24], [0, 4, 67], [1, 0, 92] + ] + series = [{ + 'name': 'Sales per employee', + 'borderWidth': 1, + 'data': sales_data, + 'dataLabels': { + 'enabled': True, + 'color': '#000000' + } + }] + + result = gizmo_plot_view.HeatMap(series=series) + # Check result + self.assertEqual(series[0]['name'], result['plot_object']['series'][0]['name']) + self.assertEqual(series[0]['data'], result['plot_object']['series'][0]['data']) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_plotly_view.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_plotly_view.py new file mode 100644 index 000000000..ba32c734c --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_plotly_view.py @@ -0,0 +1,32 @@ +import unittest +import tethys_gizmos.gizmo_options.plotly_view as gizmo_plotly_view +import plotly.graph_objs as go + + +class TestPlotlyView(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_PlotlyView(self): + trace0 = go.Scatter( + x=[1, 2, 3, 4], + y=[10, 15, 13, 17] + ) + trace1 = go.Scatter( + x=[1, 2, 3, 4], + y=[16, 5, 11, 9] + ) + plot_input = [trace0, trace1] + + result = gizmo_plotly_view.PlotlyView(plot_input) + # Check Result + self.assertIn(', '.join(str(e) for e in trace0.x), result['plotly_div']) + self.assertIn(', '.join(str(e) for e in trace0.y), result['plotly_div']) + self.assertIn(', '.join(str(e) for e in trace1.x), result['plotly_div']) + self.assertIn(', '.join(str(e) for e in trace1.y), result['plotly_div']) + + self.assertIn('.js', gizmo_plotly_view.PlotlyView.get_vendor_js()[0]) + self.assertNotIn('.css', gizmo_plotly_view.PlotlyView.get_vendor_js()[0]) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_range_slider.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_range_slider.py new file mode 100644 index 000000000..715475061 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_range_slider.py @@ -0,0 +1,29 @@ +import unittest +import tethys_gizmos.gizmo_options.range_slider as gizmo_range_slider + + +class TestRangeSlider(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_RangeSlider(self): + name = 'Test Range Slider' + min = 0 + max = 100 + initial = 50 + step = 1 + + result = gizmo_range_slider.RangeSlider(name=name, min=min, max=max, initial=initial, step=step) + + # Check Result + self.assertEqual(name, result['name']) + self.assertEqual(min, result['min']) + self.assertEqual(max, result['max']) + self.assertEqual(initial, result['initial']) + self.assertEqual(step, result['step']) + + self.assertIn('.js', gizmo_range_slider.RangeSlider.get_gizmo_js()[0]) + self.assertNotIn('.css', gizmo_range_slider.RangeSlider.get_gizmo_js()[0]) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_select_input.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_select_input.py new file mode 100644 index 000000000..7633e5bc2 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_select_input.py @@ -0,0 +1,35 @@ +import unittest +import tethys_gizmos.gizmo_options.select_input as gizmo_select_input + + +class TestSelectInput(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_SelectInput(self): + display_text = 'Select2 Multiple' + name = 'select21' + multiple = True + options = [('One', '1'), ('Two', '2'), ('Three', '3')] + initial = ['Two', 'One'] + + result = gizmo_select_input.SelectInput(name=name, display_text=display_text, multiple=multiple, + options=options, initial=initial) + + self.assertEqual(name, result['name']) + self.assertEqual(display_text, result['display_text']) + self.assertTrue(result['multiple']) + self.assertEqual(options, result['options']) + self.assertEqual(initial, result['initial']) + + self.assertIn('.js', gizmo_select_input.SelectInput.get_vendor_js()[0]) + self.assertNotIn('.css', gizmo_select_input.SelectInput.get_vendor_js()[0]) + + self.assertIn('.css', gizmo_select_input.SelectInput.get_vendor_css()[0]) + self.assertNotIn('.js', gizmo_select_input.SelectInput.get_vendor_css()[0]) + + self.assertIn('.js', gizmo_select_input.SelectInput.get_gizmo_js()[0]) + self.assertNotIn('.css', gizmo_select_input.SelectInput.get_gizmo_js()[0]) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_table_view.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_table_view.py new file mode 100644 index 000000000..a9b970dd4 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_table_view.py @@ -0,0 +1,21 @@ +import unittest +import tethys_gizmos.gizmo_options.table_view as gizmo_table_view + + +class TestTableView(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_TableView(self): + rows = [('Bill', '30', 'contractor'), + ('Fred', '18', 'programmer'), + ('Bob', '26', 'boss')] + column_names = ['Name', 'Age', 'Job'] + + result = gizmo_table_view.TableView(rows=rows, column_names=column_names) + + self.assertEqual(rows, result['rows']) + self.assertEqual(column_names, result['column_names']) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_text_input.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_text_input.py new file mode 100644 index 000000000..0497075da --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_text_input.py @@ -0,0 +1,26 @@ +import unittest +import tethys_gizmos.gizmo_options.text_input as gizmo_text_input + + +class TestTextInput(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_TextInput(self): + display_text = 'Text Error' + name = 'inputEmail' + initial = 'bob@example.com' + icon_append = 'glyphicon glyphicon-envelope' + error = 'Here is my error text' + + result = gizmo_text_input.TextInput(name=name, display_text=display_text, initial=initial, + icon_append=icon_append, error=error) + # Check Result + self.assertEqual(display_text, result['display_text']) + self.assertEqual(name, result['name']) + self.assertEqual(initial, result['initial']) + self.assertEqual(icon_append, result['icon_append']) + self.assertEqual(error, result['error']) diff --git a/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_toogle_switch.py b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_toogle_switch.py new file mode 100644 index 000000000..f67b64844 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_gizmo_options/test_toogle_switch.py @@ -0,0 +1,41 @@ +import unittest +import tethys_gizmos.gizmo_options.toggle_switch as gizmo_toggle_switch + + +class TestToggleSwitch(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_ToggleSwitch(self): + display_text = 'Styled Toggle' + name = 'toggle2' + on_label = 'Yes' + off_label = 'No' + on_style = 'success' + off_style = 'danger' + initial = True + size = 'large' + + result = gizmo_toggle_switch.ToggleSwitch(name=name, display_text=display_text, on_label=on_label, + off_label=off_label, on_style=on_style, off_style=off_style, + initial=initial, size=size) + # Check Result + self.assertEqual(display_text, result['display_text']) + self.assertEqual(name, result['name']) + self.assertEqual(on_label, result['on_label']) + self.assertEqual(off_label, result['off_label']) + self.assertEqual(on_style, result['on_style']) + self.assertTrue(result['initial']) + self.assertEqual(size, result['size']) + + self.assertIn('.js', gizmo_toggle_switch.ToggleSwitch.get_vendor_js()[0]) + self.assertNotIn('.css', gizmo_toggle_switch.ToggleSwitch.get_vendor_js()[0]) + + self.assertIn('.css', gizmo_toggle_switch.ToggleSwitch.get_vendor_css()[0]) + self.assertNotIn('.js', gizmo_toggle_switch.ToggleSwitch.get_vendor_css()[0]) + + self.assertIn('.js', gizmo_toggle_switch.ToggleSwitch.get_gizmo_js()[0]) + self.assertNotIn('.css', gizmo_toggle_switch.ToggleSwitch.get_gizmo_js()[0]) diff --git a/tests/unit_tests/test_tethys_gizmos/test_templatetags/__init__.py b/tests/unit_tests/test_tethys_gizmos/test_templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_gizmos/test_templatetags/test_tethys_gizmos.py b/tests/unit_tests/test_tethys_gizmos/test_templatetags/test_tethys_gizmos.py new file mode 100644 index 000000000..c6ddfd81e --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_templatetags/test_tethys_gizmos.py @@ -0,0 +1,330 @@ +import mock +import unittest +import tethys_gizmos.templatetags.tethys_gizmos as gizmos_templatetags +from tethys_gizmos.gizmo_options.base import TethysGizmoOptions +from datetime import datetime, date +from django.template import base +from django.template import TemplateSyntaxError +from django.template import Context +try: + reload +except NameError: # Python 3 + from imp import reload + + +class TestGizmo(TethysGizmoOptions): + + gizmo_name = 'test_gizmo' + + def __init__(self, name, *args, **kwargs): + super(TestGizmo, self).__init__(*args, **kwargs) + self.name = name + + @staticmethod + def get_vendor_js(): + return ('tethys_gizmos/vendor/openlayers/ol.js',) + + @staticmethod + def get_gizmo_js(): + return ('tethys_gizmos/js/plotly-load_from_python.js',) + + @staticmethod + def get_vendor_css(): + return ('tethys_gizmos/vendor/openlayers/ol.css',) + + @staticmethod + def get_gizmo_css(): + return ('tethys_gizmos/css/tethys_map_view.min.css',) + + +class TestTethysGizmos(unittest.TestCase): + def setUp(self): + self.gizmo_name = 'tethysext.test_extension' + pass + + def tearDown(self): + pass + + @mock.patch('tethys_apps.harvester.SingletonHarvester') + def test_TestTethysGizmos(self, mock_harvest): + mock_harvest().extension_modules = {'Test Extension': 'tethysext.test_extension'} + gizmos_templatetags.EXTENSION_PATH_MAP = {} + reload(gizmos_templatetags) + self.assertIn('custom_select_input', gizmos_templatetags.EXTENSION_PATH_MAP) + + @mock.patch('tethys_apps.harvester.SingletonHarvester') + def test_TestTethysGizmos_import_error(self, mock_harvest): + mock_harvest().extension_modules = {'Test Extension': 'tethysext.test_extension1'} + reload(gizmos_templatetags) + # self.assertRaises(ImportError, reload, gizmos_templatetags) + + def test_HighchartsDateEncoder(self): + result = gizmos_templatetags.HighchartsDateEncoder().default(datetime(2018, 1, 1)) + + # Timestamp should be 1514764800 + self.assertEqual(1514764800000.0, result) + + def test_HighchartsDateEncoder_no_dt(self): + result = gizmos_templatetags.HighchartsDateEncoder().default(date(2018, 1, 1)) + + # Check Result + self.assertEqual('2018-01-01', result) + + def test_isstring(self): + result = gizmos_templatetags.isstring(type('string')) + + # Check Result + self.assertTrue(result) + + result = gizmos_templatetags.isstring(type(['list'])) + + # Check Result + self.assertFalse(result) + + def test_return_item(self): + result = gizmos_templatetags.return_item(['0', '1'], 1) + + # Check Result + self.assertEqual('1', result) + + def test_return_item_none(self): + result = gizmos_templatetags.return_item(['0', '1'], 2) + + # Check Result + self.assertFalse(result) + + def test_json_date_handler(self): + result = gizmos_templatetags.json_date_handler(datetime(2018, 1, 1)) + + # Timestamp should be 1514764800 + self.assertEqual(1514764800000.0, result) + + def test_json_date_handler_no_datetime(self): + result = gizmos_templatetags.json_date_handler('2018') + + # Check Result + self.assertEqual('2018', result) + + def test_jsonify(self): + data = ['foo', {'bar': ('baz', None, 1.0, 2)}] + + result = gizmos_templatetags.jsonify(data) + + # Check Result + self.assertEqual('["foo", {"bar": ["baz", null, 1.0, 2]}]', result) + + def test_divide(self): + value = 10 + divisor = 2 + expected_result = 5 + + result = gizmos_templatetags.divide(value, divisor) + + # Check Result + self.assertEqual(expected_result, result) + + +class TestTethysGizmoIncludeDependency(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_load_gizmo_name(self): + gizmo_name = '"plotly_view"' + # _load_gizmo_name is loaded in init + result = gizmos_templatetags.TethysGizmoIncludeDependency(gizmo_name=gizmo_name) + + # Check result + self.assertEqual('plotly_view', result.gizmo_name) + + def test_load_gizmos_rendered(self): + gizmo_name = 'plotly_view' + context = {} + + # _load_gizmos_rendered is loaded in render + gizmos_templatetags.TethysGizmoIncludeDependency(gizmo_name=gizmo_name).render(context=context) + + self.assertEqual(['plotly_view'], context['gizmos_rendered']) + + @mock.patch('tethys_gizmos.templatetags.tethys_gizmos.settings') + def test_load_gizmos_rendered_syntax_error(self, mock_settings): + mock_settings.return_value = mock.MagicMock(TEMPLATE_DEBUG=True) + gizmo_name = 'plotly_view1' + context = {} + + t = gizmos_templatetags.TethysGizmoIncludeDependency(gizmo_name=gizmo_name) + + self.assertRaises(TemplateSyntaxError, t.render, context=context) + + +class TestTethysGizmoIncludeNode(unittest.TestCase): + def setUp(self): + self.gizmo_name = 'tethysext.test_extension' + pass + + def tearDown(self): + pass + + def test_render(self): + gizmos_templatetags.GIZMO_NAME_MAP[TestGizmo.gizmo_name] = TestGizmo + result = gizmos_templatetags.TethysGizmoIncludeNode(options='foo', gizmo_name=TestGizmo.gizmo_name) + + context = {'foo': TestGizmo(name='test_render')} + result_render = result.render(context) + + # Check Result + self.assertEqual('test_render', result_render) + + def test_render_no_gizmo_name(self): + result = gizmos_templatetags.TethysGizmoIncludeNode(options='foo', gizmo_name=None) + + context = {'foo': TestGizmo(name='test_render_no_name')} + result_render = result.render(context) + + # Check Result + self.assertEqual('test_render_no_name', result_render) + + @mock.patch('tethys_gizmos.templatetags.tethys_gizmos.get_template') + def test_render_in_extension_path(self, mock_gt): + # Reset EXTENSION_PATH_MAP + gizmos_templatetags.EXTENSION_PATH_MAP = {TestGizmo.gizmo_name: 'tethys_gizmos'} + mock_gt.return_value = mock.MagicMock() + result = gizmos_templatetags.TethysGizmoIncludeNode(options='foo', gizmo_name=TestGizmo.gizmo_name) + context = Context({'foo': TestGizmo(name='test_render')}) + result.render(context) + + # Check Result + mock_gt.assert_called_with('tethys_gizmos/templates/gizmos/test_gizmo.html') + + # We need to delete this extension path map to avoid template not exist error on the + # previous test + del gizmos_templatetags.EXTENSION_PATH_MAP[TestGizmo.gizmo_name] + + @mock.patch('tethys_gizmos.templatetags.tethys_gizmos.settings') + @mock.patch('tethys_gizmos.templatetags.tethys_gizmos.template') + def test_render_syntax_error_debug(self, mock_template, mock_setting): + mock_resolve = mock_template.Variable().resolve() + mock_resolve.return_value = mock.MagicMock() + del mock_resolve.gizmo_name + mock_setting.TEMPLATES = [{'OPTIONS': {'debug': True}}] + + context = Context({'foo': TestGizmo(name='test_render')}) + tgin = gizmos_templatetags.TethysGizmoIncludeNode(options='foo', gizmo_name='not_gizmo') + + self.assertRaises(TemplateSyntaxError, tgin.render, context=context) + + @mock.patch('tethys_gizmos.templatetags.tethys_gizmos.settings') + @mock.patch('tethys_gizmos.templatetags.tethys_gizmos.template') + def test_render_syntax_error_no_debug(self, mock_template, mock_setting): + mock_resolve = mock_template.Variable().resolve() + mock_resolve.return_value = mock.MagicMock() + del mock_resolve.gizmo_name + mock_setting.TEMPLATES = [{'OPTIONS': {'debug': False}}] + + context = Context({'foo': TestGizmo(name='test_render')}) + + result = gizmos_templatetags.TethysGizmoIncludeNode(options='foo', gizmo_name=TestGizmo.gizmo_name) + self.assertEqual('', result.render(context=context)) + + +class TestTags(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_gizmos.templatetags.tethys_gizmos.TethysGizmoIncludeNode') + def test_gizmo(self, mock_tgin): + token1 = base.Token(token_type='TOKEN_TEXT', contents='token test_options') + gizmos_templatetags.gizmo(parser='', token=token1) + + # Check Result + mock_tgin.assert_called_with('test_options', None) + + token2 = base.Token(token_type='TOKEN_TEXT', contents='token test_gizmo_name test_options') + gizmos_templatetags.gizmo(parser='', token=token2) + + # Check Result + mock_tgin.assert_called_with('test_options', 'test_gizmo_name') + + def test_gizmo_syntax_error(self): + token = base.Token(token_type='TOKEN_TEXT', contents='token') + + # Check Error + self.assertRaises(TemplateSyntaxError, gizmos_templatetags.gizmo, parser='', token=token) + + @mock.patch('tethys_gizmos.templatetags.tethys_gizmos.TethysGizmoIncludeDependency') + def test_import_gizmo_dependency(self, mock_tgid): + token = base.Token(token_type='TOKEN_TEXT', contents='test_tag_name test_gizmo_name') + + gizmos_templatetags.import_gizmo_dependency(parser='', token=token) + # Check Result + mock_tgid.assert_called_with('test_gizmo_name') + + def test_import_gizmo_dependency_syntax_error(self): + token = base.Token(token_type='TOKEN_TEXT', contents='token') + + self.assertRaises(TemplateSyntaxError, gizmos_templatetags.import_gizmo_dependency, + parser='', token=token) + + @mock.patch('tethys_gizmos.templatetags.tethys_gizmos.TethysGizmoDependenciesNode') + def test_gizmo_dependencies(self, mock_tgdn): + token = base.Token(token_type='TOKEN_TEXT', contents='token "css"') + gizmos_templatetags.gizmo_dependencies(parser='', token=token) + + # Check Result + mock_tgdn.assert_called_with('css') + + def test_gizmo_dependencies_syntax_error(self): + token = base.Token(token_type='TOKEN_TEXT', contents='token css js') + self.assertRaises(TemplateSyntaxError, gizmos_templatetags.gizmo_dependencies, + parser='', token=token) + + def test_gizmo_dependencies_not_valid(self): + token = base.Token(token_type='TOKEN_TEXT', contents='token css1') + self.assertRaises(TemplateSyntaxError, gizmos_templatetags.gizmo_dependencies, + parser='', token=token) + + +class TestTethysGizmoDependenciesNode(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_render(self): + gizmos_templatetags.GIZMO_NAME_MAP[TestGizmo.gizmo_name] = TestGizmo + output_global_css = 'global_css' + output_css = 'css' + output_global_js = 'global_js' + output_js = 'js' + result = gizmos_templatetags.TethysGizmoDependenciesNode(output_type=output_global_css) + + # Check result + self.assertEqual(output_global_css, result.output_type) + + # TEST render + context = Context({'foo': TestGizmo(name='test_render')}) + context.update({'gizmos_rendered': []}) + + # unless it has the same gizmo name as the predefined one + render_globalcss = gizmos_templatetags.TethysGizmoDependenciesNode(output_type=output_global_css).\ + render(context=context) + render_css = gizmos_templatetags.TethysGizmoDependenciesNode(output_type=output_css).\ + render(context=context) + render_globaljs = gizmos_templatetags.TethysGizmoDependenciesNode(output_type=output_global_js).\ + render(context=context) + render_js = gizmos_templatetags.TethysGizmoDependenciesNode(output_type=output_js).\ + render(context=context) + + self.assertIn('openlayers/ol.css', render_globalcss) + self.assertNotIn('tethys_gizmos.css', render_globalcss) + self.assertIn('tethys_gizmos.css', render_css) + self.assertNotIn('openlayers/ol.css', render_css) + self.assertIn('openlayers/ol.js', render_globaljs) + self.assertIn('plotly-load_from_python.js', render_js) + self.assertNotIn('openlayers/ol.js', render_js) diff --git a/tests/unit_tests/test_tethys_gizmos/test_views/__init__.py b/tests/unit_tests/test_tethys_gizmos/test_views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_gizmos/test_views/test_gizmo_showcase.py b/tests/unit_tests/test_tethys_gizmos/test_views/test_gizmo_showcase.py new file mode 100644 index 000000000..a397db3f0 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_views/test_gizmo_showcase.py @@ -0,0 +1,120 @@ +import unittest +import tethys_gizmos.views.gizmo_showcase as gizmo_showcase +from requests.exceptions import ConnectionError +import mock +from django.test import RequestFactory +from tests.factories.django_user import UserFactory + + +class TestGizmoShowcase(unittest.TestCase): + def setUp(self): + self.user = UserFactory() + self.request_factory = RequestFactory() + + def tearDown(self): + pass + + @mock.patch('tethys_gizmos.views.gizmo_showcase.list_spatial_dataset_engines') + def test_get_geoserver_wms(self, mock_list_sdes): + endpoint = 'http://localhost:8080/geoserver/rest' + expected_endpoint = 'http://localhost:8080/geoserver/wms' + mock_sde = mock.MagicMock(type='GEOSERVER', + endpoint=endpoint) + mock_list_sdes.return_value = [mock_sde] + result = gizmo_showcase.get_geoserver_wms() + + # Check Result + self.assertEqual(expected_endpoint, result) + + @mock.patch('tethys_gizmos.views.gizmo_showcase.list_spatial_dataset_engines') + def test_get_geoserver_wms_connection_error(self, mock_list_sdes): + # Connection Error Case + endpoint = 'http://localhost:8080/geoserver/rest' + expected_endpoint = 'http://ciwmap.chpc.utah.edu:8080/geoserver/wms' + mock_sde = mock.MagicMock(type='GEOSERVER', + endpoint=endpoint) + mock_sde.validate.side_effect = ConnectionError + mock_list_sdes.return_value = [mock_sde] + result = gizmo_showcase.get_geoserver_wms() + + # Check Result + self.assertEqual(expected_endpoint, result) + + def test_index(self): + request = self.request_factory.post('/jobs', {'editable_map_submit': '1', 'geometry': '[100, 40]'}) + request.user = self.user + result = gizmo_showcase.index(request) + + self.assertEqual(200, result.status_code) + + def test_get_kml(self): + request = self.request_factory + result = gizmo_showcase.get_kml(request) + + self.assertIn('kml_link', result._container[0].decode()) + self.assertEqual(200, result.status_code) + + def test_swap_kml(self): + request = self.request_factory + result = gizmo_showcase.swap_kml(request) + + self.assertIn('.kml', result._container[0].decode()) + self.assertEqual(200, result.status_code) + + def test_swap_overlays(self): + request = self.request_factory + result = gizmo_showcase.swap_overlays(request) + + self.assertIn('"type": "GeometryCollection"', result._container[0].decode()) + self.assertEqual(200, result.status_code) + + @mock.patch('tethys_gizmos.views.gizmo_showcase.messages') + def test_google_map_view(self, mock_messages): + mock_mi = mock_messages.info + request = self.request_factory.post('/jobs', {'editable_map_submit': '1', 'geometry': '[100, 40]'}) + request.user = self.user + # Need this to fix the You cannot add messages without installing + # django.contrib.messages.middleware.MessageMiddleware + result = gizmo_showcase.google_map_view(request) + + # Check result + mock_mi.assert_called_with(request, '[100, 40]') + self.assertEqual(200, result.status_code) + + @mock.patch('tethys_gizmos.views.gizmo_showcase.messages') + def test_map_view(self, mock_messages): + mock_mi = mock_messages.info + request = self.request_factory.post('/jobs', {'editable_map_submit': '1', 'geometry': '[100, 40]'}) + request.user = self.user + # Need this to fix the You cannot add messages without installing + # django.contrib.messages.middleware.MessageMiddleware + result = gizmo_showcase.map_view(request) + + # Check result + mock_mi.assert_called_with(request, '[100, 40]') + self.assertEqual(200, result.status_code) + + def test_esri_map(self): + request = self.request_factory.post('/jobs', {'editable_map_submit': '1', 'geometry': '[100, 40]'}) + request.user = self.user + result = gizmo_showcase.esri_map(request) + + self.assertEqual(200, result.status_code) + + def test_jobs_table_result(self): + request = self.request_factory.post('/jobs', {'editable_map_submit': '1', 'geometry': '[100, 40]'}) + request.user = self.user + result = gizmo_showcase.jobs_table_results(request=request, job_id='1') + + self.assertEqual(302, result.status_code) + + @mock.patch('tethys_gizmos.views.gizmo_showcase.BasicJob') + def test_create_sample_jobs(self, mock_bj): + mock_bj().return_value = mock.MagicMock() + request = self.request_factory + request.user = 'test_user' + gizmo_showcase.create_sample_jobs(request) + + # Check BasicJob Call + mock_bj.assert_called_with(_status='VCP', description='Completed multi-process job with some errors', + label='gizmos_showcase', name='job_8', user='test_user') diff --git a/tests/unit_tests/test_tethys_gizmos/test_views/test_gizmos/__init__.py b/tests/unit_tests/test_tethys_gizmos/test_views/test_gizmos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_gizmos/test_views/test_gizmos/test_jobs_table.py b/tests/unit_tests/test_tethys_gizmos/test_views/test_gizmos/test_jobs_table.py new file mode 100644 index 000000000..8b16d4d09 --- /dev/null +++ b/tests/unit_tests/test_tethys_gizmos/test_views/test_gizmos/test_jobs_table.py @@ -0,0 +1,144 @@ +import unittest +import tethys_gizmos.views.gizmos.jobs_table as gizmo_jobs_table +import mock +from django.test import RequestFactory + + +class TestJobsTable(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.TethysJob.objects.get_subclass') + def test_execute(self, mock_tj): + tj = mock_tj() + tj.execute.return_value = mock.MagicMock() + + result = gizmo_jobs_table.execute(request='', job_id='1') + + self.assertEqual(200, result.status_code) + + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.log') + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.TethysJob') + def test_execute_exception(self, mock_tj, mock_log): + tj = mock_tj.objects.get_subclass() + tj.execute.side_effect = Exception('error') + + gizmo_jobs_table.execute(request='', job_id='1') + + mock_log.error.assert_called_with('The following error occurred when executing job %s: %s', '1', 'error') + + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.TethysJob') + def test_delete(self, mock_tj): + tj = mock_tj.objects.get_subclass() + tj.delete.return_value = mock.MagicMock() + + result = gizmo_jobs_table.delete(request='', job_id='1') + + self.assertEqual(200, result.status_code) + + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.log') + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.TethysJob') + def test_delete_exception(self, mock_tj, mock_log): + tj = mock_tj.objects.get_subclass() + tj.delete.side_effect = Exception('error') + + gizmo_jobs_table.delete(request='', job_id='1') + + mock_log.error.assert_called_with('The following error occurred when deleting job %s: %s', '1', 'error') + + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.render_to_string') + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.TethysJob') + def test_update_row(self, mock_tj, mock_rts): + mock_rts.return_value = '{"job_statuses":[]}' + mock_tj.objects.get_subclass.return_value = mock.MagicMock(status='Various', label='gizmos_showcase') + rows = [('1', '30')] + column_names = ['ID', 'Time(s)'] + request = RequestFactory().post('/jobs', {'column_fields': column_names, 'row': rows}) + result = gizmo_jobs_table.update_row(request, job_id='1') + + # Check Result + rts_call_args = mock_rts.call_args_list + self.assertIn('job_statuses', rts_call_args[0][0][1]) + self.assertEqual({'Completed': 40, 'Error': 10, 'Running': 30, 'Aborted': 5}, + rts_call_args[0][0][1]['job_statuses']) + self.assertEqual(200, result.status_code) + + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.render_to_string') + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.TethysJob') + def test_update_row_not_gizmos(self, mock_tj, mock_rts): + # Another Case where job.label is not gizmos_showcase + mock_rts.return_value = '{"job_statuses":[]}' + mock_tj.objects.get_subclass.return_value = mock.MagicMock(status='Various', label='test_label', + statuses={'Completed': 1}) + rows = [('1', '30')] + column_names = ['ID', 'Time(s)'] + request = RequestFactory().post('/jobs', {'column_fields': column_names, 'row': rows}) + result = gizmo_jobs_table.update_row(request, job_id='1') + + # Check Result + rts_call_args = mock_rts.call_args_list + self.assertIn('job_statuses', rts_call_args[0][0][1]) + self.assertEqual({'Completed': 1}, rts_call_args[0][0][1]['job_statuses']) + self.assertEqual(200, result.status_code) + + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.log') + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.TethysJob') + def test_update_row_exception(self, mock_tj, mock_log): + mock_tj.objects.get_subclass.side_effect = Exception('error') + rows = [('1', '30'), + ('2', '18'), + ('3', '26')] + column_names = ['ID', 'Time(s)'] + request = RequestFactory().post('/jobs', {'column_fields': column_names, 'row': rows}) + gizmo_jobs_table.update_row(request, job_id='1') + + # Check Result + mock_log.error.assert_called_with('The following error occurred when updating row for job %s: %s', '1', + str('error')) + + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.TethysJob') + def test_update_status(self, mock_tj): + mock_tj.objects.get_subclass.return_value = mock.MagicMock(status='Various', label='gizmos_showcase') + rows = [('1', '30'), + ('2', '18'), + ('3', '26')] + column_names = ['ID', 'Time(s)'] + request = RequestFactory().post('/jobs', {'column_fields': column_names, 'row': rows}) + result = gizmo_jobs_table.update_status(request, job_id='1') + + # Check Result + self.assertEqual(200, result.status_code) + + # Another Case + mock_tj.objects.get_subclass.return_value = mock.MagicMock(status='Various', label='test_label') + result = gizmo_jobs_table.update_status(request, job_id='1') + + # Check Result + self.assertEqual(200, result.status_code) + + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.log') + @mock.patch('tethys_gizmos.views.gizmos.jobs_table.TethysJob') + def test_update_status_exception(self, mock_tj, mock_log): + mock_tj.objects.get_subclass.side_effect = Exception('error') + rows = [('1', '30'), + ('2', '18'), + ('3', '26')] + column_names = ['ID', 'Time(s)'] + request = RequestFactory().post('/jobs', {'column_fields': column_names, 'row': rows}) + gizmo_jobs_table.update_status(request, job_id='1') + + mock_log.error.assert_called_with('The following error occurred when updating status for job %s: %s', '1', + str('error')) + + def test_parse_value(self): + result = gizmo_jobs_table._parse_value('True') + self.assertTrue(result) + + result = gizmo_jobs_table._parse_value('False') + self.assertFalse(result) + + result = gizmo_jobs_table._parse_value('Test') + self.assertEqual('Test', result) diff --git a/tests/unit_tests/test_tethys_portal/__init__.py b/tests/unit_tests/test_tethys_portal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_portal/test_forms.py b/tests/unit_tests/test_tethys_portal/test_forms.py new file mode 100644 index 000000000..7228a677c --- /dev/null +++ b/tests/unit_tests/test_tethys_portal/test_forms.py @@ -0,0 +1,244 @@ +from django.test import TestCase +from captcha.models import CaptchaStore +from tethys_portal.forms import LoginForm, RegisterForm, UserSettingsForm, UserPasswordChangeForm +from django.contrib.auth.models import User +from django import forms +import mock + + +class TethysPortalFormsTests(TestCase): + + def setUp(self): + CaptchaStore.generate_key() + self.hashkey = CaptchaStore.objects.all()[0].hashkey + self.response = CaptchaStore.objects.all()[0].response + self.user = User.objects.create_user(username='user_exist', + email='foo_exist@aquaveo.com', + password='glass_onion') + + def tearDown(self): + pass + # Login Form + + def test_LoginForm(self): + login_data = {'username': 'admin', 'password': 'test1231', 'captcha_0': self.hashkey, + 'captcha_1': self.response} + login_form = LoginForm(login_data) + self.assertTrue(login_form.is_valid()) + + def test_LoginForm_invalid_username(self): + login_data = {'username': '$!admin', 'password': 'test1231', 'captcha_0': self.hashkey, + 'captcha_1': self.response} + login_form = LoginForm(login_data) + err_msg = "This value may contain only letters, numbers and @/./+/-/_ characters." + self.assertEquals(login_form.errors['username'], [err_msg]) + self.assertFalse(login_form.is_valid()) + + def test_LoginForm_invalid_password(self): + login_data = {'username': 'admin', 'password': '', 'captcha_0': self.hashkey, + 'captcha_1': self.response} + login_form = LoginForm(login_data) + self.assertFalse(login_form.is_valid()) + + def test_LoginForm_invalid(self): + login_invalid_data = {'username': 'admin', 'password': 'test1231', 'captcha_0': self.hashkey, + 'captcha_1': ''} + login_form = LoginForm(login_invalid_data) + self.assertFalse(login_form.is_valid()) + + # Register Form + + def test_RegisterForm(self): + register_data = {'username': 'user1', 'email': 'foo@aquaveo.com', 'password1': 'abc123', + 'password2': 'abc123', 'captcha_0': self.hashkey, 'captcha_1': self.response} + register_form = RegisterForm(data=register_data) + self.assertTrue(register_form.is_valid()) + + def test_RegisterForm_invalid_user(self): + register_data = {'username': 'user1&!$', 'email': 'foo@aquaveo.com', 'password1': 'abc123', + 'password2': 'abc123', 'captcha_0': self.hashkey, 'captcha_1': self.response} + register_form = RegisterForm(data=register_data) + err_msg = "This value may contain only letters, numbers and @/./+/-/_ characters." + self.assertEquals(register_form.errors['username'], [err_msg]) + self.assertFalse(register_form.is_valid()) + + def test_RegisterForm_clean_username(self): + register_data = {'username': 'user', 'email': 'foo@aquaveo.com', 'password1': 'abc123', + 'password2': 'abc123', 'captcha_0': self.hashkey, 'captcha_1': self.response} + + register_form = RegisterForm(data=register_data) + + self.assertTrue(register_form.is_valid()) + + ret = register_form.clean_username() + + self.assertEquals('user', ret) + + def test_RegisterForm_clean_username_dup(self): + register_data = {'username': 'user_exist', 'email': 'foo@aquaveo.com', 'password1': 'abc123', + 'password2': 'abc123', 'captcha_0': self.hashkey, 'captcha_1': self.response} + + register_form = RegisterForm(data=register_data) + + # validate form, false because duplicated user + self.assertFalse(register_form.is_valid()) + + # user is duplicated so is_valid removed from cleaned_data, we add it back to test + register_form.cleaned_data['username'] = 'user_exist' + + self.assertRaises(forms.ValidationError, register_form.clean_username) + + def test_RegisterForm_clean_email(self): + register_data = {'username': 'user1', 'email': 'foo@aquaveo.com', 'password1': 'abc123', + 'password2': 'abc123', 'captcha_0': self.hashkey, 'captcha_1': self.response} + + register_form = RegisterForm(data=register_data) + + self.assertTrue(register_form.is_valid()) + + ret = register_form.clean_email() + + self.assertEquals('foo@aquaveo.com', ret) + + def test_RegisterForm_clean_email_dup(self): + register_data = {'username': 'user12', 'email': 'foo_exist@aquaveo.com', 'password1': 'abc123', + 'password2': 'abc123', 'captcha_0': self.hashkey, 'captcha_1': self.response} + + register_form = RegisterForm(data=register_data) + + register_form.is_valid() + + # is_valid is removing duplicated email + self.assertNotIn('email', register_form.cleaned_data) + + # To test raise error, we need to put it back in to test + register_form.cleaned_data['email'] = 'foo_exist@aquaveo.com' + + self.assertRaises(forms.ValidationError, register_form.clean_email) + + @mock.patch('tethys_portal.forms.validate_password') + def test_RegisterForm_clean_password2(self, mock_vp): + register_data = {'username': 'user1', 'email': 'foo@aquaveo.com', 'password1': 'abc123', + 'password2': 'abc123', 'captcha_0': self.hashkey, 'captcha_1': self.response} + + register_form = RegisterForm(data=register_data) + + # Check if form is valid and to generate cleaned_data + self.assertTrue(register_form.is_valid()) + + ret = register_form.clean_password2() + + mock_vp.assert_called_with('abc123') + + self.assertEquals('abc123', ret) + + def test_RegisterForm_clean_password2_diff(self): + register_data = {'username': 'user1', 'email': 'foo@aquaveo.com', 'password1': 'abcd123', + 'password2': 'abc123', 'captcha_0': self.hashkey, 'captcha_1': self.response} + + register_form = RegisterForm(data=register_data) + + # use is_valid to get cleaned_data attributes + self.assertFalse(register_form.is_valid()) + + # is_valid removed cleaned_data password2, need to update + register_form.cleaned_data['password2'] = 'abc123' + + self.assertRaises(forms.ValidationError, register_form.clean_password2) + + def test_RegisterForm_save(self): + register_data = {'username': 'user1', 'email': 'foo@aquaveo.com', 'password1': 'abc123', + 'password2': 'abc123', 'captcha_0': self.hashkey, 'captcha_1': self.response} + + register_form = RegisterForm(data=register_data) + + ret = register_form.save() + + # Also try to get from database after it's saved + ret_database = User.objects.get(username='user1') + + # Check result + self.assertIsInstance(ret, User) + self.assertIsInstance(ret_database, User) + self.assertEqual('user1', ret.username) + self.assertEqual('user1', ret_database.username) + + def test_UserSettingsForm(self): + user_settings_data = {'first_name': 'fname', 'last_name': 'lname', 'email': 'user@aquaveo.com'} + user_settings_form = UserSettingsForm(data=user_settings_data) + self.assertTrue(user_settings_form.is_valid()) + + # UserPasswordChange Form + + def test_UserPasswordChangeForm_valid(self): + user_password_change_data = {'old_password': 'glass_onion', 'new_password1': 'pass2', 'new_password2': 'pass2'} + user_password_change_form = UserPasswordChangeForm(self.user, data=user_password_change_data) + self.assertTrue(user_password_change_form.is_valid()) + + def test_UserPasswordChangeForm_clean_old_password(self): + user_password_change_data = {'old_password': 'glass_onion', 'new_password1': 'pass2', 'new_password2': 'pass2'} + user_password_change_form = UserPasswordChangeForm(self.user, data=user_password_change_data) + + self.assertTrue(user_password_change_form.is_valid()) + + ret = user_password_change_form.clean_old_password() + + self.assertEqual('glass_onion', ret) + + def test_UserPasswordChangeForm_clean_old_password_invalid(self): + user_password_change_data = {'old_password': 'abc123', 'new_password1': 'pass2', 'new_password2': 'pass2'} + user_password_change_form = UserPasswordChangeForm(self.user, data=user_password_change_data) + + # is_valid to get cleaned_data + self.assertFalse(user_password_change_form.is_valid()) + + # is_valid removes old_password, add it back for testing + user_password_change_form.cleaned_data['old_password'] = 'abc123' + + self.assertRaises(forms.ValidationError, user_password_change_form.clean_old_password) + + @mock.patch('tethys_portal.forms.validate_password') + def test_UserPasswordChangeForm_clean_new_password2(self, mock_vp): + user_password_change_data = {'old_password': 'glass_onion', 'new_password1': 'pass2', 'new_password2': 'pass2'} + user_password_change_form = UserPasswordChangeForm(self.user, data=user_password_change_data) + + self.assertTrue(user_password_change_form.is_valid()) + + ret = user_password_change_form.clean_new_password2() + self.assertEqual('pass2', ret) + + mock_vp.assert_called_with('pass2') + + def test_UserPasswordChangeForm_clean_new_password2_diff(self): + user_password_change_data = {'old_password': 'glass_onion', 'new_password1': 'pass1', 'new_password2': 'pass2'} + user_password_change_form = UserPasswordChangeForm(self.user, data=user_password_change_data) + + # run is_valid to get cleaned_data + self.assertFalse(user_password_change_form.is_valid()) + + # is_valid removes new_password2 because it's different from pass1, we update here to run the test + user_password_change_form.cleaned_data['new_password2'] = 'pass2' + + self.assertRaises(forms.ValidationError, user_password_change_form.clean_new_password2) + + def test_UserPasswordChangeForm_save(self): + # password hash before save + ret_old = User.objects.get(username='user_exist') + old_pass = ret_old.password + + # Update new password + user_password_change_data = {'old_password': 'glass_onion', 'new_password1': 'pass2', 'new_password2': 'pass2'} + user_password_change_form = UserPasswordChangeForm(self.user, data=user_password_change_data) + + # run is_valid to get cleaned_data attributes. + self.assertTrue(user_password_change_form.is_valid()) + + user_password_change_form.save() + + # Also try to get from database after it's saved + ret_new = User.objects.get(username='user_exist') + new_pass = ret_new.password + + # Check result + self.assertIsInstance(ret_new, User) + self.assertNotEqual(old_pass, new_pass) diff --git a/tests/unit_tests/test_tethys_portal/test_middleware.py b/tests/unit_tests/test_tethys_portal/test_middleware.py new file mode 100644 index 000000000..b90922bb5 --- /dev/null +++ b/tests/unit_tests/test_tethys_portal/test_middleware.py @@ -0,0 +1,267 @@ +import unittest +import mock + +from tethys_portal.middleware import TethysSocialAuthExceptionMiddleware + + +class TethysPortalMiddlewareTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_portal.middleware.isinstance') + @mock.patch('tethys_portal.middleware.hasattr') + @mock.patch('tethys_portal.middleware.redirect') + def test_process_exception_anonymous_user(self, mock_redirect, mock_hasattr, mock_isinstance): + mock_request = mock.MagicMock() + mock_exception = mock.MagicMock() + mock_hasattr.return_value = True + mock_isinstance.return_value = True + mock_request.user.is_anonymous = True + + obj = TethysSocialAuthExceptionMiddleware() + obj.process_exception(mock_request, mock_exception) + + mock_redirect.assert_called_once_with('accounts:login') + + @mock.patch('tethys_portal.middleware.isinstance') + @mock.patch('tethys_portal.middleware.hasattr') + @mock.patch('tethys_portal.middleware.redirect') + def test_process_exception_user(self, mock_redirect, mock_hasattr, mock_isinstance): + mock_request = mock.MagicMock() + mock_exception = mock.MagicMock() + mock_hasattr.return_value = True + mock_isinstance.return_value = True + mock_request.user.is_anonymous = False + mock_request.user.username = 'foo' + + obj = TethysSocialAuthExceptionMiddleware() + obj.process_exception(mock_request, mock_exception) + + mock_redirect.assert_called_once_with('user:settings', username='foo') + + @mock.patch('tethys_portal.middleware.pretty_output') + @mock.patch('tethys_portal.middleware.messages.success') + @mock.patch('tethys_portal.middleware.isinstance') + @mock.patch('tethys_portal.middleware.hasattr') + @mock.patch('tethys_portal.middleware.redirect') + def test_process_exception_isinstance_google(self, mock_redirect, mock_hasattr, mock_isinstance, mock_success, + mock_pretty_output): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = True + + mock_exception = mock.MagicMock() + mock_exception.backend.name = 'google' + + mock_hasattr.return_value = True + mock_isinstance.side_effect = False, True + + obj = TethysSocialAuthExceptionMiddleware() + + obj.process_exception(mock_request, mock_exception) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('google', po_call_args[0][0][0]) + + call_args = mock_success.call_args_list + + self.assertEquals(mock_request, call_args[0][0][0]) + + self.assertEquals('The Google account you tried to connect to has already been associated with another ' + 'account.', call_args[0][0][1]) + + mock_redirect.assert_called_once_with('accounts:login') + + @mock.patch('tethys_portal.middleware.pretty_output') + @mock.patch('tethys_portal.middleware.messages.success') + @mock.patch('tethys_portal.middleware.isinstance') + @mock.patch('tethys_portal.middleware.hasattr') + @mock.patch('tethys_portal.middleware.redirect') + def test_process_exception_isinstance_linkedin(self, mock_redirect, mock_hasattr, mock_isinstance, + mock_success, mock_pretty_output): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = True + + mock_exception = mock.MagicMock() + mock_exception.backend.name = 'linkedin' + + mock_hasattr.return_value = True + mock_isinstance.side_effect = False, True + + obj = TethysSocialAuthExceptionMiddleware() + + obj.process_exception(mock_request, mock_exception) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('linkedin', po_call_args[0][0][0]) + + call_args = mock_success.call_args_list + + self.assertEquals(mock_request, call_args[0][0][0]) + + self.assertEquals('The LinkedIn account you tried to connect to has already been associated with another ' + 'account.', call_args[0][0][1]) + + mock_redirect.assert_called_once_with('accounts:login') + + @mock.patch('tethys_portal.middleware.pretty_output') + @mock.patch('tethys_portal.middleware.messages.success') + @mock.patch('tethys_portal.middleware.isinstance') + @mock.patch('tethys_portal.middleware.hasattr') + @mock.patch('tethys_portal.middleware.redirect') + def test_process_exception_isinstance_hydroshare(self, mock_redirect, mock_hasattr, mock_isinstance, + mock_success, + mock_pretty_output): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = True + + mock_exception = mock.MagicMock() + mock_exception.backend.name = 'hydroshare' + + mock_hasattr.return_value = True + mock_isinstance.side_effect = False, True + + obj = TethysSocialAuthExceptionMiddleware() + + obj.process_exception(mock_request, mock_exception) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('hydroshare', po_call_args[0][0][0]) + + call_args = mock_success.call_args_list + + self.assertEquals(mock_request, call_args[0][0][0]) + + self.assertEquals('The HydroShare account you tried to connect to has already been associated with ' + 'another account.', call_args[0][0][1]) + + mock_redirect.assert_called_once_with('accounts:login') + + @mock.patch('tethys_portal.middleware.pretty_output') + @mock.patch('tethys_portal.middleware.messages.success') + @mock.patch('tethys_portal.middleware.isinstance') + @mock.patch('tethys_portal.middleware.hasattr') + @mock.patch('tethys_portal.middleware.redirect') + def test_process_exception_isinstance_facebook(self, mock_redirect, mock_hasattr, mock_isinstance, mock_success, + mock_pretty_output): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = True + + mock_exception = mock.MagicMock() + mock_exception.backend.name = 'facebook' + + mock_hasattr.return_value = True + mock_isinstance.side_effect = False, True + + obj = TethysSocialAuthExceptionMiddleware() + + obj.process_exception(mock_request, mock_exception) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('facebook', po_call_args[0][0][0]) + + call_args = mock_success.call_args_list + + self.assertEquals(mock_request, call_args[0][0][0]) + + self.assertEquals('The Facebook account you tried to connect to has already been associated with ' + 'another account.', call_args[0][0][1]) + + mock_redirect.assert_called_once_with('accounts:login') + + @mock.patch('tethys_portal.middleware.pretty_output') + @mock.patch('tethys_portal.middleware.messages.success') + @mock.patch('tethys_portal.middleware.isinstance') + @mock.patch('tethys_portal.middleware.hasattr') + @mock.patch('tethys_portal.middleware.redirect') + def test_process_exception_isinstance_social(self, mock_redirect, mock_hasattr, mock_isinstance, + mock_success, + mock_pretty_output): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = False + mock_request.user.username = 'foo' + + mock_exception = mock.MagicMock() + mock_exception.backend.name = 'social' + + mock_hasattr.return_value = True + mock_isinstance.side_effect = False, True + + obj = TethysSocialAuthExceptionMiddleware() + + obj.process_exception(mock_request, mock_exception) + + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEqual(1, len(po_call_args)) + self.assertIn('social', po_call_args[0][0][0]) + + call_args = mock_success.call_args_list + + self.assertEquals(mock_request, call_args[0][0][0]) + + self.assertEquals('The social account you tried to connect to has already been associated with ' + 'another account.', call_args[0][0][1]) + + mock_redirect.assert_called_once_with('user:settings', username='foo') + + @mock.patch('tethys_portal.middleware.messages.success') + @mock.patch('tethys_portal.middleware.isinstance') + @mock.patch('tethys_portal.middleware.hasattr') + @mock.patch('tethys_portal.middleware.redirect') + def test_process_exception_isinstance_exception_with_anonymous_user(self, mock_redirect, mock_hasattr, + mock_isinstance, mock_success): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = False + mock_request.user.username = 'foo' + + mock_exception = mock.MagicMock() + mock_exception.backend.name = 'social' + + mock_hasattr.return_value = True + mock_isinstance.side_effect = False, False, True + + obj = TethysSocialAuthExceptionMiddleware() + + obj.process_exception(mock_request, mock_exception) + + call_args = mock_success.call_args_list + + self.assertEquals(mock_request, call_args[0][0][0]) + + self.assertEquals('Unable to disconnect from this social account.', call_args[0][0][1]) + + mock_redirect.assert_called_once_with('user:settings', username='foo') + + @mock.patch('tethys_portal.middleware.messages.success') + @mock.patch('tethys_portal.middleware.isinstance') + @mock.patch('tethys_portal.middleware.hasattr') + @mock.patch('tethys_portal.middleware.redirect') + def test_process_exception_isinstance_exception_user(self, mock_redirect, mock_hasattr, mock_isinstance, + mock_success): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = True + + mock_exception = mock.MagicMock() + mock_exception.backend.name = 'social' + + mock_hasattr.return_value = True + mock_isinstance.side_effect = False, False, True + + obj = TethysSocialAuthExceptionMiddleware() + + obj.process_exception(mock_request, mock_exception) + + call_args = mock_success.call_args_list + + self.assertEquals(mock_request, call_args[0][0][0]) + + self.assertEquals('Unable to disconnect from this social account.', call_args[0][0][1]) + + mock_redirect.assert_called_once_with('accounts:login') diff --git a/tests/unit_tests/test_tethys_portal/test_urls.py b/tests/unit_tests/test_tethys_portal/test_urls.py new file mode 100644 index 000000000..feacf223d --- /dev/null +++ b/tests/unit_tests/test_tethys_portal/test_urls.py @@ -0,0 +1,125 @@ +from django.urls import reverse, resolve +from tethys_sdk.testing import TethysTestCase + + +class TestUrls(TethysTestCase): + def set_up(self): + pass + + def tear_down(self): + pass + + def test_account_urls_account_login(self): + url = reverse('accounts:login') + resolver = resolve(url) + self.assertEqual('/accounts/login/', url) + self.assertEqual('login_view', resolver.func.__name__) + self.assertEqual('tethys_portal.views.accounts', resolver.func.__module__) + + def test_account_urls_accounts_logout(self): + url = reverse('accounts:logout') + resolver = resolve(url) + self.assertEqual('/accounts/logout/', url) + self.assertEqual('logout_view', resolver.func.__name__) + self.assertEqual('tethys_portal.views.accounts', resolver.func.__module__) + + def test_account_urls_accounts_register(self): + url = reverse('accounts:register') + resolver = resolve(url) + self.assertEqual('/accounts/register/', url) + self.assertEqual('register', resolver.func.__name__) + self.assertEqual('tethys_portal.views.accounts', resolver.func.__module__) + + def test_account_urls_accounts_password_reset(self): + url = reverse('accounts:password_reset') + resolver = resolve(url) + self.assertEqual('/accounts/password/reset/', url) + self.assertEqual('password_reset', resolver.func.__name__) + self.assertEqual('django.contrib.auth.views', resolver.func.__module__) + + def test_account_urls_accounts_password_confirm(self): + url = reverse('accounts:password_confirm', kwargs={'uidb64': 'f00Bar', 'token': 'tok'}) + resolver = resolve(url) + self.assertEqual('/accounts/password/reset/f00Bar-tok/', url) + self.assertEqual('password_reset_confirm', resolver.func.__name__) + self.assertEqual('django.contrib.auth.views', resolver.func.__module__) + + def test_user_urls_profile(self): + url = reverse('user:profile', kwargs={'username': 'foo'}) + resolver = resolve(url) + + self.assertEqual('/user/foo/', url) + self.assertEqual('profile', resolver.func.__name__) + self.assertEqual('tethys_portal.views.user', resolver.func.__module__) + + def test_user_urls_settings(self): + url = reverse('user:settings', kwargs={'username': 'foo'}) + resolver = resolve(url) + self.assertEqual('/user/foo/settings/', url) + self.assertEqual('settings', resolver.func.__name__) + self.assertEqual('tethys_portal.views.user', resolver.func.__module__) + + def test_user_urls_change_password(self): + url = reverse('user:change_password', kwargs={'username': 'foo'}) + resolver = resolve(url) + self.assertEqual('/user/foo/change-password/', url) + self.assertEqual('change_password', resolver.func.__name__) + self.assertEqual('tethys_portal.views.user', resolver.func.__module__) + + def test_user_urls_disconnect(self): + url = reverse('user:change_password', kwargs={'username': 'foo'}) + resolver = resolve(url) + self.assertEqual('/user/foo/change-password/', url) + self.assertEqual('change_password', resolver.func.__name__) + self.assertEqual('tethys_portal.views.user', resolver.func.__module__) + + def test_user_urls_delete(self): + url = reverse('user:delete', kwargs={'username': 'foo'}) + resolver = resolve(url) + self.assertEqual('/user/foo/delete-account/', url) + self.assertEqual('delete_account', resolver.func.__name__) + self.assertEqual('tethys_portal.views.user', resolver.func.__module__) + + def test_developer_urls_developer_home(self): + url = reverse('developer_home') + resolver = resolve(url) + self.assertEqual('/developer/', url) + self.assertEqual('home', resolver.func.__name__) + self.assertEqual('tethys_portal.views.developer', resolver.func.__module__) + + def test_developer_urls_gizmos(self): + url = reverse('gizmos:showcase') + resolver = resolve(url) + self.assertEqual('/developer/gizmos/', url) + self.assertEqual('index', resolver.func.__name__) + self.assertEqual('tethys_gizmos.views.gizmo_showcase', resolver.func.__module__) + self.assertEqual('gizmos', resolver.namespaces[0]) + + def test_developer_urls_services(self): + url = reverse('services:wps_home') + resolver = resolve(url) + self.assertEqual('/developer/services/wps/', url) + self.assertEqual('wps_home', resolver.func.__name__) + self.assertEqual('tethys_services.views', resolver.func.__module__) + self.assertEqual('services', resolver.namespaces[0]) + + def test_urlpatterns_handoff_capabilities(self): + url = reverse('handoff_capabilities', kwargs={'app_name': 'foo'}) + resolver = resolve(url) + self.assertEqual('/handoff/foo/', url) + self.assertEqual('handoff_capabilities', resolver.func.__name__) + self.assertEqual('tethys_apps.views', resolver.func.__module__) + + def test_urlpatterns_handoff(self): + url = reverse('handoff', kwargs={'app_name': 'foo', 'handler_name': 'Bar'}) + resolver = resolve(url) + self.assertEqual('/handoff/foo/Bar/', url) + self.assertEqual('handoff', resolver.func.__name__) + self.assertEqual('tethys_apps.views', resolver.func.__module__) + + def test_urlpatterns_update_job_status(self): + url = reverse('update_job_status', kwargs={'job_id': 'JI001'}) + resolver = resolve(url) + self.assertEqual('/update-job-status/JI001/', url) + self.assertEqual('update_job_status', resolver.func.__name__) + self.assertEqual('tethys_apps.views', resolver.func.__module__) diff --git a/tests/unit_tests/test_tethys_portal/test_views/__init__.py b/tests/unit_tests/test_tethys_portal/test_views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_portal/test_views/test_accounts.py b/tests/unit_tests/test_tethys_portal/test_views/test_accounts.py new file mode 100644 index 000000000..fb1c5fb51 --- /dev/null +++ b/tests/unit_tests/test_tethys_portal/test_views/test_accounts.py @@ -0,0 +1,565 @@ +import unittest +import mock +from tethys_portal.views.accounts import login_view, register, logout_view, reset_confirm, reset + + +class TethysPortalViewsAccountsTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_portal.views.accounts.redirect') + def test_login_view_not_anonymous_user(self, mock_redirect): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = False + mock_request.user.username = 'sam' + login_view(mock_request) + mock_redirect.assert_called_once_with('user:profile', username='sam') + + @mock.patch('tethys_portal.views.accounts.login') + @mock.patch('tethys_portal.views.accounts.authenticate') + @mock.patch('tethys_portal.views.accounts.LoginForm') + @mock.patch('tethys_portal.views.accounts.redirect') + def test_login_view_post_request(self, mock_redirect, mock_login_form, mock_authenticate, mock_login): + mock_request = mock.MagicMock() + mock_request.method = 'POST' + mock_request.POST = 'login-submit' + mock_request.user.is_anonymous = True + mock_request.user.username = 'sam' + mock_request.GET = '' + + mock_form = mock.MagicMock() + mock_login_form.return_value = mock_form + + # mock validate the form + mock_form.is_valid.return_value = True + + mock_username = mock.MagicMock() + mock_password = mock.MagicMock() + mock_form.cleaned_data('username').return_value = mock_username + mock_form.cleaned_data('password').return_value = mock_password + + # mock authenticate + mock_user = mock.MagicMock() + mock_authenticate.return_value = mock_user + + # mock the password has been verified for the user + mock_user.is_active = True + + # call the login function with mock args + login_view(mock_request) + + mock_login_form.assert_called_with(mock_request.POST) + + # mock authenticate call + mock_authenticate.asset_called_with(username=mock_username, password=mock_password) + + # mock the user is valid, active, and authenticated, so login in the user + mock_login.assert_called_with(mock_request, mock_user) + + # mock redirect after logged in using next parameter or default to user profile + mock_redirect.assert_called_once_with('app_library') + + @mock.patch('tethys_portal.views.accounts.login') + @mock.patch('tethys_portal.views.accounts.authenticate') + @mock.patch('tethys_portal.views.accounts.LoginForm') + @mock.patch('tethys_portal.views.accounts.redirect') + def test_login_view_get_method_next(self, mock_redirect, mock_login_form, mock_authenticate, mock_login): + mock_request = mock.MagicMock() + mock_request.method = 'POST' + mock_request.POST = 'login-submit' + mock_request.user.is_anonymous = True + mock_request.user.username = 'sam' + mock_request.GET = {'next': 'foo'} + + mock_form = mock.MagicMock() + mock_login_form.return_value = mock_form + + # mock validate the form + mock_form.is_valid.return_value = True + + mock_username = mock.MagicMock() + mock_password = mock.MagicMock() + mock_form.cleaned_data('username').return_value = mock_username + mock_form.cleaned_data('password').return_value = mock_password + + # mock authenticate + mock_user = mock.MagicMock() + mock_authenticate.return_value = mock_user + + # mock the password has been verified for the user + mock_user.is_active = True + + # call the login function with mock args + login_view(mock_request) + + mock_login_form.assert_called_with(mock_request.POST) + + # mock authenticate call + mock_authenticate.asset_called_with(username=mock_username, password=mock_password) + + # mock the user is valid, active, and authenticated, so login in the user + mock_login.assert_called_with(mock_request, mock_user) + + # mock redirect after logged in using next parameter or default to user profile + mock_redirect.assert_called_once_with(mock_request.GET['next']) + + @mock.patch('tethys_portal.views.accounts.render') + @mock.patch('tethys_portal.views.accounts.messages') + @mock.patch('tethys_portal.views.accounts.login') + @mock.patch('tethys_portal.views.accounts.authenticate') + @mock.patch('tethys_portal.views.accounts.LoginForm') + @mock.patch('tethys_portal.views.accounts.redirect') + def test_login_view_get_method_not_active_user(self, mock_redirect, mock_login_form, mock_authenticate, mock_login, + mock_messages, mock_render): + mock_request = mock.MagicMock() + mock_request.method = 'POST' + mock_request.POST = 'login-submit' + mock_request.user.is_anonymous = True + mock_request.user.username = 'sam' + mock_request.GET = {'next': 'foo'} + + mock_form = mock.MagicMock() + mock_login_form.return_value = mock_form + + # mock validate the form + mock_form.is_valid.return_value = True + + mock_username = mock.MagicMock() + mock_password = mock.MagicMock() + mock_form.cleaned_data('username').return_value = mock_username + mock_form.cleaned_data('password').return_value = mock_password + + # mock authenticate + mock_user = mock.MagicMock() + mock_authenticate.return_value = mock_user + + # mock the password has been verified for the user + mock_user.is_active = False + + # call the login function with mock args + login_view(mock_request) + + mock_login_form.assert_called_with(mock_request.POST) + + # mock authenticate call + mock_authenticate.asset_called_with(username=mock_username, password=mock_password) + + # mock the user is valid, active, and authenticated, so login in the user + mock_login.assert_not_called() + + # mock redirect after logged in using next parameter or default to user profile + mock_redirect.assert_not_called() + + mock_messages.error.assert_called_once_with(mock_request, "Sorry, but your account has been disabled. " + "Please contact the site " + "administrator for more details.") + + context = {'form': mock_login_form(), + 'signup_enabled': False} + + mock_render.assert_called_once_with(mock_request, 'tethys_portal/accounts/login.html', context) + + @mock.patch('tethys_portal.views.accounts.render') + @mock.patch('tethys_portal.views.accounts.messages') + @mock.patch('tethys_portal.views.accounts.login') + @mock.patch('tethys_portal.views.accounts.authenticate') + @mock.patch('tethys_portal.views.accounts.LoginForm') + @mock.patch('tethys_portal.views.accounts.redirect') + def test_login_view_get_method_user_none(self, mock_redirect, mock_login_form, mock_authenticate, mock_login, + mock_messages, mock_render): + mock_request = mock.MagicMock() + mock_request.method = 'POST' + mock_request.POST = 'login-submit' + mock_request.user.is_anonymous = True + mock_request.user.username = 'sam' + mock_request.GET = {'next': 'foo'} + + mock_form = mock.MagicMock() + mock_login_form.return_value = mock_form + + # mock validate the form + mock_form.is_valid.return_value = True + + mock_username = mock.MagicMock() + mock_password = mock.MagicMock() + mock_form.cleaned_data('username').return_value = mock_username + mock_form.cleaned_data('password').return_value = mock_password + + # mock authenticate + mock_user = mock.MagicMock() + mock_authenticate.return_value = None + + # mock the password has been verified for the user + mock_user.is_active = False + + # call the login function with mock args + login_view(mock_request) + + mock_login_form.assert_called_with(mock_request.POST) + + # mock authenticate call + mock_authenticate.asset_called_with(username=mock_username, password=mock_password) + + # mock the user is valid, active, and authenticated, so login in the user + mock_login.assert_not_called() + + # mock redirect after logged in using next parameter or default to user profile + mock_redirect.assert_not_called() + + mock_messages.warning.assert_called_once_with(mock_request, "Whoops! We were not able to log you in. " + "Please check your username and " + "password and try again.") + + context = {'form': mock_login_form(), + 'signup_enabled': False} + + mock_render.assert_called_once_with(mock_request, 'tethys_portal/accounts/login.html', context) + + @mock.patch('tethys_portal.views.accounts.render') + @mock.patch('tethys_portal.views.accounts.login') + @mock.patch('tethys_portal.views.accounts.LoginForm') + @mock.patch('tethys_portal.views.accounts.redirect') + def test_login_view_wrong_method(self, mock_redirect, mock_login_form, mock_login, mock_render): + mock_request = mock.MagicMock() + mock_request.method = 'foo' + + mock_form = mock.MagicMock() + mock_login_form.return_value = mock_form + + # call the login function with mock args + login_view(mock_request) + + mock_login_form.assert_called_with() + + # mock the user is valid, active, and authenticated, so login in the user + mock_login.assert_not_called() + + # mock redirect after logged in using next parameter or default to user profile + mock_redirect.assert_not_called() + + context = {'form': mock_login_form(), + 'signup_enabled': False} + + mock_render.assert_called_once_with(mock_request, 'tethys_portal/accounts/login.html', context) + + @mock.patch('tethys_portal.views.accounts.redirect') + def test_register_not_anonymous_user(self, mock_redirect): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = False + mock_request.user.username = 'sam' + register(mock_request) + mock_redirect.assert_called_once_with('user:profile', username='sam') + + @mock.patch('tethys_portal.views.accounts.settings') + @mock.patch('tethys_portal.views.accounts.redirect') + def test_register_not_enable_open_signup(self, mock_redirect, mock_settings): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = True + mock_request.user.username = 'sam' + mock_settings.ENABLE_OPEN_SIGNUP = False + register(mock_request) + mock_redirect.assert_called_once_with('accounts:login') + + @mock.patch('tethys_portal.views.accounts.settings') + @mock.patch('tethys_portal.views.accounts.login') + @mock.patch('tethys_portal.views.accounts.authenticate') + @mock.patch('tethys_portal.views.accounts.RegisterForm') + @mock.patch('tethys_portal.views.accounts.redirect') + def test_register_post_request(self, mock_redirect, mock_register_form, mock_authenticate, mock_login, + mock_settings): + mock_request = mock.MagicMock() + mock_request.method = 'POST' + mock_request.POST = 'register-submit' + mock_request.user.is_anonymous = True + mock_request.user.username = 'sam' + mock_request.GET = '' + + mock_settings.ENABLE_OPEN_SIGNUP = True + + mock_form = mock.MagicMock() + mock_register_form.return_value = mock_form + + # mock validate the form + mock_form.is_valid.return_value = True + + mock_username = mock.MagicMock() + mock_email = mock.MagicMock() + mock_password = mock.MagicMock() + mock_form.clean_username.return_value = mock_username + mock_form.clean_email.return_value = mock_email + mock_form.clean_password2.return_value = mock_password + + # mock authenticate + mock_user = mock.MagicMock() + mock_authenticate.return_value = mock_user + + # mock the password has been verified for the user + mock_user.is_active = True + + # call the login function with mock args + register(mock_request) + + mock_form.save.assert_called_once() + + mock_register_form.assert_called_with(mock_request.POST) + + # mock authenticate call + mock_authenticate.asset_called_with(username=mock_username, password=mock_password) + + # mock the user is valid, active, and authenticated, so login in the user + mock_login.assert_called_with(mock_request, mock_user) + + # mock redirect after logged in using next parameter or default to user profile + mock_redirect.assert_called_once_with('user:profile', username=mock_user.username) + + @mock.patch('tethys_portal.views.accounts.settings') + @mock.patch('tethys_portal.views.accounts.login') + @mock.patch('tethys_portal.views.accounts.authenticate') + @mock.patch('tethys_portal.views.accounts.RegisterForm') + @mock.patch('tethys_portal.views.accounts.redirect') + def test_register_post_request_next(self, mock_redirect, mock_register_form, mock_authenticate, mock_login, + mock_settings): + mock_request = mock.MagicMock() + mock_request.method = 'POST' + mock_request.POST = 'register-submit' + mock_request.user.is_anonymous = True + mock_request.user.username = 'sam' + mock_request.GET = {'next': 'foo'} + + mock_settings.ENABLE_OPEN_SIGNUP = True + + mock_form = mock.MagicMock() + mock_register_form.return_value = mock_form + + # mock validate the form + mock_form.is_valid.return_value = True + + mock_username = mock.MagicMock() + mock_email = mock.MagicMock() + mock_password = mock.MagicMock() + mock_form.clean_username.return_value = mock_username + mock_form.clean_email.return_value = mock_email + mock_form.clean_password2.return_value = mock_password + + # mock authenticate + mock_user = mock.MagicMock() + mock_authenticate.return_value = mock_user + + # mock the password has been verified for the user + mock_user.is_active = True + + # call the login function with mock args + register(mock_request) + + mock_form.save.assert_called_once() + + mock_register_form.assert_called_with(mock_request.POST) + + # mock authenticate call + mock_authenticate.asset_called_with(username=mock_username, password=mock_password) + + # mock the user is valid, active, and authenticated, so login in the user + mock_login.assert_called_with(mock_request, mock_user) + + # mock redirect after logged in using next parameter or default to user profile + mock_redirect.assert_called_once_with(mock_request.GET['next']) + + @mock.patch('tethys_portal.views.accounts.messages') + @mock.patch('tethys_portal.views.accounts.settings') + @mock.patch('tethys_portal.views.accounts.login') + @mock.patch('tethys_portal.views.accounts.authenticate') + @mock.patch('tethys_portal.views.accounts.RegisterForm') + @mock.patch('tethys_portal.views.accounts.render') + def test_register_post_request_not_active_user(self, mock_render, mock_register_form, mock_authenticate, + mock_login, mock_settings, mock_messages): + mock_request = mock.MagicMock() + mock_request.method = 'POST' + mock_request.POST = 'register-submit' + mock_request.user.is_anonymous = True + mock_request.user.username = 'sam' + mock_request.GET = '' + + mock_settings.ENABLE_OPEN_SIGNUP = True + + mock_form = mock.MagicMock() + mock_register_form.return_value = mock_form + + # mock validate the form + mock_form.is_valid.return_value = True + + mock_username = mock.MagicMock() + mock_email = mock.MagicMock() + mock_password = mock.MagicMock() + mock_form.clean_username.return_value = mock_username + mock_form.clean_email.return_value = mock_email + mock_form.clean_password2.return_value = mock_password + + # mock authenticate + mock_user = mock.MagicMock() + mock_authenticate.return_value = mock_user + + # mock the password has been verified for the user + mock_user.is_active = False + + # call the login function with mock args + register(mock_request) + + mock_form.save.assert_called_once() + + mock_register_form.assert_called_with(mock_request.POST) + + # mock authenticate call + mock_authenticate.asset_called_with(username=mock_username, password=mock_password) + + # mock the user is valid, active, and authenticated, so login in the user + mock_login.assert_not_called() + + mock_messages.error.assert_called_once_with(mock_request, "Sorry, but your account has been disabled. " + "Please contact the site " + "administrator for more details.") + + context = {'form': mock_form} + + # mock redirect after logged in using next parameter or default to user profile + mock_render.assert_called_once_with(mock_request, 'tethys_portal/accounts/register.html', context) + + @mock.patch('tethys_portal.views.accounts.messages') + @mock.patch('tethys_portal.views.accounts.settings') + @mock.patch('tethys_portal.views.accounts.login') + @mock.patch('tethys_portal.views.accounts.authenticate') + @mock.patch('tethys_portal.views.accounts.RegisterForm') + @mock.patch('tethys_portal.views.accounts.render') + def test_register_post_request_user_none(self, mock_render, mock_register_form, mock_authenticate, + mock_login, mock_settings, mock_messages): + mock_request = mock.MagicMock() + mock_request.method = 'POST' + mock_request.POST = 'register-submit' + mock_request.user.is_anonymous = True + mock_request.user.username = 'sam' + mock_request.GET = '' + + mock_settings.ENABLE_OPEN_SIGNUP = True + + mock_form = mock.MagicMock() + mock_register_form.return_value = mock_form + + # mock validate the form + mock_form.is_valid.return_value = True + + mock_username = mock.MagicMock() + mock_email = mock.MagicMock() + mock_password = mock.MagicMock() + mock_form.clean_username.return_value = mock_username + mock_form.clean_email.return_value = mock_email + mock_form.clean_password2.return_value = mock_password + + # mock authenticate + mock_user = mock.MagicMock() + mock_authenticate.return_value = None + + # mock the password has been verified for the user + mock_user.is_active = False + + # call the login function with mock args + register(mock_request) + + mock_form.save.assert_called_once() + + mock_register_form.assert_called_with(mock_request.POST) + + # mock authenticate call + mock_authenticate.asset_called_with(username=mock_username, password=mock_password) + + # mock the user is valid, active, and authenticated, so login in the user + mock_login.assert_not_called() + + mock_messages.warning.assert_called_once_with(mock_request, "Whoops! We were not able to log you in. " + "Please check your username and " + "password and try again.") + + context = {'form': mock_form} + + # mock redirect after logged in using next parameter or default to user profile + mock_render.assert_called_once_with(mock_request, 'tethys_portal/accounts/register.html', context) + + @mock.patch('tethys_portal.views.accounts.settings') + @mock.patch('tethys_portal.views.accounts.RegisterForm') + @mock.patch('tethys_portal.views.accounts.render') + def test_register_bad_request(self, mock_render, mock_register_form, mock_settings): + mock_request = mock.MagicMock() + mock_request.method = 'FOO' + + mock_settings.ENABLE_OPEN_SIGNUP = True + + mock_form = mock.MagicMock() + + mock_register_form.return_value = mock_form + + # mock validate the form + mock_form.is_valid.return_value = True + + # call the login function with mock args + register(mock_request) + + mock_form.save.assert_not_called() + + mock_register_form.assert_called_with() + + context = {'form': mock_form} + + # mock redirect after logged in using next parameter or default to user profile + mock_render.assert_called_once_with(mock_request, 'tethys_portal/accounts/register.html', context) + + @mock.patch('tethys_portal.views.accounts.messages') + @mock.patch('tethys_portal.views.accounts.logout') + @mock.patch('tethys_portal.views.accounts.redirect') + def test_logout_view(self, mock_redirect, mock_logout, mock_messages): + mock_request = mock.MagicMock() + mock_request.user.is_anonymous = False + mock_request.user.first_name = 'foo' + mock_request.user.username = 'bar' + + mock_redirect.return_value = 'home' + + ret = logout_view(mock_request) + + self.assertEquals('home', ret) + + mock_logout.assert_called_once_with(mock_request) + + mock_messages.success.assert_called_once_with(mock_request, 'Goodbye, {0}. Come back soon!'. + format(mock_request.user.first_name)) + + mock_redirect.assert_called_once_with('home') + + @mock.patch('tethys_portal.views.accounts.reverse') + @mock.patch('tethys_portal.views.accounts.password_reset_confirm') + def test_reset_confirm(self, mock_prc, mock_reverse): + mock_request = mock.MagicMock() + mock_reverse.return_value = 'accounts:login' + mock_prc.return_value = True + ret = reset_confirm(mock_request) + self.assertTrue(ret) + mock_prc.assert_called_once_with(mock_request, + template_name='tethys_portal/accounts/password_reset/reset_confirm.html', + uidb64=None, + token=None, + post_reset_redirect='accounts:login') + + @mock.patch('tethys_portal.views.accounts.reverse') + @mock.patch('tethys_portal.views.accounts.password_reset') + def test_reset(self, mock_pr, mock_reverse): + mock_request = mock.MagicMock() + mock_reverse.return_value = 'accounts:login' + mock_pr.return_value = True + ret = reset(mock_request) + self.assertTrue(ret) + mock_pr.assert_called_once_with(mock_request, + template_name='tethys_portal/accounts/password_reset/reset_request.html', + email_template_name='tethys_portal/accounts/password_reset/reset_email.html', + subject_template_name='tethys_portal/accounts/password_reset/reset_subject.txt', + post_reset_redirect='accounts:login') diff --git a/tests/unit_tests/test_tethys_portal/test_views/test_developer.py b/tests/unit_tests/test_tethys_portal/test_views/test_developer.py new file mode 100644 index 000000000..d2db59e2a --- /dev/null +++ b/tests/unit_tests/test_tethys_portal/test_views/test_developer.py @@ -0,0 +1,26 @@ +import unittest +import mock + +from tethys_portal.views.developer import is_staff, home + + +class TethysPortalDeveloperTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_is_staff(self): + mock_user = mock.MagicMock() + mock_user.is_staff = 'foo' + self.assertEquals('foo', is_staff(mock_user)) + + @mock.patch('tethys_portal.views.developer.render') + def test_home(self, mock_render): + mock_request = mock.MagicMock() + context = {} + mock_render.return_value = 'foo' + self.assertEquals('foo', home(mock_request)) + mock_render.assert_called_once_with(mock_request, 'tethys_portal/developer/home.html', context) diff --git a/tests/unit_tests/test_tethys_portal/test_views/test_error.py b/tests/unit_tests/test_tethys_portal/test_views/test_error.py new file mode 100644 index 000000000..a39faf69c --- /dev/null +++ b/tests/unit_tests/test_tethys_portal/test_views/test_error.py @@ -0,0 +1,62 @@ +import unittest +import mock +from tethys_portal.views.error import handler_400, handler_403, handler_404, handler_500 + + +class TethysPortalViewsErrorTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_portal.views.error.render') + def test_handler_400(self, mock_render): + mock_request = mock.MagicMock() + mock_render.return_value = '400' + context = {'error_code': '400', + 'error_title': 'Bad Request', + 'error_message': "Sorry, but we can't process your request. Try something different.", + 'error_image': '/static/tethys_portal/images/error_500.png'} + + self.assertEquals('400', handler_400(mock_request)) + mock_render.assert_called_once_with(mock_request, 'tethys_portal/error.html', context, status=400) + + @mock.patch('tethys_portal.views.error.render') + def test_handler_403(self, mock_render): + mock_request = mock.MagicMock() + mock_render.return_value = '403' + context = {'error_code': '403', + 'error_title': 'Forbidden', + 'error_message': "We apologize, but this operation is not permitted.", + 'error_image': '/static/tethys_portal/images/error_403.png'} + + self.assertEquals('403', handler_403(mock_request)) + mock_render.assert_called_once_with(mock_request, 'tethys_portal/error.html', context, status=403) + + @mock.patch('tethys_portal.views.error.render') + def test_handler_404(self, mock_render): + mock_request = mock.MagicMock() + mock_render.return_value = '404' + context = {'error_code': '404', + 'error_title': 'Page Not Found', + 'error_message': "We are unable to find the page you requested. Please, check the address and " + "try again.", + 'error_image': '/static/tethys_portal/images/error_404.png'} + + self.assertEquals('404', handler_404(mock_request)) + mock_render.assert_called_once_with(mock_request, 'tethys_portal/error.html', context, status=404) + + @mock.patch('tethys_portal.views.error.render') + def test_handler_500(self, mock_render): + mock_request = mock.MagicMock() + mock_render.return_value = '500' + context = {'error_code': '500', + 'error_title': 'Internal Server Error', + 'error_message': "We're sorry, but we seem to have a problem. " + "Please, come back later and try again.", + 'error_image': '/static/tethys_portal/images/error_500.png'} + + self.assertEquals('500', handler_500(mock_request)) + mock_render.assert_called_once_with(mock_request, 'tethys_portal/error.html', context, status=500) diff --git a/tests/unit_tests/test_tethys_portal/test_views/test_home.py b/tests/unit_tests/test_tethys_portal/test_views/test_home.py new file mode 100644 index 000000000..375c10c15 --- /dev/null +++ b/tests/unit_tests/test_tethys_portal/test_views/test_home.py @@ -0,0 +1,42 @@ +import unittest +import mock + +from tethys_portal.views.home import home + + +class TethysPortalHomeTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_portal.views.home.hasattr') + @mock.patch('tethys_portal.views.home.render') + @mock.patch('tethys_portal.views.home.redirect') + @mock.patch('tethys_portal.views.home.settings') + def test_home(self, mock_settings, mock_redirect, mock_render, mock_hasattr): + mock_request = mock.MagicMock() + mock_hasattr.return_value = True + mock_settings.BYPASS_TETHYS_HOME_PAGE = True + mock_redirect.return_value = 'foo' + mock_render.return_value = 'bar' + self.assertEquals('foo', home(mock_request)) + mock_render.assert_not_called() + mock_redirect.assert_called_once_with('app_library') + + @mock.patch('tethys_portal.views.home.hasattr') + @mock.patch('tethys_portal.views.home.render') + @mock.patch('tethys_portal.views.home.redirect') + @mock.patch('tethys_portal.views.home.settings') + def test_home_with_no_attribute(self, mock_settings, mock_redirect, mock_render, mock_hasattr): + mock_request = mock.MagicMock() + mock_hasattr.return_value = False + mock_settings.ENABLE_OPEN_SIGNUP = True + mock_redirect.return_value = 'foo' + mock_render.return_value = 'bar' + self.assertEquals('bar', home(mock_request)) + mock_redirect.assert_not_called() + mock_render.assert_called_once_with(mock_request, 'tethys_portal/home.html', + {"ENABLE_OPEN_SIGNUP": mock_settings.ENABLE_OPEN_SIGNUP, }) diff --git a/tests/unit_tests/test_tethys_portal/test_views/test_receivers.py b/tests/unit_tests/test_tethys_portal/test_views/test_receivers.py new file mode 100644 index 000000000..c9b4f36b6 --- /dev/null +++ b/tests/unit_tests/test_tethys_portal/test_views/test_receivers.py @@ -0,0 +1,30 @@ +import unittest +import mock +from tethys_portal.views.receivers import create_auth_token + + +class TethysPortalReceiversTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_portal.views.receivers.Token') + def test_create_auth_token(self, mock_token): + expected_sender = 'foo' + expected_created = True + mock_instance = mock.MagicMock() + + create_auth_token(expected_sender, instance=mock_instance, created=expected_created) + mock_token.objects.create.assert_called_with(user=mock_instance) + + @mock.patch('tethys_portal.views.receivers.Token') + def test_create_auth_token_not_created(self, mock_token): + expected_sender = 'foo' + expected_created = False + mock_instance = mock.MagicMock() + + create_auth_token(expected_sender, instance=mock_instance, created=expected_created) + mock_token.objects.create.assert_not_called() diff --git a/tests/unit_tests/test_tethys_portal/test_views/test_user.py b/tests/unit_tests/test_tethys_portal/test_views/test_user.py new file mode 100644 index 000000000..9722e6e01 --- /dev/null +++ b/tests/unit_tests/test_tethys_portal/test_views/test_user.py @@ -0,0 +1,286 @@ +import unittest +import mock +from tethys_portal.views.user import profile, settings, change_password, social_disconnect, delete_account + + +class TethysPortalUserTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_portal.views.user.render') + @mock.patch('tethys_portal.views.user.Token.objects.get_or_create') + @mock.patch('tethys_portal.views.user.User.objects.get') + def test_profile(self, mock_get_user, mock_token_get_create, mock_render): + mock_request = mock.MagicMock() + username = 'foo' + + mock_context_user = mock.MagicMock() + mock_get_user.return_value = mock_context_user + + mock_user_token = mock.MagicMock() + mock_token_created = mock.MagicMock() + mock_token_get_create.return_value = mock_user_token, mock_token_created + + expected_context = { + 'context_user': mock_context_user, + 'user_token': mock_user_token.key + } + + profile(mock_request, username) + + mock_render.assert_called_once_with(mock_request, 'tethys_portal/user/profile.html', expected_context) + + mock_get_user.assert_called_once_with(username='foo') + + mock_token_get_create.assert_called_once_with(user=mock_context_user) + + @mock.patch('tethys_portal.views.user.messages.warning') + @mock.patch('tethys_portal.views.user.redirect') + def test_settings(self, mock_redirect, mock_message_warn): + mock_request = mock.MagicMock() + username = 'foo' + mock_user = mock.MagicMock() + mock_user.username = 'sam' + mock_request.user = mock_user + + settings(mock_request, username) + + mock_message_warn.assert_called_once_with(mock_request, "You are not allowed to change other users' settings.") + mock_redirect.assert_called_once_with('user:profile', username='sam') + + @mock.patch('tethys_portal.views.user.UserSettingsForm') + @mock.patch('tethys_portal.views.user.redirect') + def test_settings_request_post(self, mock_redirect, mock_usf): + username = 'foo' + + mock_first_name = mock.MagicMock() + mock_last_name = mock.MagicMock() + mock_email = mock.MagicMock() + + mock_user = mock.MagicMock() + mock_user.username = 'foo' + mock_user.first_name = mock_first_name + mock_user.last_name = mock_last_name + mock_user.email = mock_email + + mock_request = mock.MagicMock() + mock_request.user = mock_user + mock_request.method = 'POST' + mock_request.POST = 'user-settings-submit' + + mock_form = mock.MagicMock() + mock_form.is_valid.return_value = True + mock_usf.return_value = mock_form + + settings(mock_request, username) + + mock_user.save.assert_called() + + mock_usf.assert_called_once_with(mock_request.POST) + + mock_redirect.assert_called_once_with('user:profile', username='foo') + + @mock.patch('tethys_portal.views.user.Token.objects.get_or_create') + @mock.patch('tethys_portal.views.user.UserSettingsForm') + @mock.patch('tethys_portal.views.user.render') + def test_settings_request_get(self, mock_render, mock_usf, mock_token_get_create): + username = 'foo' + + mock_request_user = mock.MagicMock() + mock_request_user.username = 'foo' + + mock_request = mock.MagicMock() + mock_request.user = mock_request_user + mock_request.method = 'GET' + + mock_form = mock.MagicMock() + mock_usf.return_value = mock_form + + mock_user_token = mock.MagicMock() + mock_token_created = mock.MagicMock() + mock_token_get_create.return_value = mock_user_token, mock_token_created + + expected_context = {'form': mock_form, + 'context_user': mock_request.user, + 'user_token': mock_user_token.key} + + settings(mock_request, username) + + mock_usf.assert_called_once_with(instance=mock_request_user) + + mock_token_get_create.assert_called_once_with(user=mock_request_user) + + mock_render.assert_called_once_with(mock_request, 'tethys_portal/user/settings.html', expected_context) + + @mock.patch('tethys_portal.views.user.messages.warning') + @mock.patch('tethys_portal.views.user.redirect') + def test_change_password(self, mock_redirect, mock_message_warn): + mock_request = mock.MagicMock() + username = 'foo' + mock_user = mock.MagicMock() + mock_user.username = 'sam' + mock_request.user = mock_user + + change_password(mock_request, username) + + mock_message_warn.assert_called_once_with(mock_request, "You are not allowed to change other users' settings.") + mock_redirect.assert_called_once_with('user:profile', username='sam') + + @mock.patch('tethys_portal.views.user.UserPasswordChangeForm') + @mock.patch('tethys_portal.views.user.redirect') + def test_change_password_post(self, mock_redirect, mock_upf): + username = 'foo' + + mock_user = mock.MagicMock() + mock_user.username = 'foo' + + mock_request = mock.MagicMock() + mock_request.user = mock_user + + mock_request.method = 'POST' + mock_request.POST = 'change-password-submit' + + mock_form = mock.MagicMock() + mock_form.is_valid.return_value = True + mock_upf.return_value = mock_form + + change_password(mock_request, username) + + mock_redirect.assert_called_once_with('user:settings', username='foo') + + mock_form.clean_old_password.assert_called() + + mock_form.clean_new_password2.assert_called() + + mock_form.save.assert_called() + + mock_upf.assert_called_once_with(user=mock_request.user, data=mock_request.POST) + + @mock.patch('tethys_portal.views.user.UserPasswordChangeForm') + @mock.patch('tethys_portal.views.user.render') + def test_change_password_get(self, mock_render, mock_upf): + username = 'foo' + + mock_request_user = mock.MagicMock() + mock_request_user.username = 'foo' + + mock_request = mock.MagicMock() + mock_request.user = mock_request_user + mock_request.method = 'GET' + + mock_form = mock.MagicMock() + mock_upf.return_value = mock_form + + expected_context = {'form': mock_form} + + change_password(mock_request, username) + + mock_upf.assert_called_once_with(user=mock_request_user) + + mock_render.assert_called_once_with(mock_request, 'tethys_portal/user/change_password.html', expected_context) + + @mock.patch('tethys_portal.views.user.messages.warning') + @mock.patch('tethys_portal.views.user.redirect') + def test_social_disconnect_invalid_user(self, mock_redirect, mock_message_warn): + username = 'foo' + + mock_request_user = mock.MagicMock() + mock_request_user.username = 'sam' + + mock_request = mock.MagicMock() + mock_request.user = mock_request_user + + mock_provider = mock.MagicMock() + + mock_association_id = mock.MagicMock() + + social_disconnect(mock_request, username, mock_provider, mock_association_id) + + mock_message_warn.assert_called_once_with(mock_request, "You are not allowed to change other users' settings.") + + mock_redirect.assert_called_once_with('user:profile', username='sam') + + @mock.patch('tethys_portal.views.user.render') + def test_social_disconnect_valid_user(self, mock_render): + username = 'foo' + + mock_request_user = mock.MagicMock() + mock_request_user.username = 'foo' + + mock_request = mock.MagicMock() + mock_request.user = mock_request_user + + mock_provider = mock.MagicMock() + + mock_association_id = mock.MagicMock() + + expected_context = {'provider': mock_provider, + 'association_id': mock_association_id} + + social_disconnect(mock_request, username, mock_provider, mock_association_id) + + mock_render.assert_called_once_with(mock_request, 'tethys_portal/user/disconnect.html', expected_context) + + @mock.patch('tethys_portal.views.user.messages.warning') + @mock.patch('tethys_portal.views.user.redirect') + def test_delete_account(self, mock_redirect, mock_message_warn): + username = 'foo' + + mock_request_user = mock.MagicMock() + mock_request_user.username = 'sam' + + mock_request = mock.MagicMock() + mock_request.user = mock_request_user + + delete_account(mock_request, username) + + mock_message_warn.assert_called_once_with(mock_request, "You are not allowed to change other users' settings.") + + mock_redirect.assert_called_once_with('user:profile', username='sam') + + @mock.patch('tethys_portal.views.user.messages.success') + @mock.patch('tethys_portal.views.user.logout') + @mock.patch('tethys_portal.views.user.redirect') + def test_delete_account_post(self, mock_redirect, mock_logout, mock_messages_success): + username = 'foo' + + mock_user = mock.MagicMock() + mock_user.username = 'foo' + + mock_request = mock.MagicMock() + mock_request.user = mock_user + + mock_request.method = 'POST' + mock_request.POST = 'delete-account-submit' + + delete_account(mock_request, username) + + mock_request.user.delete.assert_called() + + mock_logout.assert_called_once_with(mock_request) + + mock_messages_success.assert_called_once_with(mock_request, 'Your account has been successfully deleted.') + + mock_redirect.assert_called_once_with('home') + + @mock.patch('tethys_portal.views.user.render') + def test_delete_account_not_post(self, mock_render): + username = 'foo' + + mock_user = mock.MagicMock() + mock_user.username = 'foo' + + mock_request = mock.MagicMock() + mock_request.user = mock_user + + mock_request.method = 'GET' + + delete_account(mock_request, username) + + expected_context = {} + + mock_render.assert_called_once_with(mock_request, 'tethys_portal/user/delete.html', expected_context) diff --git a/tests/unit_tests/test_tethys_services/test_admin.py b/tests/unit_tests/test_tethys_services/test_admin.py new file mode 100644 index 000000000..a5ad0e626 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_admin.py @@ -0,0 +1,105 @@ +import unittest +import mock + +from django.utils.translation import ugettext_lazy as _ +from tethys_services.models import DatasetService, SpatialDatasetService, WebProcessingService, PersistentStoreService +from tethys_services.admin import DatasetServiceForm, SpatialDatasetServiceForm, WebProcessingServiceForm,\ + PersistentStoreServiceForm, DatasetServiceAdmin, SpatialDatasetServiceAdmin, WebProcessingServiceAdmin,\ + PersistentStoreServiceAdmin + + +class TestTethysServicesAdmin(unittest.TestCase): + + def setUp(self): + self.expected_labels = { + 'public_endpoint': _('Public Endpoint') + } + + def tearDown(self): + pass + + def test_DatasetServiceForm(self): + mock_args = mock.MagicMock() + expected_fields = ('name', 'engine', 'endpoint', 'public_endpoint', 'apikey', 'username', 'password') + + ret = DatasetServiceForm(mock_args) + self.assertEquals(DatasetService, ret.Meta.model) + self.assertEquals(expected_fields, ret.Meta.fields) + self.assertTrue('password' in ret.Meta.widgets) + self.assertEquals(self.expected_labels, ret.Meta.labels) + + def test_SpatialDatasetServiceForm(self): + mock_args = mock.MagicMock() + expected_fields = ('name', 'engine', 'endpoint', 'public_endpoint', 'apikey', 'username', 'password') + + ret = SpatialDatasetServiceForm(mock_args) + self.assertEquals(SpatialDatasetService, ret.Meta.model) + self.assertEquals(expected_fields, ret.Meta.fields) + self.assertTrue('password' in ret.Meta.widgets) + self.assertEquals(self.expected_labels, ret.Meta.labels) + + def test_WebProcessingServiceForm(self): + mock_args = mock.MagicMock() + expected_fields = ('name', 'endpoint', 'public_endpoint', 'username', 'password') + + ret = WebProcessingServiceForm(mock_args) + self.assertEquals(WebProcessingService, ret.Meta.model) + self.assertEquals(expected_fields, ret.Meta.fields) + self.assertTrue('password' in ret.Meta.widgets) + self.assertEquals(self.expected_labels, ret.Meta.labels) + + def test_PersistentStoreServiceForm(self): + mock_args = mock.MagicMock() + expected_fields = ('name', 'engine', 'host', 'port', 'username', 'password') + + ret = PersistentStoreServiceForm(mock_args) + self.assertEquals(PersistentStoreService, ret.Meta.model) + self.assertEquals(expected_fields, ret.Meta.fields) + self.assertTrue('password' in ret.Meta.widgets) + + def test_DatasetServiceAdmin(self): + mock_args = mock.MagicMock() + expected_fields = ('name', 'engine', 'endpoint', 'public_endpoint', 'apikey', 'username', 'password') + + ret = DatasetServiceAdmin(mock_args, mock_args) + self.assertEquals(DatasetServiceForm, ret.form) + self.assertEquals(expected_fields, ret.fields) + + def test_SpatialDatasetServiceAdmin(self): + mock_args = mock.MagicMock() + expected_fields = ('name', 'engine', 'endpoint', 'public_endpoint', 'apikey', 'username', 'password') + + ret = SpatialDatasetServiceAdmin(mock_args, mock_args) + self.assertEquals(SpatialDatasetServiceForm, ret.form) + self.assertEquals(expected_fields, ret.fields) + + def test_WebProcessingServiceAdmin(self): + mock_args = mock.MagicMock() + expected_fields = ('name', 'endpoint', 'public_endpoint', 'username', 'password') + + ret = WebProcessingServiceAdmin(mock_args, mock_args) + self.assertEquals(WebProcessingServiceForm, ret.form) + self.assertEquals(expected_fields, ret.fields) + + def test_PersistentStoreServiceAdmin(self): + mock_args = mock.MagicMock() + expected_fields = ('name', 'engine', 'host', 'port', 'username', 'password') + + ret = PersistentStoreServiceAdmin(mock_args, mock_args) + self.assertEquals(PersistentStoreServiceForm, ret.form) + self.assertEquals(expected_fields, ret.fields) + + def test_admin_site_register(self): + from django.contrib import admin + registry = admin.site._registry + self.assertIn(DatasetService, registry) + self.assertIsInstance(registry[DatasetService], DatasetServiceAdmin) + + self.assertIn(SpatialDatasetService, registry) + self.assertIsInstance(registry[SpatialDatasetService], SpatialDatasetServiceAdmin) + + self.assertIn(WebProcessingService, registry) + self.assertIsInstance(registry[WebProcessingService], WebProcessingServiceAdmin) + + self.assertIn(PersistentStoreService, registry) + self.assertIsInstance(registry[PersistentStoreService], PersistentStoreServiceAdmin) diff --git a/tests/unit_tests/test_tethys_services/test_apps.py b/tests/unit_tests/test_tethys_services/test_apps.py new file mode 100644 index 000000000..dc3c979d8 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_apps.py @@ -0,0 +1,14 @@ +import unittest +from tethys_services.apps import TethysServicesConfig + + +class TestApps(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_TethysServiceConfig(self): + self.assertEqual('tethys_services', TethysServicesConfig.name) + self.assertEqual("Tethys Services", TethysServicesConfig.verbose_name) diff --git a/tests/unit_tests/test_tethys_services/test_backends/__init__.py b/tests/unit_tests/test_tethys_services/test_backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_services/test_backends/test_hs_restclient_helper.py b/tests/unit_tests/test_tethys_services/test_backends/test_hs_restclient_helper.py new file mode 100644 index 000000000..831d9f8c1 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_backends/test_hs_restclient_helper.py @@ -0,0 +1,182 @@ +import unittest +import mock +import time +from tethys_services.backends.hs_restclient_helper import HSClientInitException +import tethys_services.backends.hs_restclient_helper as hs_client_init_exception + + +class HsRestClientHelperTest(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_init(self): + exc = HSClientInitException('foo') + self.assertEquals('foo', exc.value) + self.assertEquals("'foo'", str(exc)) + + @mock.patch('tethys_services.backends.hs_restclient_helper.logger') + def test_get_get_oauth_hs_main_exception(self, mock_logger): + mock_request = mock.MagicMock() + mock_request.user.social_auth.all.side_effect = Exception('foo') + + self.assertRaises(HSClientInitException, hs_client_init_exception.get_oauth_hs, mock_request) + mock_logger.exception.assert_called_once_with('Failed to initialize hs object: foo') + + @mock.patch('tethys_services.backends.hs_restclient_helper.hs_r') + @mock.patch('tethys_services.backends.hs_restclient_helper.refresh_user_token') + @mock.patch('tethys_services.backends.hs_restclient_helper.logger') + def test_get_get_oauth_hs_one_hydroshare(self, mock_logger, mock_refresh_user_token, mock_hs_r): + mock_social_auth_obj = mock.MagicMock() + mock_backend_instance = mock.MagicMock() + mock_backend_instance.name = 'hydroshare' + mock_backend_instance.auth_server_hostname = 'foo' + mock_data1 = { + 'id': 'id', + 'access_token': 'my_access_token', + 'token_type': 'my_token_type', + 'expires_in': 'my_expires_in', + 'expires_at': 'my_expires_at', + 'refresh_token': 'my_refresh_token', + 'scope': 'my_scope' + } + + mock_request = mock.MagicMock() + mock_request.user.social_auth.all.return_value = [mock_social_auth_obj] + mock_social_auth_obj.get_backend_instance.return_value = mock_backend_instance + mock_social_auth_obj.extra_data.return_value = mock_data1 + mock_refresh_user_token.return_value = True + + hs_client_init_exception.get_oauth_hs(mock_request) + + mock_logger.debug.assert_any_call('Found oauth backend: hydroshare') + mock_refresh_user_token.assert_called_once_with(mock_social_auth_obj) + mock_hs_r.HydroShareAuthOAuth2.assert_called_once_with('', '', token=mock_social_auth_obj.extra_data) + mock_hs_r.HydroShare.assert_called_once_with(auth=mock_hs_r.HydroShareAuthOAuth2(), + hostname=mock_backend_instance.auth_server_hostname) + mock_logger.debug.assert_called_with('hs object initialized: {0} @ {1}'. + format(mock_social_auth_obj.extra_data['id'], + mock_backend_instance.auth_server_hostname)) + + @mock.patch('tethys_services.backends.hs_restclient_helper.hs_r') + @mock.patch('tethys_services.backends.hs_restclient_helper.refresh_user_token') + @mock.patch('tethys_services.backends.hs_restclient_helper.logger') + def test_get_get_oauth_two_hydroshare_exception(self, mock_logger, mock_refresh_user_token, mock_hs_r): + mock_social_auth_obj = mock.MagicMock() + mock_backend_instance = mock.MagicMock() + mock_backend_instance.name = 'hydroshare' + mock_backend_instance.auth_server_hostname = 'foo' + mock_data1 = { + 'id': 'id', + 'access_token': 'my_access_token', + 'token_type': 'my_token_type', + 'expires_in': 'my_expires_in', + 'expires_at': 'my_expires_at', + 'refresh_token': 'my_refresh_token', + 'scope': 'my_scope' + } + + mock_request = mock.MagicMock() + mock_request.user.social_auth.all.return_value = [mock_social_auth_obj, mock_social_auth_obj] + mock_social_auth_obj.get_backend_instance.return_value = mock_backend_instance + mock_social_auth_obj.extra_data.return_value = mock_data1 + mock_refresh_user_token.return_value = True + + self.assertRaises(HSClientInitException, hs_client_init_exception.get_oauth_hs, mock_request) + + mock_logger.debug.assert_any_call('Found oauth backend: hydroshare') + mock_refresh_user_token.assert_called_once_with(mock_social_auth_obj) + mock_hs_r.HydroShareAuthOAuth2.assert_called_once_with('', '', token=mock_social_auth_obj.extra_data) + mock_hs_r.HydroShare.assert_called_once_with(auth=mock_hs_r.HydroShareAuthOAuth2(), + hostname=mock_backend_instance.auth_server_hostname) + mock_logger.debug.assert_any_call('hs object initialized: {0} @ {1}'. + format(mock_social_auth_obj.extra_data['id'], + mock_backend_instance.auth_server_hostname)) + mock_logger.debug.assert_called_with('Found oauth backend: hydroshare') + mock_logger.exception.assert_called_once_with('Failed to initialize hs object: Found another hydroshare oauth ' + 'instance: {0} @ {1}'. + format(mock_social_auth_obj.extra_data['id'], + mock_backend_instance.auth_server_hostname)) + + @mock.patch('tethys_services.backends.hs_restclient_helper.logger') + def test_get_get_oauth_no_hydroshare_exception(self, mock_logger): + mock_request = mock.MagicMock() + mock_request.user.social_auth.all.return_value = [] + + self.assertRaises(HSClientInitException, hs_client_init_exception.get_oauth_hs, mock_request) + + mock_logger.exception.assert_called_once_with('Failed to initialize hs object: Not logged in through ' + 'HydroShare') + mock_request.user.social_auth.all.assert_called_once() + + @mock.patch('tethys_services.backends.hs_restclient_helper.logger') + @mock.patch('tethys_services.backends.hs_restclient_helper.load_strategy') + def test__send_refresh_request(self, mock_load_st, mock_log): + # mock token data + mock_data1 = { + 'access_token': 'my_access_token', + 'token_type': 'my_token_type', + 'expires_in': 'my_expires_in', + 'expires_at': 'my_expires_at', + 'refresh_token': 'my_refresh_token', + 'scope': 'my_scope' + } + # mock user social data as mock token data + mock_user_social = mock.MagicMock() + mock_user_social.refresh.return_value = True + mock_user_social.extra_data.return_value = mock_data1 + mock_user_social.set_extra_data.return_value = True + mock_user_social.save.return_value = True + + # mock the load_strategy() call + mock_load_st.return_value = mock.MagicMock() + + # call the method to test + hs_client_init_exception._send_refresh_request(mock_user_social) + + # check mock user_social is called with mock_load_st + mock_user_social.refresh_token.assert_called_with(mock_load_st()) + mock_user_social.set_extra_data.assert_called_once_with(extra_data=mock_user_social.extra_data) + mock_user_social.save.assert_called_once() + mock_log.debug.assert_called() + + @mock.patch('tethys_services.backends.hs_restclient_helper._send_refresh_request') + def test_refresh_user_token(self, mock_refresh_request): + # mock user social data as mock token data + mock_user_social = mock.MagicMock() + mock_user_social.extra_data.get.return_value = int(time.time()) + + # call the method to test + hs_client_init_exception.refresh_user_token(mock_user_social) + mock_user_social.extra_data.get.assert_called_once_with('expires_at') + mock_refresh_request.assert_called_once_with(mock_user_social) + + @mock.patch('tethys_services.backends.hs_restclient_helper._send_refresh_request') + def test_refresh_user_token_exception_1(self, mock_refresh_request): + # mock user social data as mock token data + mock_user_social = mock.MagicMock() + mock_user_social.extra_data.get.side_effect = Exception + + # call the method to test + hs_client_init_exception.refresh_user_token(mock_user_social) + + mock_user_social.extra_data.get.assert_called_once_with('expires_at') + mock_refresh_request.assert_called_once_with(mock_user_social) + + @mock.patch('tethys_services.backends.hs_restclient_helper.logger') + @mock.patch('tethys_services.backends.hs_restclient_helper.time.time', side_effect=Exception('foo')) + @mock.patch('tethys_services.backends.hs_restclient_helper._send_refresh_request') + def test_refresh_user_token_exception_2(self, mock_refresh_request, mock_time, mock_log): + # mock user social data as mock token data + mock_user_social = mock.MagicMock() + mock_user_social.extra_data.get.return_value = 5 + + # call the method to test + self.assertRaises(Exception, hs_client_init_exception.refresh_user_token, mock_user_social) + + mock_user_social.extra_data.get.assert_called_once_with('expires_at') + mock_refresh_request.assert_not_called() + mock_time.assert_called_once() + mock_log.error.assert_called_once_with('Failed to refresh token: foo') diff --git a/tests/unit_tests/test_tethys_services/test_backends/test_hydroshare.py b/tests/unit_tests/test_tethys_services/test_backends/test_hydroshare.py new file mode 100644 index 000000000..1f97f69af --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_backends/test_hydroshare.py @@ -0,0 +1,118 @@ +import unittest +import mock +from tethys_services.backends.hydroshare import HydroShareOAuth2 + + +class HydroShareBackendTest(unittest.TestCase): + + def setUp(self): + self.auth_server_hostname = "www.hydroshare.org" + self.http_scheme = "https" + self.auth_server_full_url = "{0}://{1}".format(self.http_scheme, self.auth_server_hostname) + self.name = 'hydroshare' + self.user_data_url = '{0}/hsapi/userInfo/'.format(self.auth_server_full_url) + + def tearDown(self): + pass + + def test_HydroShareOAuth2(self): + hydro_share_auth2_obj = HydroShareOAuth2() + + expected_auth_server_full_url = "{0}://{1}".format(self.http_scheme, self.auth_server_hostname) + self.assertEqual(expected_auth_server_full_url, hydro_share_auth2_obj.auth_server_full_url) + + expected_authorization_url = '{0}/o/authorize/'.format(self.auth_server_full_url) + self.assertEqual(expected_authorization_url, hydro_share_auth2_obj.AUTHORIZATION_URL) + + expected_access_token_url = '{0}/o/token/'.format(self.auth_server_full_url) + self.assertEqual(expected_access_token_url, hydro_share_auth2_obj.ACCESS_TOKEN_URL) + + # user data endpoint + expected_user_data_url = '{0}/hsapi/userInfo/'.format(self.auth_server_full_url) + self.assertEqual(expected_user_data_url, hydro_share_auth2_obj.USER_DATA_URL) + + def test_extra_data(self): + mock_response = dict( + email='foo@gmail.com', + username='user1', + access_token='token1', + token_type='type1', + expires_in='500', + expires_at='10000000', + refresh_token='234234', + scope='scope' + ) + + hydro_share_auth2_obj = HydroShareOAuth2() + + hydro_share_auth2_obj.set_expires_in_to = 100 + + ret = hydro_share_auth2_obj.extra_data('user1', '0001-009', mock_response) + + self.assertEquals('foo@gmail.com', ret['email']) + self.assertEquals('token1', ret['access_token']) + self.assertEquals('type1', ret['token_type']) + self.assertEquals(100, ret['expires_in']) + self.assertEquals('234234', ret['refresh_token']) + self.assertEquals('scope', ret['scope']) + + def test_get_user_details(self): + hydro_share_auth2_obj = HydroShareOAuth2() + mock_response = mock.MagicMock(username='name', email='email') + mock_response.get('username').return_value = 'name' + mock_response.get('email').return_value = 'email' + ret = hydro_share_auth2_obj.get_user_details(mock_response) + self.assertIn('username', ret) + self.assertIn('email', ret) + + @mock.patch('tethys_services.backends.hydroshare.HydroShareOAuth2.get_json') + def test_user_data(self, mock_get_json): + # mock the jason response + mock_json_rval = mock.MagicMock() + mock_get_json.return_value = mock_json_rval + access_token = 'token1' + + hydro_share_auth2_obj = HydroShareOAuth2() + ret = hydro_share_auth2_obj.user_data(access_token) + + self.assertEquals(mock_json_rval, ret) + mock_get_json.assert_called_once_with(self.user_data_url, params={'access_token': 'token1'}) + + @mock.patch('tethys_services.backends.hydroshare.HydroShareOAuth2.get_json') + def test_user_data_value_error(self, mock_get_json): + # mock the jason response + mock_get_json.side_effect = ValueError + access_token = 'token1' + hydro_share_auth2_obj = HydroShareOAuth2() + ret = hydro_share_auth2_obj.user_data(access_token) + + self.assertEquals(None, ret) + mock_get_json.assert_called_once_with(self.user_data_url, params={'access_token': 'token1'}) + + @mock.patch('tethys_services.backends.hydroshare.BaseOAuth2.refresh_token') + def test_refresh_token(self, mock_request): + mock_response = dict( + email='foo@gmail.com', + username='user1', + access_token='token1', + token_type='type1', + expires_in=500, + expires_at=10000000, + refresh_token='234234', + scope='scope' + ) + mock_request.return_value = mock_response + + hydro_share_auth2_obj = HydroShareOAuth2() + + hydro_share_auth2_obj.set_expires_in_to = 100 + + ret = hydro_share_auth2_obj.refresh_token('token1') + + self.assertEquals('foo@gmail.com', ret['email']) + self.assertEquals('token1', ret['access_token']) + self.assertEquals('type1', ret['token_type']) + self.assertEquals(100, ret['expires_in']) + self.assertEquals('234234', ret['refresh_token']) + self.assertEquals('scope', ret['scope']) + self.assertIsNotNone(mock_response['expires_in']) diff --git a/tests/unit_tests/test_tethys_services/test_backends/test_hydroshare_beta.py b/tests/unit_tests/test_tethys_services/test_backends/test_hydroshare_beta.py new file mode 100644 index 000000000..9a9e772a9 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_backends/test_hydroshare_beta.py @@ -0,0 +1,27 @@ +import unittest +import mock +from tethys_services.backends.hydroshare_beta import HydroShareBetaOAuth2 + + +class TestHydroShareBeta(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_services.backends.hydroshare_beta.HydroShareOAuth2') + def test_HydroShareBetaOAuth2(self, mock_hydro_share_auth2): + hydro_share_beta_obj = HydroShareBetaOAuth2(mock_hydro_share_auth2) + + expected_auth_server_full_url = 'https://beta.hydroshare.org' + self.assertEqual(expected_auth_server_full_url, hydro_share_beta_obj.auth_server_full_url) + + expected_authorization_url = "https://beta.hydroshare.org/o/authorize/" + self.assertEqual(expected_authorization_url, hydro_share_beta_obj.AUTHORIZATION_URL) + + expected_access_toekn_url = "https://beta.hydroshare.org/o/token/" + self.assertEqual(expected_access_toekn_url, hydro_share_beta_obj.ACCESS_TOKEN_URL) + + expected_user_info = "https://beta.hydroshare.org/hsapi/userInfo/" + self.assertEqual(expected_user_info, hydro_share_beta_obj.USER_DATA_URL) diff --git a/tests/unit_tests/test_tethys_services/test_backends/test_hydroshare_playground.py b/tests/unit_tests/test_tethys_services/test_backends/test_hydroshare_playground.py new file mode 100644 index 000000000..0ba767601 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_backends/test_hydroshare_playground.py @@ -0,0 +1,27 @@ +import unittest +import mock +from tethys_services.backends.hydroshare_playground import HydroSharePlaygroundOAuth2 + + +class TestHydroSharePlayground(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_services.backends.hydroshare_beta.HydroShareOAuth2') + def test_HydroSharePlaygroundOAuth2(self, mock_hydro_share_auth2): + hydro_share_beta_obj = HydroSharePlaygroundOAuth2(mock_hydro_share_auth2) + + expected_auth_server_full_url = 'https://playground.hydroshare.org' + self.assertEqual(expected_auth_server_full_url, hydro_share_beta_obj.auth_server_full_url) + + expected_authorization_url = "https://playground.hydroshare.org/o/authorize/" + self.assertEqual(expected_authorization_url, hydro_share_beta_obj.AUTHORIZATION_URL) + + expected_access_toekn_url = "https://playground.hydroshare.org/o/token/" + self.assertEqual(expected_access_toekn_url, hydro_share_beta_obj.ACCESS_TOKEN_URL) + + expected_user_info = "https://playground.hydroshare.org/hsapi/userInfo/" + self.assertEqual(expected_user_info, hydro_share_beta_obj.USER_DATA_URL) diff --git a/tests/unit_tests/test_tethys_services/test_base.py b/tests/unit_tests/test_tethys_services/test_base.py new file mode 100644 index 000000000..6d6c92ab1 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_base.py @@ -0,0 +1,175 @@ +import unittest +import mock + +from tethys_services.base import DatasetService, SpatialDatasetService, WpsService + + +class TethysServicesBaseTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + # Data Services + + @mock.patch('tethys_services.base.pretty_output') + def test_DatasetService_init_valid_engine(self, mock_pretty_output): + expected_name = 'DataServices' + expected_type = 'ckan' + expected_endpoint = 'tethys_dataset_services.engines.CkanDatasetEngine' + ret = DatasetService(name=expected_name, type=expected_type, endpoint=expected_endpoint) + self.assertEquals(expected_name, ret.name) + self.assertEquals(expected_type, ret.type) + self.assertEquals(expected_endpoint, ret.endpoint) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertIn('DEPRECATION WARNING', po_call_args[0][0][0]) + + @mock.patch('tethys_services.base.pretty_output') + @mock.patch('tethys_services.base.len') + @mock.patch('tethys_services.base.list') + def test_DatasetService_init_with_more_than_two(self, mock_list, mock_len, mock_pretty_output): + mock_len.return_value = 3 + mock_list.return_value = ['ckan', 'hydroshare', 'geoserver'] + expected_name = 'DataServices' + expected_type = 'foo' + expected_endpoint = 'tethys_dataset_services.engines.CkanDatasetEngine' + + self.assertRaises(ValueError, DatasetService, name=expected_name, type=expected_type, + endpoint=expected_endpoint) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals(0, len(po_call_args)) + + @mock.patch('tethys_services.base.pretty_output') + def test_DatasetService_init_with_two(self, mock_pretty_output): + expected_name = 'DataServices' + expected_type = 'foo' + expected_endpoint = 'tethys_dataset_services.engines.CkanDatasetEngine' + self.assertRaises(ValueError, DatasetService, name=expected_name, type=expected_type, + endpoint=expected_endpoint) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals(0, len(po_call_args)) + + @mock.patch('tethys_services.base.pretty_output') + @mock.patch('tethys_services.base.len') + @mock.patch('tethys_services.base.list') + def test_DatasetService_init_with_less_than_two(self, mock_list, mock_len, mock_pretty_output): + mock_len.return_value = 1 + mock_list.return_value = ['ckan'] + expected_name = 'DataServices' + expected_type = 'foo' + expected_endpoint = 'tethys_dataset_services.engines.CkanDatasetEngine' + self.assertRaises(ValueError, DatasetService, name=expected_name, type=expected_type, + endpoint=expected_endpoint) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals(0, len(po_call_args)) + + @mock.patch('tethys_services.base.pretty_output') + def test_DatasetService_repr(self, mock_pretty_output): + expected_name = 'DataServices' + expected_type = 'ckan' + expected_endpoint = 'tethys_dataset_services.engines.CkanDatasetEngine' + ret = DatasetService(name=expected_name, type=expected_type, endpoint=expected_endpoint) + self.assertEquals('', + ret.__repr__()) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals(1, len(po_call_args)) + self.assertIn('DEPRECATION WARNING', po_call_args[0][0][0]) + + # Spatial Data Services + + @mock.patch('tethys_services.base.pretty_output') + def test_SpatialDatasetService_init_with_valid_spatial_engine(self, mock_pretty_output): + expected_name = 'SpatialDataServices' + expected_type = 'geoserver' + expected_endpoint = 'tethys_dataset_services.engines.GeoServerSpatialDatasetEngine' + ret = SpatialDatasetService(name=expected_name, type=expected_type, endpoint=expected_endpoint) + self.assertEquals(expected_name, ret.name) + self.assertEquals(expected_type, ret.type) + self.assertEquals(expected_endpoint, ret.endpoint) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertIn('DEPRECATION WARNING', po_call_args[0][0][0]) + + @mock.patch('tethys_services.base.pretty_output') + @mock.patch('tethys_services.base.len') + @mock.patch('tethys_services.base.list') + def test_SpatialDatasetService_init_with_invalid_spatial_engine_more_than_two(self, mock_list, + mock_len, + mock_pretty_output): + mock_len.return_value = 3 + mock_list.return_value = ['ckan', 'hydroshare', 'geoserver'] + expected_name = 'SpatialDataServices' + expected_type = 'foo' + expected_endpoint = 'end-point' + self.assertRaises(ValueError, SpatialDatasetService, name=expected_name, type=expected_type, + endpoint=expected_endpoint) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals(0, len(po_call_args)) + + @mock.patch('tethys_services.base.pretty_output') + @mock.patch('tethys_services.base.len') + @mock.patch('tethys_services.base.list') + def test_SpatialDatasetService_init_with_valid_spatial_engine_equals_two(self, mock_list, + mock_len, + mock_pretty_output): + mock_len.return_value = 2 + mock_list.return_value = ['hydroshare', 'geoserver'] + expected_name = 'SpatialDataServices' + expected_type = 'foo' + expected_endpoint = 'end-point' + self.assertRaises(ValueError, SpatialDatasetService, name=expected_name, type=expected_type, + endpoint=expected_endpoint) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals(0, len(po_call_args)) + + @mock.patch('tethys_services.base.pretty_output') + @mock.patch('tethys_services.base.len') + @mock.patch('tethys_services.base.list') + def test_SpatialDatasetService_init_with_valid_spatial_engine_less_than_two(self, mock_list, + mock_len, + mock_pretty_output): + mock_len.return_value = 1 + mock_list.return_value = ['geoserver'] + expected_name = 'SpatialDataServices' + expected_type = 'foo' + expected_endpoint = 'end-point' + self.assertRaises(ValueError, SpatialDatasetService, name=expected_name, type=expected_type, + endpoint=expected_endpoint) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals(0, len(po_call_args)) + + @mock.patch('tethys_services.base.pretty_output') + def test_SpatialDatasetService_repr(self, mock_pretty_output): + expected_name = 'SpatialDataServices' + expected_type = 'geoserver' + expected_endpoint = 'tethys_dataset_services.engines.GeoServerSpatialDatasetEngine' + ret = SpatialDatasetService(name=expected_name, type=expected_type, endpoint=expected_endpoint) + self.assertEquals('', ret.__repr__()) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals(1, len(po_call_args)) + self.assertIn('DEPRECATION WARNING', po_call_args[0][0][0]) + + # WpsService + + @mock.patch('tethys_services.base.pretty_output') + def test_WpsService_init(self, mock_pretty_output): + expected_name = 'foo' + expected_endpoint = 'end_point' + ret = WpsService(name=expected_name, endpoint=expected_endpoint) + self.assertEquals(expected_name, ret.name) + self.assertEquals(expected_endpoint, ret.endpoint) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals(1, len(po_call_args)) + self.assertIn('DEPRECATION WARNING', po_call_args[0][0][0]) + + @mock.patch('tethys_services.base.pretty_output') + def test_WpsService_repr(self, mock_pretty_output): + expected_name = 'foo' + expected_endpoint = 'end_point' + self.assertEquals('', + WpsService(name=expected_name, endpoint=expected_endpoint).__repr__()) + po_call_args = mock_pretty_output().__enter__().write.call_args_list + self.assertEquals(1, len(po_call_args)) + self.assertIn('DEPRECATION WARNING', po_call_args[0][0][0]) diff --git a/tests/unit_tests/test_tethys_services/test_models/__init__.py b/tests/unit_tests/test_tethys_services/test_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_services/test_models/test_DatasetService.py b/tests/unit_tests/test_tethys_services/test_models/test_DatasetService.py new file mode 100644 index 000000000..4306d94eb --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_models/test_DatasetService.py @@ -0,0 +1,62 @@ +from tethys_sdk.testing import TethysTestCase +import tethys_services.models as service_model +from django.core.exceptions import ObjectDoesNotExist +from social_core.exceptions import AuthException +import mock + + +class DatasetServiceTests(TethysTestCase): + def set_up(self): + pass + + def tear_down(self): + pass + + def test_unicode(self): + ds = service_model.DatasetService( + name='test_ds', + ) + + self.assertEqual('test_ds', str(ds)) + + @mock.patch('tethys_services.models.HydroShareDatasetEngine') + def test_get_engine_hydroshare(self, mock_hsde): + request = mock.MagicMock() + ds = service_model.DatasetService( + name='test_ds', + engine='tethys_dataset_services.engines.HydroShareDatasetEngine', + endpoint='http://localhost/api/3/action/', + apikey='test_api', + username='foo', + password='password' + + ) + ds.save() + ds.get_engine(request=request) + mock_hsde.assert_called_with(apikey='test_api', endpoint='http://localhost/api/3/action/', + password='password', username='foo') + + @mock.patch('tethys_services.models.HydroShareDatasetEngine') + def test_get_engine_hydroshare_error(self, _): + user = mock.MagicMock() + user.social_auth.get.side_effect = ObjectDoesNotExist + request = mock.MagicMock(user=user) + ds = service_model.DatasetService( + name='test_ds', + engine='tethys_dataset_services.engines.HydroShareDatasetEngine', + ) + self.assertRaises(AuthException, ds.get_engine, request=request) + + @mock.patch('tethys_services.models.CkanDatasetEngine') + def test_get_engine_ckan(self, mock_ckan): + ds = service_model.DatasetService( + name='test_ds', + apikey='test_api', + endpoint='http://localhost/api/3/action/', + username='foo', + password='password' + ) + ds.save() + ds.get_engine() + mock_ckan.assert_called_with(apikey='test_api', endpoint='http://localhost/api/3/action/', + password='password', username='foo') diff --git a/tests/unit_tests/test_tethys_services/test_models/test_PersistentStoreService.py b/tests/unit_tests/test_tethys_services/test_models/test_PersistentStoreService.py new file mode 100644 index 000000000..7ad155f08 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_models/test_PersistentStoreService.py @@ -0,0 +1,50 @@ +from tethys_sdk.testing import TethysTestCase +import tethys_services.models as service_model +import mock + + +class PersistentStoreServiceTests(TethysTestCase): + def set_up(self): + pass + + def tear_down(self): + pass + + def test_unicode(self): + pss = service_model.PersistentStoreService( + name='test_pss', + username='foo', + password='pass' + ) + self.assertEqual('test_pss', str(pss)) + + @mock.patch('sqlalchemy.engine.url.URL') + def test_get_url(self, mock_url): + pss = service_model.PersistentStoreService( + name='test_pss', + username='foo', + password='pass' + ) + + # Execute + pss.get_url() + + # Check if called correctly + mock_url.assert_called_with(database=None, drivername='postgresql', host='localhost', + password='pass', port=5435, username='foo') + + @mock.patch('tethys_services.models.PersistentStoreService.get_url') + @mock.patch('sqlalchemy.create_engine') + def test_get_engine(self, mock_ce, mock_url): + pss = service_model.PersistentStoreService( + name='test_pss', + username='foo', + password='pass' + ) + + mock_url.return_value = 'test_url' + # Execute + pss.get_engine() + + # Check if called correctly + mock_ce.assert_called_with('test_url') diff --git a/tests/unit_tests/test_tethys_services/test_models/test_SpatialDatasetService.py b/tests/unit_tests/test_tethys_services/test_models/test_SpatialDatasetService.py new file mode 100644 index 000000000..c020b0f43 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_models/test_SpatialDatasetService.py @@ -0,0 +1,33 @@ +from tethys_sdk.testing import TethysTestCase +import tethys_services.models as service_model +import mock + + +class SpatialDatasetServiceTests(TethysTestCase): + def set_up(self): + pass + + def tear_down(self): + pass + + def test_unicode(self): + sds = service_model.SpatialDatasetService( + name='test_sds', + ) + self.assertEqual('test_sds', sds.__unicode__()) + + @mock.patch('tethys_services.models.GeoServerSpatialDatasetEngine') + def test_get_engine_geo_server(self, mock_sds): + sds = service_model.SpatialDatasetService( + name='test_sds', + endpoint='http://localhost/geoserver/rest/', + public_endpoint='http://publichost/geoserver/rest/', + username='foo', + password='password' + ) + sds.save() + ret = sds.get_engine() + + # Check result + mock_sds.assert_called_with(endpoint='http://localhost/geoserver/rest/', password='password', username='foo') + self.assertEqual('http://publichost/geoserver/rest/', ret.public_endpoint) diff --git a/tests/unit_tests/test_tethys_services/test_models/test_WebProcessingService.py b/tests/unit_tests/test_tethys_services/test_models/test_WebProcessingService.py new file mode 100644 index 000000000..8ec4ad6ee --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_models/test_WebProcessingService.py @@ -0,0 +1,120 @@ +from tethys_sdk.testing import TethysTestCase +import tethys_services.models as service_model +import mock + +from tethys_services.models import HTTPError, URLError + + +class WebProcessingServiceTests(TethysTestCase): + def set_up(self): + pass + + def tear_down(self): + pass + + def test_unicode(self): + wps = service_model.WebProcessingService( + name='test_sds', + ) + self.assertEqual('test_sds', str(wps)) + + def test_activate(self): + wps = service_model.WebProcessingService( + name='test_sds', + endpoint='http://localhost/geoserver/rest/', + public_endpoint='http://publichost/geoserver/rest/', + username='foo', + password='password' + ) + wps.save() + # Check result + mock_wps = mock.MagicMock() + mock_wps.getcapabilities.return_value = 'test' + + ret = wps.activate(mock_wps) + + mock_wps.getcapabilities.assert_called() + self.assertEqual(mock_wps, ret) + + def test_activate_http_error_404(self): + wps = service_model.WebProcessingService( + name='test_sds', + endpoint='http://localhost/geoserver/rest/', + public_endpoint='http://publichost/geoserver/rest/', + username='foo', + password='password' + ) + + # Check result + mock_wps = mock.MagicMock() + mock_wps.getcapabilities.side_effect = HTTPError(url='test_url', code=404, msg='test_message', + hdrs='test_header', fp=None) + + self.assertRaises(HTTPError, wps.activate, mock_wps) + + def test_activate_http_error_not_404(self): + wps = service_model.WebProcessingService( + name='test_sds', + endpoint='http://localhost/geoserver/rest/', + public_endpoint='http://publichost/geoserver/rest/', + username='foo', + password='password' + ) + + # Check result + mock_wps = mock.MagicMock() + mock_wps.getcapabilities.side_effect = HTTPError(url='test_url', code=500, msg='test_message', + hdrs='test_header', fp=None) + + self.assertRaises(HTTPError, wps.activate, mock_wps) + + def test_activate_url_error(self): + wps = service_model.WebProcessingService( + name='test_sds', + endpoint='http://localhost/geoserver/rest/', + public_endpoint='http://publichost/geoserver/rest/', + username='foo', + password='password' + ) + + # Check result + mock_wps = mock.MagicMock() + mock_wps.getcapabilities.side_effect = URLError(reason='test_url') + + ret = wps.activate(mock_wps) + + self.assertIsNone(ret) + + def test_activate_error(self): + wps = service_model.WebProcessingService( + name='test_sds', + endpoint='http://localhost/geoserver/rest/', + public_endpoint='http://publichost/geoserver/rest/', + username='foo', + password='password' + ) + + # Check result + mock_wps = mock.MagicMock() + mock_wps.getcapabilities.side_effect = Exception + + self.assertRaises(Exception, wps.activate, mock_wps) + + @mock.patch('tethys_services.models.WebProcessingService.activate') + @mock.patch('tethys_services.models.WPS') + def test_get_engine(self, mock_wps, mock_activate): + wps = service_model.WebProcessingService( + name='test_sds', + endpoint='http://localhost/geoserver/rest/', + public_endpoint='http://publichost/geoserver/rest/', + username='foo', + password='password' + ) + wps.save() + # Execute + wps.get_engine() + + # Check called + mock_wps.assert_called_with('http://localhost/geoserver/rest/', password='password', + skip_caps=True, username='foo', verbose=False) + mock_activate.assert_called_with(wps=mock_wps()) diff --git a/tests/unit_tests/test_tethys_services/test_models/test_helper_functions.py b/tests/unit_tests/test_tethys_services/test_models/test_helper_functions.py new file mode 100644 index 000000000..85debeb79 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_models/test_helper_functions.py @@ -0,0 +1,40 @@ +from tethys_sdk.testing import TethysTestCase +import tethys_services.models as service_model +from django.core.exceptions import ValidationError + + +class HelperFunctionTests(TethysTestCase): + def set_up(self): + pass + + def tear_down(self): + pass + + def test_validate_url_valid(self): + test_url = 'http://' + raised = False + try: + service_model.validate_url(test_url) + except ValidationError: + raised = True + self.assertFalse(raised) + + def test_validate_url(self): + test_url = 'test_url' + self.assertRaises(ValidationError, service_model.validate_url, test_url) + + def test_validate_dataset_service_endpoint(self): + test_url = 'http://test_url' + self.assertRaises(ValidationError, service_model.validate_dataset_service_endpoint, test_url) + + def test_validate_spatial_dataset_service_endpoint(self): + test_url = 'http://test_url' + self.assertRaises(ValidationError, service_model.validate_spatial_dataset_service_endpoint, test_url) + + def test_validate_wps_service_endpoint(self): + test_url = 'http://test_url' + self.assertRaises(ValidationError, service_model.validate_wps_service_endpoint, test_url) + + def test_validate_persistent_store_port(self): + test_url = '800' + self.assertRaises(ValidationError, service_model.validate_persistent_store_port, test_url) diff --git a/tests/unit_tests/test_tethys_services/test_templatetags/__init__.py b/tests/unit_tests/test_tethys_services/test_templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_tethys_services/test_templatetags/test_tethys_services.py b/tests/unit_tests/test_tethys_services/test_templatetags/test_tethys_services.py new file mode 100644 index 000000000..b53eacd08 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_templatetags/test_tethys_services.py @@ -0,0 +1,25 @@ +import unittest +import mock + +from owslib.wps import ComplexData +from tethys_services.templatetags.tethys_services import is_complex_data + + +class TethysServicesIsComplexDataTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_is_complex_data_false(self): + mock_args = mock.MagicMock() + + self.assertFalse(is_complex_data(mock_args)) + + def test_is_compex_data_true(self): + mock_args = mock.MagicMock() + mock_args = ComplexData() + + self.assertTrue(is_complex_data(mock_args)) diff --git a/tests/unit_tests/test_tethys_services/test_urls.py b/tests/unit_tests/test_tethys_services/test_urls.py new file mode 100644 index 000000000..6197882d0 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_urls.py @@ -0,0 +1,39 @@ +from django.urls import reverse, resolve +from tethys_sdk.testing import TethysTestCase + + +class TestTethysServicesUrls(TethysTestCase): + + def set_up(self): + pass + + def tear_down(self): + pass + + def test_service_urls_wps_services(self): + url = reverse('services:wps_service', kwargs={'service': 'foo'}) + resolver = resolve(url) + self.assertEqual('/developer/services/wps/foo/', url) + self.assertEqual('wps_service', resolver.func.__name__) + self.assertEqual('tethys_services.views', resolver.func.__module__) + + def test_service_urls_wps_process(self): + url = reverse('services:wps_process', kwargs={'service': 'foo', 'identifier': 'bar'}) + resolver = resolve(url) + self.assertEqual('/developer/services/wps/foo/process/bar/', url) + self.assertEqual('wps_process', resolver.func.__name__) + self.assertEqual('tethys_services.views', resolver.func.__module__) + + def test_urlpatterns_datasethome(self): + url = reverse('services:datasets_home') + resolver = resolve(url) + self.assertEqual('/developer/services/datasets/', url) + self.assertEqual('datasets_home', resolver.func.__name__) + self.assertEqual('tethys_services.views', resolver.func.__module__) + + def test_urlpatterns_wpshome(self): + url = reverse('services:wps_home') + resolver = resolve(url) + self.assertEqual('/developer/services/wps/', url) + self.assertEqual('wps_home', resolver.func.__name__) + self.assertEqual('tethys_services.views', resolver.func.__module__) diff --git a/tests/unit_tests/test_tethys_services/test_utilities.py b/tests/unit_tests/test_tethys_services/test_utilities.py new file mode 100644 index 000000000..9c0f39227 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_utilities.py @@ -0,0 +1,595 @@ +import unittest +import mock + +from django.core.exceptions import ObjectDoesNotExist +from social_core.exceptions import AuthAlreadyAssociated, AuthException + +from tethys_dataset_services.engines import HydroShareDatasetEngine +from tethys_services.utilities import ensure_oauth2, initialize_engine_object, list_dataset_engines, \ + get_dataset_engine, list_spatial_dataset_engines, get_spatial_dataset_engine, abstract_is_link, activate_wps, \ + list_wps_service_engines, get_wps_service_engine +try: + from urllib2 import HTTPError, URLError +except ImportError: + from urllib.request import HTTPError, URLError + + +@ensure_oauth2('hydroshare') +def enforced_controller(request, *args, **kwargs): + return True + + +class TestUtilites(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_services.utilities.reverse') + @mock.patch('tethys_services.utilities.redirect') + def test_ensure_oauth2(self, mock_redirect, mock_reverse): + + mock_user = mock.MagicMock() + + mock_request = mock.MagicMock(user=mock_user, path='path') + + mock_redirect_url = mock.MagicMock() + + mock_reverse.return_value = mock_redirect_url + + enforced_controller(mock_request) + + mock_reverse.assert_called_once_with('social:begin', args=['hydroshare']) + + mock_redirect.assert_called_once() + + mock_user.social_auth.get.assert_called_once_with(provider='hydroshare') + + @mock.patch('tethys_services.utilities.reverse') + @mock.patch('tethys_services.utilities.redirect') + def test_ensure_oauth2_ObjectDoesNotExist(self, mock_redirect, mock_reverse): + from django.core.exceptions import ObjectDoesNotExist + + mock_user = mock.MagicMock() + + mock_request = mock.MagicMock(user=mock_user, path='path') + + mock_redirect_url = mock.MagicMock() + + mock_reverse.return_value = mock_redirect_url + + mock_user.social_auth.get.side_effect = ObjectDoesNotExist + + ret = enforced_controller(mock_request) + + mock_reverse.assert_called_once_with('social:begin', args=['hydroshare']) + + mock_redirect.assert_called_once() + + self.assertEquals(mock_redirect(), ret) + + @mock.patch('tethys_services.utilities.reverse') + @mock.patch('tethys_services.utilities.redirect') + def test_ensure_oauth2_AttributeError(self, mock_redirect, mock_reverse): + mock_user = mock.MagicMock() + + mock_request = mock.MagicMock(user=mock_user, path='path') + + mock_redirect_url = mock.MagicMock() + + mock_reverse.return_value = mock_redirect_url + + mock_user.social_auth.get.side_effect = AttributeError + + ret = enforced_controller(mock_request) + + mock_reverse.assert_called_once_with('social:begin', args=['hydroshare']) + + mock_redirect.assert_called_once() + + self.assertEquals(mock_redirect(), ret) + + @mock.patch('tethys_services.utilities.reverse') + @mock.patch('tethys_services.utilities.redirect') + def test_ensure_oauth2_AuthAlreadyAssociated(self, mock_redirect, mock_reverse): + from social_core.exceptions import AuthAlreadyAssociated + + mock_user = mock.MagicMock() + + mock_request = mock.MagicMock(user=mock_user, path='path') + + mock_redirect_url = mock.MagicMock() + + mock_reverse.return_value = mock_redirect_url + + mock_user.social_auth.get.side_effect = AuthAlreadyAssociated(mock.MagicMock(), mock.MagicMock()) + + self.assertRaises(AuthAlreadyAssociated, enforced_controller, mock_request) + + mock_reverse.assert_called_once_with('social:begin', args=['hydroshare']) + + mock_redirect.assert_called_once() + + @mock.patch('tethys_services.utilities.reverse') + @mock.patch('tethys_services.utilities.redirect') + def test_ensure_oauth2_Exception(self, mock_redirect, mock_reverse): + mock_user = mock.MagicMock() + + mock_request = mock.MagicMock(user=mock_user, path='path') + + mock_redirect_url = mock.MagicMock() + + mock_reverse.return_value = mock_redirect_url + + mock_user.social_auth.get.side_effect = Exception + + self.assertRaises(Exception, enforced_controller, mock_request) + + mock_reverse.assert_called_once_with('social:begin', args=['hydroshare']) + + mock_redirect.assert_called_once() + + def test_initialize_engine_object(self): + input_engine = 'tethys_dataset_services.engines.HydroShareDatasetEngine' + input_end_point = 'http://localhost/api/3/action' + + mock_user = mock.MagicMock() + mock_request = mock.MagicMock(user=mock_user, path='path') + + mock_social = mock.MagicMock() + + mock_user.social_auth.get.return_value = mock_social + + mock_api_key = mock.MagicMock() + + mock_social.extra_data['access_token'].return_value = mock_api_key + + ret = initialize_engine_object(engine=input_engine, endpoint=input_end_point, request=mock_request) + + mock_user.social_auth.get.assert_called_once_with(provider='hydroshare') + + self.assertEquals('http://localhost/api/3/action', ret.endpoint) + self.assertIsInstance(ret, HydroShareDatasetEngine) + + def test_initialize_engine_object_ObjectDoesNotExist(self): + input_engine = 'tethys_dataset_services.engines.HydroShareDatasetEngine' + input_end_point = 'http://localhost/api/3/action' + + mock_user = mock.MagicMock() + mock_request = mock.MagicMock(user=mock_user, path='path') + + mock_social = mock.MagicMock() + + mock_user.social_auth.get.side_effect = [ObjectDoesNotExist, mock_social] + + mock_social.extra_data['access_token'].return_value = None + + self.assertRaises(AuthException, initialize_engine_object, engine=input_engine, endpoint=input_end_point, + request=mock_request) + + mock_user.social_auth.get.assert_called_once_with(provider='hydroshare') + + def test_initialize_engine_object_AttributeError(self): + input_engine = 'tethys_dataset_services.engines.HydroShareDatasetEngine' + input_end_point = 'http://localhost/api/3/action' + + mock_user = mock.MagicMock() + mock_request = mock.MagicMock(user=mock_user, path='path') + + mock_social = mock.MagicMock() + + mock_user.social_auth.get.side_effect = [AttributeError, mock_social] + + self.assertRaises(AttributeError, initialize_engine_object, engine=input_engine, endpoint=input_end_point, + request=mock_request) + + mock_user.social_auth.get.assert_called_once_with(provider='hydroshare') + + def test_initialize_engine_object_AuthAlreadyAssociated(self): + input_engine = 'tethys_dataset_services.engines.HydroShareDatasetEngine' + input_end_point = 'http://localhost/api/3/action' + + mock_user = mock.MagicMock() + mock_request = mock.MagicMock(user=mock_user, path='path') + + mock_social = mock.MagicMock() + + mock_user.social_auth.get.side_effect = [AuthAlreadyAssociated(mock.MagicMock(), mock.MagicMock()), mock_social] + + self.assertRaises(AuthAlreadyAssociated, initialize_engine_object, engine=input_engine, + endpoint=input_end_point, request=mock_request) + + mock_user.social_auth.get.assert_called_once_with(provider='hydroshare') + + def test_initialize_engine_object_Exception(self): + input_engine = 'tethys_dataset_services.engines.HydroShareDatasetEngine' + input_end_point = 'http://localhost/api/3/action' + + mock_user = mock.MagicMock() + mock_request = mock.MagicMock(user=mock_user, path='path') + + mock_social = mock.MagicMock() + + mock_user.social_auth.get.side_effect = [Exception, mock_social] + + self.assertRaises(Exception, initialize_engine_object, engine=input_engine, endpoint=input_end_point, + request=mock_request) + + mock_user.social_auth.get.assert_called_once_with(provider='hydroshare') + + @mock.patch('tethys_services.utilities.DsModel.objects') + @mock.patch('tethys_services.utilities.initialize_engine_object') + def test_list_dataset_engines(self, mock_initialize_engine_object, mock_dsmodel): + + mock_engine = mock.MagicMock() + mock_endpoint = mock.MagicMock() + mock_api_key = mock.MagicMock() + mock_user_name = mock.MagicMock() + mock_password = mock.MagicMock() + mock_request = mock.MagicMock() + mock_public_endpoint = mock.MagicMock() + mock_site_dataset_service1 = mock.MagicMock(engine=mock_engine, + endpoint=mock_endpoint.endpoint, + apikey=mock_api_key, + username=mock_user_name, + password=mock_password, + request=mock_request, + public_endpoint=mock_public_endpoint) + + mock_site_dataset_services = [mock_site_dataset_service1] + + mock_dsmodel.all.return_value = mock_site_dataset_services + + mock_init_return = mock.MagicMock() + mock_init_return.public_endpoint = mock_site_dataset_service1.public_endpoint + + mock_initialize_engine_object.return_value = mock_init_return + + ret = list_dataset_engines() + + mock_initialize_engine_object.assert_called_with(apikey=mock_api_key, + endpoint=mock_endpoint.endpoint, + engine=mock_engine.encode('utf-8'), + password=mock_password, + request=None, + username=mock_user_name, + ) + + mock_dsmodel.all.assert_called_once() + + self.assertEquals(mock_init_return, ret[0]) + + @mock.patch('tethys_services.utilities.issubclass') + @mock.patch('tethys_services.utilities.initialize_engine_object') + def test_get_dataset_engine_app_dataset(self, mock_initialize_engine_object, mock_subclass): + from tethys_apps.base.app_base import TethysAppBase + + mock_name = 'foo' + mock_app_class = mock.MagicMock() + mock_subclass.return_value = True + mock_app_dataset_services = mock.MagicMock() + mock_app_dataset_services.name = 'foo' + + mock_app_class().dataset_services.return_value = [mock_app_dataset_services] + + mock_initialize_engine_object.return_value = True + + ret = get_dataset_engine(mock_name, mock_app_class) + + mock_subclass.assert_called_once_with(mock_app_class, TethysAppBase) + + mock_initialize_engine_object.assert_called_with(engine=mock_app_dataset_services.engine, + endpoint=mock_app_dataset_services.endpoint, + apikey=mock_app_dataset_services.apikey, + username=mock_app_dataset_services.username, + password=mock_app_dataset_services.password, + request=None) + + self.assertTrue(ret) + + @mock.patch('tethys_services.utilities.issubclass') + @mock.patch('tethys_services.utilities.initialize_engine_object') + @mock.patch('tethys_services.utilities.DsModel.objects.all') + def test_get_dataset_engine_dataset_services(self, mock_ds_model_object_all, mock_initialize_engine_object, + mock_subclass): + mock_name = 'foo' + + mock_subclass.return_value = False + + mock_init_return = mock.MagicMock() + + mock_initialize_engine_object.return_value = mock_init_return + + mock_site_dataset_services = mock.MagicMock() + + mock_site_dataset_services.name = 'foo' + + mock_ds_model_object_all.return_value = [mock_site_dataset_services] + + mock_init_return.public_endpoint = mock_site_dataset_services.public_endpoint + + ret = get_dataset_engine(mock_name, app_class=None) + + mock_initialize_engine_object.assert_called_with(engine=mock_site_dataset_services.engine.encode('utf-8'), + endpoint=mock_site_dataset_services.endpoint, + apikey=mock_site_dataset_services.apikey, + username=mock_site_dataset_services.username, + password=mock_site_dataset_services.password, + request=None) + + self.assertEquals(mock_init_return, ret) + + @mock.patch('tethys_services.utilities.initialize_engine_object') + @mock.patch('tethys_services.utilities.DsModel.objects.all') + def test_get_dataset_engine_name_error(self, mock_ds_model_object_all, mock_initialize_engine_object): + mock_name = 'foo' + + mock_site_dataset_services = mock.MagicMock() + + mock_site_dataset_services.name = 'foo' + + mock_ds_model_object_all.return_value = None + + self.assertRaises(NameError, get_dataset_engine, mock_name, app_class=None) + + mock_initialize_engine_object.assert_not_called() + + @mock.patch('tethys_services.utilities.initialize_engine_object') + @mock.patch('tethys_services.utilities.SdsModel') + def test_list_spatial_dataset_engines(self, mock_sds_model, mock_initialize): + mock_service1 = mock.MagicMock() + mock_sds_model.objects.all.return_value = [mock_service1] + mock_ret = mock.MagicMock() + mock_ret.public_endpoint = mock_service1.public_endpoint + mock_initialize.return_value = mock_ret + + ret = list_spatial_dataset_engines() + + self.assertEquals(mock_ret, ret[0]) + mock_sds_model.objects.all.assert_called_once() + mock_initialize.assert_called_once_with(engine=mock_service1.engine.encode('utf-8'), + endpoint=mock_service1.endpoint, + apikey=mock_service1.apikey, + username=mock_service1.username, + password=mock_service1.password) + + @mock.patch('tethys_services.utilities.initialize_engine_object') + @mock.patch('tethys_services.utilities.issubclass') + def test_get_spatial_dataset_engine_with_app(self, mock_issubclass, mock_initialize_engine_object): + from tethys_apps.base.app_base import TethysAppBase + + name = 'foo' + mock_app_class = mock.MagicMock() + mock_app_sds = mock.MagicMock() + mock_app_sds.name = 'foo' + mock_app_class().spatial_dataset_services.return_value = [mock_app_sds] + mock_issubclass.return_value = True + mock_initialize_engine_object.return_value = True + + ret = get_spatial_dataset_engine(name=name, app_class=mock_app_class) + + self.assertTrue(ret) + mock_issubclass.assert_called_once_with(mock_app_class, TethysAppBase) + mock_initialize_engine_object.assert_called_once_with(engine=mock_app_sds.engine, + endpoint=mock_app_sds.endpoint, + apikey=mock_app_sds.apikey, + username=mock_app_sds.username, + password=mock_app_sds.password) + + @mock.patch('tethys_services.utilities.initialize_engine_object') + @mock.patch('tethys_services.utilities.SdsModel') + def test_get_spatial_dataset_engine_with_site(self, mock_sds_model, mock_initialize_engine_object): + name = 'foo' + mock_site_sds = mock.MagicMock() + mock_site_sds.name = 'foo' + mock_sds_model.objects.all.return_value = [mock_site_sds] + mock_sdo = mock.MagicMock() + mock_sdo.public_endpoint = mock_site_sds.public_endpoint + mock_initialize_engine_object.return_value = mock_sdo + + ret = get_spatial_dataset_engine(name=name, app_class=None) + + self.assertEquals(mock_sdo, ret) + mock_initialize_engine_object.assert_called_once_with(engine=mock_site_sds.engine.encode('utf-8'), + endpoint=mock_site_sds.endpoint, + apikey=mock_site_sds.apikey, + username=mock_site_sds.username, + password=mock_site_sds.password) + + @mock.patch('tethys_services.utilities.SdsModel') + def test_get_spatial_dataset_engine_with_name_error(self, mock_sds_model): + name = 'foo' + mock_sds_model.objects.all.return_value = None + + self.assertRaises(NameError, get_spatial_dataset_engine, name=name, app_class=None) + + def test_abstract_is_link(self): + mock_process = mock.MagicMock() + mock_process.abstract = 'http://foo' + + ret = abstract_is_link(mock_process) + + self.assertTrue(ret) + + def test_abstract_is_link_false(self): + mock_process = mock.MagicMock() + mock_process.abstract = 'foo_bar' + + ret = abstract_is_link(mock_process) + + self.assertFalse(ret) + + def test_abstract_is_link_attribute_error(self): + ret = abstract_is_link(process=None) + + self.assertFalse(ret) + + def test_activate_wps(self): + mock_wps = mock.MagicMock() + mock_endpoint = mock.MagicMock() + mock_name = mock.MagicMock() + + ret = activate_wps(mock_wps, mock_endpoint, mock_name) + + mock_wps.getcapabilities.assert_called_once() + self.assertEqual(mock_wps, ret) + + def test_activate_wps_HTTPError_with_error_code_404(self): + mock_wps = mock.MagicMock() + mock_endpoint = mock.MagicMock() + mock_name = mock.MagicMock() + + mock_wps.getcapabilities.side_effect = HTTPError(url='test_url', code=404, msg='test_message', + hdrs='test_header', fp=None) + + self.assertRaises(HTTPError, activate_wps, mock_wps, mock_endpoint, mock_name) + + def test_activate_wps_HTTPError(self): + mock_wps = mock.MagicMock() + mock_endpoint = mock.MagicMock() + mock_name = mock.MagicMock() + + mock_wps.getcapabilities.side_effect = HTTPError(url='test_url', code=500, msg='test_message', + hdrs='test_header', fp=None) + + self.assertRaises(HTTPError, activate_wps, mock_wps, mock_endpoint, mock_name) + + def test_activate_wps_URLError(self): + mock_wps = mock.MagicMock() + mock_endpoint = mock.MagicMock() + mock_name = mock.MagicMock() + + mock_wps.getcapabilities.side_effect = URLError(reason='') + + self.assertIsNone(activate_wps(mock_wps, mock_endpoint, mock_name)) + + @mock.patch('tethys_services.utilities.activate_wps') + @mock.patch('tethys_services.utilities.WebProcessingService') + @mock.patch('tethys_services.utilities.issubclass') + def test_get_wps_service_engine_with_app(self, mock_issubclass, mock_wps_obj, mock_activate_wps): + from tethys_apps.base.app_base import TethysAppBase + + name = 'foo' + + mock_app_ws = mock.MagicMock() + mock_app_ws.name = 'foo' + + mock_app_class = mock.MagicMock() + mock_app_class().wps_services.return_value = [mock_app_ws] + + mock_issubclass.return_value = True + + mock_wps_obj.return_value = True + + ret = get_wps_service_engine(name=name, app_class=mock_app_class) + + self.assertTrue(ret) + + mock_issubclass.assert_called_once_with(mock_app_class, TethysAppBase) + + mock_wps_obj.assert_called_once_with(mock_app_ws.endpoint, + username=mock_app_ws.username, + password=mock_app_ws.password, + verbose=False, + skip_caps=True + ) + + mock_activate_wps.call_once_with(wps=True, endpoint=mock_app_ws.endpoint, name=mock_app_ws.name) + + @mock.patch('tethys_services.utilities.activate_wps') + @mock.patch('tethys_services.utilities.WebProcessingService') + @mock.patch('tethys_services.utilities.WpsModel') + def test_get_wps_service_engine_with_site(self, mock_wps_model, mock_wps, mock_activate_wps): + name = 'foo' + mock_site_ws = mock.MagicMock() + mock_site_ws.name = 'foo' + + mock_wps_model.objects.all.return_value = [mock_site_ws] + + mock_sdo = mock.MagicMock() + mock_sdo.public_endpoint = mock_site_ws.public_endpoint + + mock_wps.return_value = mock_sdo + + get_wps_service_engine(name=name, app_class=None) + + mock_wps.assert_called_once_with(mock_site_ws.endpoint, + username=mock_site_ws.username, + password=mock_site_ws.password, + verbose=False, + skip_caps=True) + + mock_activate_wps.call_once_with(wps=mock_sdo, endpoint=mock_site_ws.endpoint, name=mock_site_ws.name) + + @mock.patch('tethys_services.utilities.WpsModel') + def test_get_wps_service_engine_with_name_error(self, mock_wps_model): + name = 'foo' + mock_wps_model.objects.all.return_value = None + self.assertRaises(NameError, get_wps_service_engine, name=name, app_class=None) + + @mock.patch('tethys_services.utilities.activate_wps') + @mock.patch('tethys_services.utilities.WebProcessingService') + @mock.patch('tethys_services.utilities.issubclass') + def test_list_wps_service_engines_apps(self, mock_issubclass, mock_wps, mock_activate_wps): + from tethys_apps.base.app_base import TethysAppBase + + mock_app_ws = mock.MagicMock() + + mock_app_ws.name = 'foo' + + mock_app_class = mock.MagicMock() + mock_app_class().wps_services.return_value = [mock_app_ws] + + mock_issubclass.return_value = True + + mock_wps.return_value = True + + mock_activated_wps = mock.MagicMock() + + mock_activate_wps.return_value = mock_activated_wps + + ret = list_wps_service_engines(app_class=mock_app_class) + + mock_issubclass.assert_called_once_with(mock_app_class, TethysAppBase) + + mock_wps.assert_called_once_with(mock_app_ws.endpoint, + username=mock_app_ws.username, + password=mock_app_ws.password, + verbose=False, + skip_caps=True) + + mock_issubclass.assert_called_once_with(mock_app_class, TethysAppBase) + + self.assertEquals(mock_activate_wps(), ret[0]) + + @mock.patch('tethys_services.utilities.activate_wps') + @mock.patch('tethys_services.utilities.WebProcessingService') + @mock.patch('tethys_services.utilities.WpsModel') + def test_list_wps_service_engine_with_site(self, mock_wps_model, mock_wps, mock_activate_wps): + mock_site_ws = mock.MagicMock() + mock_site_ws.name = 'foo' + + mock_wps_model.objects.all.return_value = [mock_site_ws] + + mock_sdo = mock.MagicMock() + mock_sdo.public_endpoint = mock_site_ws.public_endpoint + + mock_wps.return_value = mock_sdo + + mock_activated_wps = mock.MagicMock() + + mock_activate_wps.return_value = mock_activated_wps + + ret = list_wps_service_engines(app_class=None) + + mock_wps.assert_called_once_with(mock_site_ws.endpoint, + username=mock_site_ws.username, + password=mock_site_ws.password, + verbose=False, + skip_caps=True) + + mock_activate_wps.call_once_with(wps=mock_sdo, endpoint=mock_site_ws.endpoint, name=mock_site_ws.name) + + self.assertEquals(mock_activate_wps(), ret[0]) diff --git a/tests/unit_tests/test_tethys_services/test_views.py b/tests/unit_tests/test_tethys_services/test_views.py new file mode 100644 index 000000000..55b138c12 --- /dev/null +++ b/tests/unit_tests/test_tethys_services/test_views.py @@ -0,0 +1,68 @@ +import unittest +import mock + +from tethys_services.views import datasets_home, wps_home, wps_service, wps_process + + +class TethysServicesViewsTests(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + @mock.patch('tethys_services.views.render') + def test_datasets_home(self, mock_render): + mock_request = mock.MagicMock() + mock_render.return_value = 'datasets_home' + context = {} + + self.assertEquals('datasets_home', datasets_home(mock_request)) + mock_render.assert_called_once_with(mock_request, 'tethys_services/tethys_datasets/home.html', context) + + @mock.patch('tethys_services.views.list_wps_service_engines') + @mock.patch('tethys_services.views.render') + def test_wps_home(self, mock_render, mock_list_wps_service_engines): + mock_request = mock.MagicMock() + mock_render.return_value = 'wps_home' + mock_wps = mock.MagicMock() + mock_list_wps_service_engines.return_value = mock_wps + context = {'wps_services': mock_wps} + + self.assertEquals('wps_home', wps_home(mock_request)) + mock_render.assert_called_once_with(mock_request, 'tethys_services/tethys_wps/home.html', context) + + @mock.patch('tethys_services.views.get_wps_service_engine') + @mock.patch('tethys_services.views.render') + def test_wps_service(self, mock_render, mock_get_wps_service_engine): + mock_request = mock.MagicMock() + mock_service = mock.MagicMock() + mock_render.return_value = 'wps_service' + mock_wps = mock.MagicMock() + mock_get_wps_service_engine.return_value = mock_wps + context = {'wps': mock_wps, + 'service': mock_service} + + self.assertEquals('wps_service', wps_service(mock_request, mock_service)) + mock_render.assert_called_once_with(mock_request, 'tethys_services/tethys_wps/service.html', context) + mock_get_wps_service_engine.assert_called_once_with(mock_service) + + @mock.patch('tethys_services.views.abstract_is_link') + @mock.patch('tethys_services.views.get_wps_service_engine') + @mock.patch('tethys_services.views.render') + def test_wps_process(self, mock_render, mock_get_wps_service_engine, mock_abstract_is_link): + mock_request = mock.MagicMock() + mock_service = mock.MagicMock() + mock_identifier = mock.MagicMock() + mock_wps_process = mock.MagicMock() + mock_render.return_value = 'wps_process' + mock_wps = mock.MagicMock() + mock_wps.describeprocess.return_value = mock_wps_process + mock_get_wps_service_engine.return_value = mock_wps + + self.assertEquals('wps_process', wps_process(mock_request, mock_service, mock_identifier)) + mock_render.assert_called_once() + mock_get_wps_service_engine.assert_called_once_with(mock_service) + mock_abstract_is_link.assert_called_once_with(mock_wps_process) + mock_wps.describeprocess.assert_called_once_with(mock_identifier) diff --git a/tethys_apps/__init__.py b/tethys_apps/__init__.py index cb2a86b23..b43c27210 100644 --- a/tethys_apps/__init__.py +++ b/tethys_apps/__init__.py @@ -7,20 +7,6 @@ * License: BSD 2-Clause ******************************************************************************** """ -import logging -import warnings # Load the custom app config default_app_config = 'tethys_apps.apps.TethysAppsConfig' - -# Configure logging -tethys_log = logging.getLogger('tethys') -default_log_format = logging.Formatter('%(levelname)s:%(name)s:%(message)s') -default_log_handler = logging.StreamHandler() -default_log_handler.setFormatter(default_log_format) -tethys_log.addHandler(default_log_handler) -logging.captureWarnings(True) -warnings.filterwarnings(action='always', category=DeprecationWarning) - - - diff --git a/tethys_apps/admin.py b/tethys_apps/admin.py index f8d6e636e..f026a3f72 100644 --- a/tethys_apps/admin.py +++ b/tethys_apps/admin.py @@ -10,6 +10,7 @@ from django.contrib import admin from guardian.admin import GuardedModelAdmin from tethys_apps.models import (TethysApp, + TethysExtension, CustomSetting, DatasetServiceSetting, SpatialDatasetServiceSetting, @@ -52,7 +53,7 @@ class WebProcessingServiceSettingInline(TethysAppSettingInline): model = WebProcessingServiceSetting -#TODO: Figure out how to initialize persistent stores with button in admin +# TODO: Figure out how to initialize persistent stores with button in admin # Consider: https://medium.com/@hakibenita/how-to-add-custom-action-buttons-to-django-admin-8d266f5b0d41 class PersistentStoreConnectionSettingInline(TethysAppSettingInline): readonly_fields = ('name', 'description', 'required') @@ -86,4 +87,17 @@ def has_delete_permission(self, request, obj=None): def has_add_permission(self, request): return False + +class TethysExtensionAdmin(GuardedModelAdmin): + readonly_fields = ('package', 'name', 'description') + fields = ('package', 'name', 'description', 'enabled') + + def has_delete_permission(self, request, obj=None): + return False + + def has_add_permission(self, request): + return False + + admin.site.register(TethysApp, TethysAppAdmin) +admin.site.register(TethysExtension, TethysExtensionAdmin) diff --git a/tethys_apps/app_harvester.py b/tethys_apps/app_harvester.py deleted file mode 100644 index 9f68ce0a7..000000000 --- a/tethys_apps/app_harvester.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -******************************************************************************** -* Name: app_harvester.py -* Author: Nathan Swain and Scott Christensen -* Created On: August 19, 2013 -* Copyright: (c) Brigham Young University 2013 -* License: BSD 2-Clause -******************************************************************************** -""" - -import os -import inspect - -from tethys_apps.base import TethysAppBase -from .terminal_colors import TerminalColors - - -class SingletonAppHarvester(object): - """ - Collects information for initiating apps - """ - - apps = [] - _instance = None - - def harvest_apps(self): - """ - Searches the apps package for apps - """ - # Notify user harvesting is taking place - print(TerminalColors.BLUE + 'Loading Tethys Apps...' + TerminalColors.ENDC) - - # List the apps packages in directory - apps_dir = os.path.join(os.path.dirname(__file__), 'tethysapp') - app_packages_list = [app_package for app_package in os.listdir(apps_dir) if app_package != '__pycache__'] - - # Harvest App Instances - self._harvest_app_instances(app_packages_list) - - def __new__(cls): - """ - Make App Harvester a Singleton - """ - if not cls._instance: - cls._instance = super(SingletonAppHarvester, cls).__new__(cls) - - return cls._instance - - @staticmethod - def _validate_app(app): - """ - Validate the app data that needs to be validated. Returns either the app if valid or None if not valid. - """ - # Remove prepended slash if included - if app.icon != '' and app.icon[0] == '/': - app.icon = app.icon[1:] - - # Validate color - if app.color != '' and app.color[0] != '#': - # Add hash - app.color = '#{0}'.format(app.color) - - # Must be 6 or 3 digit hex color (7 or 4 with hash symbol) - if len(app.color) != 7 and len(app.color) != 4: - app.color = '' - - return app - - def _harvest_app_instances(self, app_packages_list): - """ - Search each app package for the app.py module. Find the AppBase class in the app.py - module and instantiate it. Save the list of instantiated AppBase classes. - """ - valid_app_instance_list = [] - loaded_apps = [] - - for app_package in app_packages_list: - # Collect data from each app package in the apps directory - if app_package not in ['__init__.py', '__init__.pyc', '.gitignore', '.DS_Store']: - # Create the path to the app module in the custom app package - app_module_name = '.'.join(['tethys_apps.tethysapp', app_package, 'app']) - - # Import the app.py module from the custom app package programmatically - # (e.g.: apps.apps..app) - app_module = __import__(app_module_name, fromlist=['']) - - for name, obj in inspect.getmembers(app_module): - # Retrieve the members of the app_module and iterate through - # them to find the the class that inherits from AppBase. - try: - # issubclass() will fail if obj is not a class - if (issubclass(obj, TethysAppBase)) and (obj is not TethysAppBase): - # Assign a handle to the class - _appClass = getattr(app_module, name) - - # Instantiate app and validate - app_instance = _appClass() - validated_app_instance = self._validate_app(app_instance) - - # compile valid apps - if validated_app_instance: - valid_app_instance_list.append(validated_app_instance) - - # Notify user that the app has been loaded - loaded_apps.append(app_package) - - except TypeError: - '''DO NOTHING''' - except: - raise - - # Save valid apps - self.apps = valid_app_instance_list - - # Update user - print('Tethys Apps Loaded: {0}'.format(' '.join(loaded_apps))) diff --git a/tethys_apps/app_installation.py b/tethys_apps/app_installation.py index 6c40f1551..0f2a11cfc 100644 --- a/tethys_apps/app_installation.py +++ b/tethys_apps/app_installation.py @@ -12,8 +12,16 @@ import subprocess from setuptools.command.develop import develop from setuptools.command.install import install -from sys import platform as _platform import ctypes +from tethys_apps.cli.cli_colors import pretty_output, FG_BLACK + + +def find_resource_files(directory): + paths = [] + for (path, directories, filenames) in os.walk(directory): + for filename in filenames: + paths.append(os.path.join('..', path, filename)) + return paths def get_tethysapp_directory(): @@ -32,16 +40,17 @@ def _run_install(self): destination_dir = os.path.join(tethysapp_dir, self.app_package) # Notify user - print('Copying App Package: {0} to {1}'.format(self.app_package_dir, destination_dir)) + with pretty_output(FG_BLACK) as p: + p.write('Copying App Package: {0} to {1}'.format(self.app_package_dir, destination_dir)) # Copy files try: shutil.copytree(self.app_package_dir, destination_dir) - except: + except Exception: try: shutil.rmtree(destination_dir) - except: + except Exception: os.remove(destination_dir) shutil.copytree(self.app_package_dir, destination_dir) @@ -63,27 +72,31 @@ def _run_develop(self): destination_dir = os.path.join(tethysapp_dir, self.app_package) # Notify user - print('Creating Symbolic Link to App Package: {0} to {1}'.format(self.app_package_dir, destination_dir)) + with pretty_output(FG_BLACK) as p: + p.write('Creating Symbolic Link to App Package: {0} to {1}'.format(self.app_package_dir, destination_dir)) # Create symbolic link try: - os_symlink = getattr(os,"symlink",None) - if callable(os_symlink): - os.symlink(self.app_package_dir, destination_dir) - else: - def symlink_ms(source,dest): - csl = ctypes.windll.kernel32.CreateSymbolicLinkW - csl.argtypes = (ctypes.c_wchar_p,ctypes.c_wchar_p,ctypes.c_uint32) - csl.restype = ctypes.c_ubyte - flags = 1 if os.path.isdir(source) else 0 - if csl(dest,source.replace('/','\\'),flags) == 0: - raise ctypes.WinError() - os.symlink = symlink_ms(self.app_package_dir, destination_dir) + os_symlink = getattr(os, "symlink", None) + if callable(os_symlink): + os.symlink(self.app_package_dir, destination_dir) + else: + def symlink_ms(source, dest): + csl = ctypes.windll.kernel32.CreateSymbolicLinkW + csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32) + csl.restype = ctypes.c_ubyte + flags = 1 if os.path.isdir(source) else 0 + if csl(dest, source.replace('/', '\\'), flags) == 0: + raise ctypes.WinError() + + os.symlink = symlink_ms + symlink_ms(self.app_package_dir, destination_dir) except Exception as e: - print e + with pretty_output(FG_BLACK) as p: + p.write(e) try: shutil.rmtree(destination_dir) - except Exception as e: + except Exception: os.remove(destination_dir) os.symlink(self.app_package_dir, destination_dir) @@ -119,4 +132,4 @@ def custom_develop_command(app_package, app_package_dir, dependencies): 'dependencies': dependencies, 'run': _run_develop} - return type('CustomDevelopCommand', (develop, object), properties) \ No newline at end of file + return type('CustomDevelopCommand', (develop, object), properties) diff --git a/tethys_apps/apps.py b/tethys_apps/apps.py index 18608eada..97a1dfdf5 100644 --- a/tethys_apps/apps.py +++ b/tethys_apps/apps.py @@ -9,7 +9,7 @@ """ from django.apps import AppConfig -from tethys_apps.app_harvester import SingletonAppHarvester +from tethys_apps.harvester import SingletonHarvester class TethysAppsConfig(AppConfig): @@ -21,6 +21,5 @@ def ready(self): Startup method for Tethys Apps django app. """ # Perform App Harvesting - harvester = SingletonAppHarvester() - harvester.harvest_apps() - + harvester = SingletonHarvester() + harvester.harvest() diff --git a/tethys_apps/base/__init__.py b/tethys_apps/base/__init__.py index cb5689677..e7c01a70d 100644 --- a/tethys_apps/base/__init__.py +++ b/tethys_apps/base/__init__.py @@ -8,8 +8,8 @@ ******************************************************************************** """ # DO NOT ERASE -from tethys_apps.base.app_base import TethysAppBase -from tethys_apps.base.controller import app_controller_maker -from tethys_apps.base.url_map import url_map_maker -from tethys_apps.base.workspace import TethysWorkspace -from tethys_apps.base.permissions import Permission, PermissionGroup, has_permission +from tethys_apps.base.app_base import TethysAppBase, TethysExtensionBase # noqa: F401 +from tethys_apps.base.controller import app_controller_maker # noqa: F401 +from tethys_apps.base.url_map import url_map_maker # noqa: F401 +from tethys_apps.base.workspace import TethysWorkspace # noqa: F401 +from tethys_apps.base.permissions import Permission, PermissionGroup, has_permission # noqa: F401 diff --git a/tethys_apps/base/app_base.py b/tethys_apps/base/app_base.py index 7f8d6c128..d50eefa86 100644 --- a/tethys_apps/base/app_base.py +++ b/tethys_apps/base/app_base.py @@ -15,42 +15,132 @@ from django.core.exceptions import ObjectDoesNotExist from django.http import HttpRequest from django.utils.functional import SimpleLazyObject +from django.conf.urls import url -from tethys_apps.base.testing.environment import is_testing_environment +from tethys_apps.base.testing.environment import is_testing_environment, get_test_db_name, TESTING_DB_FLAG +from tethys_apps.base import permissions from .handoff import HandoffManager from .workspace import TethysWorkspace +from .mixins import TethysBaseMixin from ..exceptions import TethysAppSettingDoesNotExist, TethysAppSettingNotAssigned +from past.builtins import basestring + tethys_log = logging.getLogger('tethys.app_base') -class TethysAppBase(object): +class TethysBase(TethysBaseMixin): """ - Base class used to define the app class for Tethys apps. - - Attributes: - name (string): Name of the app. - index (string): Lookup term for the index URL of the app. - icon (string): Location of the image to use for the app icon. - package (string): Name of the app package. - root_url (string): Root URL of the app. - color (string): App theme color as RGB hexadecimal. - description (string): Description of the app. - tag (string): A string for filtering apps. - enable_feedback (boolean): Shows feedback button on all app pages. - feedback_emails (list): A list of emails corresponding to where submitted feedback forms are sent. - + Abstract base class of app and extension classes. """ name = '' - index = '' - icon = '' package = '' root_url = '' - color = '' description = '' - tags = '' - enable_feedback = False - feedback_emails = [] + + def __init__(self): + self._url_patterns = None + self._namespace = None + + def url_maps(self): + """ + Override this method to define the URL Maps for your app. Your ``UrlMap`` objects must be created from a ``UrlMap`` class that is bound to the ``root_url`` of your app. Use the ``url_map_maker()`` function to create the bound ``UrlMap`` class. If you generate your app project from the scaffold, this will be done automatically. + + Returns: + iterable: A list or tuple of ``UrlMap`` objects. + + **Example:** + + :: + + from tethys_sdk.base import url_map_maker + + class MyFirstApp(TethysAppBase): + + def url_maps(self): + \""" + Example url_maps method. + \""" + # Create UrlMap class that is bound to the root url. + UrlMap = url_map_maker(self.root_url) + + url_maps = (UrlMap(name='home', + url='my-first-app', + controller='my_first_app.controllers.home', + ), + ) + + return url_maps + """ # noqa: E501 + return [] + + @property + def url_patterns(self): + """ + Generate the url pattern lists for app and namespace them accordingly. + """ + if self._url_patterns is None: + is_extension = isinstance(self, TethysExtensionBase) + + url_patterns = dict() + + if hasattr(self, 'url_maps'): + url_maps = self.url_maps() + + for url_map in url_maps: + namespace = self.namespace + + if namespace not in url_patterns: + url_patterns[namespace] = [] + + # Create django url object + if isinstance(url_map.controller, basestring): + root_controller_path = 'tethysext' if is_extension else 'tethys_apps.tethysapp' + full_controller_path = '.'.join([root_controller_path, url_map.controller]) + controller_parts = full_controller_path.split('.') + module_name = '.'.join(controller_parts[:-1]) + function_name = controller_parts[-1] + try: + module = __import__(module_name, fromlist=[function_name]) + except Exception as e: + error_msg = 'The following error occurred while trying to import the controller function ' \ + '"{0}":\n {1}'.format(url_map.controller, traceback.format_exc(2)) + tethys_log.error(error_msg) + raise e + try: + controller_function = getattr(module, function_name) + except AttributeError as e: + error_msg = 'The following error occurred while trying to access the controller function ' \ + '"{0}":\n {1}'.format(url_map.controller, traceback.format_exc(2)) + tethys_log.error(error_msg) + raise e + else: + controller_function = url_map.controller + django_url = url(url_map.url, controller_function, name=url_map.name) + + # Append to namespace list + url_patterns[namespace].append(django_url) + self._url_patterns = url_patterns + + return self._url_patterns + + def sync_with_tethys_db(self): + """ + Sync installed apps with database. + """ + raise NotImplementedError + + def remove_from_db(self): + """ + Remove the instance from the db. + """ + raise NotImplementedError + + +class TethysExtensionBase(TethysBase): + """ + Base class used to define the extension class for Tethys extensions. + """ def __unicode__(self): """ @@ -93,8 +183,80 @@ def url_maps(self): ) return url_maps + """ # noqa: E501 + return [] + + def sync_with_tethys_db(self): + """ + Sync installed apps with database. """ - raise NotImplementedError() + from django.conf import settings + from tethys_apps.models import TethysExtension + try: + # Query to see if installed extension is in the database + db_extensions = TethysExtension.objects. \ + filter(package__exact=self.package). \ + all() + + # If the extension is not in the database, then add it + if len(db_extensions) == 0: + extension = TethysExtension( + name=self.name, + package=self.package, + description=self.description, + root_url=self.root_url, + ) + extension.save() + + # If the extension is in the database, update developer-first attributes + elif len(db_extensions) == 1: + db_extension = db_extensions[0] + db_extension.root_url = self.root_url + db_extension.save() + + if hasattr(settings, 'DEBUG') and settings.DEBUG: + db_extension.name = self.name + db_extension.description = self.description + db_extension.save() + except Exception as e: + tethys_log.error(e) + + +class TethysAppBase(TethysBase): + """ + Base class used to define the app class for Tethys apps. + + Attributes: + name (string): Name of the app. + index (string): Lookup term for the index URL of the app. + icon (string): Location of the image to use for the app icon. + package (string): Name of the app package. + root_url (string): Root URL of the app. + color (string): App theme color as RGB hexadecimal. + description (string): Description of the app. + tag (string): A string for filtering apps. + enable_feedback (boolean): Shows feedback button on all app pages. + feedback_emails (list): A list of emails corresponding to where submitted feedback forms are sent. + + """ + index = '' + icon = '' + color = '' + tags = '' + enable_feedback = False + feedback_emails = [] + + def __unicode__(self): + """ + String representation + """ + return ''.format(self.name) + + def __repr__(self): + """ + String representation + """ + return ''.format(self.name) def custom_settings(self): """ @@ -198,7 +360,7 @@ def persistent_store_settings(self): ) return ps_settings - """ + """ # noqa: E501 return None def dataset_service_settings(self): @@ -379,6 +541,132 @@ def permissions(self): """ return None + def register_app_permissions(self): + """ + Register and sync the app permissions. + """ + from guardian.shortcuts import assign_perm, remove_perm, get_perms + from django.contrib.contenttypes.models import ContentType + from django.contrib.auth.models import Permission, Group + from tethys_apps.models import TethysApp + + perms = self.permissions() + app_permissions = dict() + app_groups = dict() + + # Name spaced prefix for app permissions + # e.g. my_first_app:view_things + # e.g. my_first_app | View things + perm_codename_prefix = self.package + ':' + perm_name_prefix = self.package + ' | ' + + if perms is not None: + # Thing is either a Permission or a PermissionGroup object + + for thing in perms: + # Permission Case + if isinstance(thing, permissions.Permission): + # Name space the permissions and add it to the list + permission_codename = perm_codename_prefix + thing.name + permission_name = perm_name_prefix + thing.description + app_permissions[permission_codename] = permission_name + + # PermissionGroup Case + elif isinstance(thing, permissions.PermissionGroup): + # Record in dict of groups + group_permissions = [] + group_name = perm_codename_prefix + thing.name + + for perm in thing.permissions: + # Name space the permissions and add it to the list + permission_codename = perm_codename_prefix + perm.name + permission_name = perm_name_prefix + perm.description + app_permissions[permission_codename] = permission_name + group_permissions.append(permission_codename) + + # Store all groups for all apps + app_groups[group_name] = {'permissions': group_permissions, 'app_package': self.package} + + # Get the TethysApp content type + tethys_content_type = ContentType.objects.get( + app_label='tethys_apps', + model='tethysapp' + ) + + # Remove any permissions that no longer exist + db_app_permissions = Permission.objects.\ + filter(content_type=tethys_content_type).\ + filter(codename__icontains=perm_codename_prefix).\ + all() + + for db_app_permission in db_app_permissions: + # Delete the permission if the permission is no longer required by this app + if db_app_permission.codename not in app_permissions: + db_app_permission.delete() + + # Create permissions that need to be created + for perm in app_permissions: + # Create permission if it doesn't exist + try: + # If permission exists, update it + p = Permission.objects.get(codename=perm) + + p.name = app_permissions[perm] + p.content_type = tethys_content_type + p.save() + + except Permission.DoesNotExist: + p = Permission( + name=app_permissions[perm], + codename=perm, + content_type=tethys_content_type + ) + p.save() + + # Remove any groups that no longer exist + db_groups = Group.objects.filter(name__icontains=perm_codename_prefix).all() + db_apps = TethysApp.objects.all() + db_app_names = [db_app.package for db_app in db_apps] + + for db_group in db_groups: + db_group_name_parts = db_group.name.split(':') + + # Only perform maintenance on groups that belong to Tethys Apps + if (len(db_group_name_parts) > 1) and (db_group_name_parts[0] in db_app_names): + + # Delete groups that is no longer required by this app + if db_group.name not in app_groups: + db_group.delete() + + # Create groups that need to be created + for group in app_groups: + # Look up the app + db_app = TethysApp.objects.get(package=app_groups[group]['app_package']) + + # Create group if it doesn't exist + try: + # If it exists, update the permissions assigned to it + g = Group.objects.get(name=group) + + # Get the permissions for the group and remove all of them + perms = get_perms(g, db_app) + + for p in perms: + remove_perm(p, g, db_app) + + # Assign the permission to the group and the app instance + for p in app_groups[group]['permissions']: + assign_perm(p, g, db_app) + + except Group.DoesNotExist: + # Create a new group + g = Group(name=group) + g.save() + + # Assign the permission to the group and the app instance + for p in app_groups[group]['permissions']: + assign_perm(p, g, db_app) + def job_templates(self): """ Override this method to define job templates to easily create and submit jobs in your app. @@ -414,7 +702,7 @@ def job_templates(cls): ) return job_templates - """ + """ # noqa: E501 return None @classmethod @@ -521,8 +809,8 @@ def a_controller(request): """ # Find the path to the app project directory - ## Hint: cls is a child class of this class. - ## Credits: http://stackoverflow.com/questions/4006102/is-possible-to-know-the-_path-of-the-file-of-a-subclass-in-python + # Hint: cls is a child class of this class. + # Credits: http://stackoverflow.com/questions/4006102/ is-possible-to-know-the-_path-of-the-file-of-a-subclass-in-python # noqa: E501 project_directory = os.path.dirname(sys.modules[cls.__module__].__file__) workspace_directory = os.path.join(project_directory, 'workspaces', 'app_workspace') return TethysWorkspace(workspace_directory) @@ -552,10 +840,9 @@ def get_custom_setting(cls, name): custom_settings = db_app.custom_settings try: custom_setting = custom_settings.get(name=name) + return custom_setting.get_value() except ObjectDoesNotExist: - raise TethysAppSettingDoesNotExist('CustomTethysAppSetting named "{0}" does not exist.'.format(name)) - - return custom_setting.get_value() + raise TethysAppSettingDoesNotExist('CustomTethysAppSetting', name, cls.name) @classmethod def get_dataset_service(cls, name, as_public_endpoint=False, as_endpoint=False, @@ -587,21 +874,11 @@ def get_dataset_service(cls, name, as_public_endpoint=False, as_endpoint=False, dataset_services_settings = db_app.dataset_services_settings try: - dataset_services_settings = dataset_services_settings.get(name=name) + dataset_services_setting = dataset_services_settings.get(name=name) + dataset_services_setting.get_value(as_public_endpoint=as_public_endpoint, as_endpoint=as_endpoint, + as_engine=as_engine) except ObjectDoesNotExist: - raise TethysAppSettingDoesNotExist('DatasetServiceSetting named "{0}" does not exist.'.format(name)) - - dataset_service = dataset_services_settings.dataset_service - - if not dataset_service: - return None - elif as_engine: - return dataset_service.get_engine() - elif as_endpoint: - return dataset_service.endpoint - elif as_public_endpoint: - return dataset_service.public_endpoint - return dataset_service + raise TethysAppSettingDoesNotExist('DatasetServiceSetting', name, cls.name) @classmethod def get_spatial_dataset_service(cls, name, as_public_endpoint=False, as_endpoint=False, as_wms=False, @@ -637,24 +914,14 @@ def get_spatial_dataset_service(cls, name, as_public_endpoint=False, as_endpoint try: spatial_dataset_service_setting = spatial_dataset_service_settings.get(name=name) + return spatial_dataset_service_setting.get_value( + as_public_endpoint=as_public_endpoint, + as_endpoint=as_endpoint, + as_wms=as_wms, as_wfs=as_wfs, + as_engine=as_engine + ) except ObjectDoesNotExist: - raise TethysAppSettingDoesNotExist('SpatialDatasetServiceSetting named "{0}" does not exist.'.format(name)) - - spatial_dataset_service = spatial_dataset_service_setting.spatial_dataset_service - - if not spatial_dataset_service: - return None - elif as_engine: - return spatial_dataset_service.get_engine() - elif as_wms: - return spatial_dataset_service.endpoint.split('/rest')[0] + '/wms' - elif as_wfs: - return spatial_dataset_service.endpoint.split('/rest')[0] + '/ows' - elif as_endpoint: - return spatial_dataset_service.endpoint - elif as_public_endpoint: - return spatial_dataset_service.public_endpoint - return spatial_dataset_service + raise TethysAppSettingDoesNotExist('SpatialDatasetServiceSetting', name, cls.name) @classmethod def get_web_processing_service(cls, name, as_public_endpoint=False, as_endpoint=False, as_engine=False): @@ -684,19 +951,10 @@ def get_web_processing_service(cls, name, as_public_endpoint=False, as_endpoint= wps_services_settings = db_app.wps_services_settings try: wps_service_setting = wps_services_settings.objects.get(name=name) + return wps_service_setting.get_value(as_public_endpoint=as_public_endpoint, + as_endpoint=as_endpoint, as_engine=as_engine) except ObjectDoesNotExist: - raise TethysAppSettingDoesNotExist('WebProcessingServiceSetting named "{0}" does not exist.'.format(name)) - wps_service = wps_service_setting.web_processing_service - - if not wps_service: - return None - elif as_engine: - return wps_service.get_engine() - elif as_endpoint: - return wps_service.endpoint - elif as_public_endpoint: - return wps_service.pubic_endpoint - return wps_service + raise TethysAppSettingDoesNotExist('WebProcessingServiceSetting', name, cls.name) @classmethod def get_persistent_store_connection(cls, name, as_url=False, as_sessionmaker=False): @@ -728,16 +986,13 @@ def get_persistent_store_connection(cls, name, as_url=False, as_sessionmaker=Fal db_app = TethysApp.objects.get(package=cls.package) ps_connection_settings = db_app.persistent_store_connection_settings - if is_testing_environment(): - if 'tethys-testing_' not in name: - test_store_name = 'tethys-testing_{0}'.format(name) - name = test_store_name - try: + # Return as_engine if the other two are False + as_engine = not as_sessionmaker and not as_url ps_connection_setting = ps_connection_settings.get(name=name) - return ps_connection_setting.get_engine(as_url=as_url, as_sessionmaker=as_sessionmaker) + return ps_connection_setting.get_value(as_url=as_url, as_sessionmaker=as_sessionmaker, as_engine=as_engine) except ObjectDoesNotExist: - raise TethysAppSettingDoesNotExist('PersistentStoreConnectionSetting named "{0}" does not exist.'.format(name)) + raise TethysAppSettingDoesNotExist('PersistentStoreConnectionSetting', name, cls.name) except TethysAppSettingNotAssigned: cls._log_tethys_app_setting_not_assigned_error('PersistentStoreConnectionSetting', name) @@ -750,7 +1005,7 @@ def get_persistent_store_database(cls, name, as_url=False, as_sessionmaker=False name(string): Name of the PersistentStoreConnectionSetting as defined in app.py. as_url(bool): Return SQLAlchemy URL object instead of engine object if True. Defaults to False. as_sessionmaker(bool): Returns SessionMaker class bound to the engine if True. Defaults to False. - + Returns: sqlalchemy.Engine or sqlalchemy.URL: An SQLAlchemy Engine or URL object for the persistent store requested. @@ -770,18 +1025,18 @@ def get_persistent_store_database(cls, name, as_url=False, as_sessionmaker=False db_app = TethysApp.objects.get(package=cls.package) ps_database_settings = db_app.persistent_store_database_settings - if is_testing_environment(): - if 'tethys-testing_' not in name: - test_store_name = 'tethys-testing_{0}'.format(name) - name = test_store_name + verified_name = name if not is_testing_environment() else get_test_db_name(name) try: - ps_database_setting = ps_database_settings.get(name=name) - return ps_database_setting.get_engine(with_db=True, as_url=as_url, as_sessionmaker=as_sessionmaker) + # Return as_engine if the other two are False + as_engine = not as_sessionmaker and not as_url + ps_database_setting = ps_database_settings.get(name=verified_name) + return ps_database_setting.get_value(with_db=True, as_url=as_url, as_sessionmaker=as_sessionmaker, + as_engine=as_engine) except ObjectDoesNotExist: - raise TethysAppSettingDoesNotExist('PersistentStoreDatabaseSetting named "{0}" does not exist.'.format(name)) + raise TethysAppSettingDoesNotExist('PersistentStoreDatabaseSetting', verified_name, cls.name) except TethysAppSettingNotAssigned: - cls._log_tethys_app_setting_not_assigned_error('PersistentStoreDatabaseSetting', name) + cls._log_tethys_app_setting_not_assigned_error('PersistentStoreDatabaseSetting', verified_name) @classmethod def create_persistent_store(cls, db_name, connection_name, spatial=False, initializer='', refresh=False, @@ -791,7 +1046,7 @@ def create_persistent_store(cls, db_name, connection_name, spatial=False, initia Args: db_name(string): Name of the persistent store that will be created. - connection_name(string): Name of persistent store connection. + connection_name(string|None): Name of persistent store connection or None if creating a test copy of an existing persistent store (only while in the testing environment) spatial(bool): Enable spatial extension on the database being created when True. Connection must have superuser role. Defaults to False. initializer(string): Dot-notation path to initializer function (e.g.: 'my_first_app.models.init_db'). refresh(bool): Drop database if it exists and create again when True. Defaults to False. @@ -810,9 +1065,9 @@ def create_persistent_store(cls, db_name, connection_name, spatial=False, initia result = app.create_persistent_store('example_db', 'primary') if result: - engine = app.get_persistent_store_database('example_db') + engine = app.get_persistent_store_engine('example_db') - """ + """ # noqa: E501 # Get named persistent store service connection from tethys_apps.models import TethysApp from tethys_apps.models import PersistentStoreDatabaseSetting @@ -822,28 +1077,39 @@ def create_persistent_store(cls, db_name, connection_name, spatial=False, initia ps_connection_settings = db_app.persistent_store_connection_settings if is_testing_environment(): - if 'tethys-testing_' not in connection_name: - test_store_name = 'test_{0}'.format(connection_name) - connection_name = test_store_name + verified_db_name = get_test_db_name(db_name) + else: + verified_db_name = db_name + if connection_name is None: + raise ValueError('The connection_name cannot be None unless running in the testing environment.') try: - ps_connection_setting = ps_connection_settings.get(name=connection_name) + if connection_name is None: + ps_database_settings = db_app.persistent_store_database_settings + ps_setting = ps_database_settings.get(name=db_name) + else: + ps_setting = ps_connection_settings.get(name=connection_name) except ObjectDoesNotExist: - raise TethysAppSettingDoesNotExist( - 'PersistentStoreConnectionSetting named "{0}" does not exist.'.format(connection_name)) + if connection_name is None: + raise TethysAppSettingDoesNotExist( + 'PersistentStoreDatabaseSetting named "{0}" does not exist.'.format(db_name), + connection_name, cls.name) + else: + raise TethysAppSettingDoesNotExist( + 'PersistentStoreConnectionSetting ', connection_name, cls.name) - ps_service = ps_connection_setting.persistent_store_service + ps_service = ps_setting.persistent_store_service # Check if persistent store database setting already exists before creating it try: - db_setting = db_app.persistent_store_database_settings.get(name=db_name) + db_setting = db_app.persistent_store_database_settings.get(name=verified_db_name) db_setting.persistent_store_service = ps_service db_setting.initializer = initializer db_setting.save() except ObjectDoesNotExist: # Create new PersistentStoreDatabaseSetting db_setting = PersistentStoreDatabaseSetting( - name=db_name, + name=verified_db_name, description='', required=False, initializer=initializer, @@ -892,14 +1158,10 @@ def drop_persistent_store(cls, name): from tethys_apps.models import TethysApp db_app = TethysApp.objects.get(package=cls.package) ps_database_settings = db_app.persistent_store_database_settings - - if is_testing_environment(): - if 'tethys-testing_' not in name: - test_store_name = 'tethys-testing_{0}'.format(name) - name = test_store_name + verified_name = name if not is_testing_environment() else get_test_db_name(name) try: - ps_database_setting = ps_database_settings.get(name=name) + ps_database_setting = ps_database_settings.get(name=verified_name) except ObjectDoesNotExist: return True @@ -940,7 +1202,7 @@ def list_persistent_store_databases(cls, dynamic_only=False, static_only=False): elif static_only: ps_database_settings = ps_database_settings.filter(persistentstoredatabasesetting__dynamic=False) return [ps_database_setting.name for ps_database_setting in ps_database_settings - if 'tethys-testing_' not in ps_database_setting.name] + if TESTING_DB_FLAG not in ps_database_setting.name] @classmethod def list_persistent_store_connections(cls): @@ -964,7 +1226,7 @@ def list_persistent_store_connections(cls): db_app = TethysApp.objects.get(package=cls.package) ps_connection_settings = db_app.persistent_store_connection_settings return [ps_connection_setting.name for ps_connection_setting in ps_connection_settings - if 'tethys-testing_' not in ps_connection_setting.name] + if TESTING_DB_FLAG not in ps_connection_setting.name] @classmethod def persistent_store_exists(cls, name): @@ -987,21 +1249,18 @@ def persistent_store_exists(cls, name): result = app.persistent_store_exists('example_db') if result: - engine = app.get_persistent_store_database('example_db') + engine = app.get_persistent_store_engine('example_db') """ from tethys_apps.models import TethysApp db_app = TethysApp.objects.get(package=cls.package) ps_database_settings = db_app.persistent_store_database_settings - if is_testing_environment(): - if 'tethys-testing_' not in name: - test_store_name = 'tethys-testing_{0}'.format(name) - name = test_store_name + verified_name = name if not is_testing_environment() else get_test_db_name(name) try: # If it exists return True - ps_database_setting = ps_database_settings.get(name=name) + ps_database_setting = ps_database_settings.get(name=verified_name) except ObjectDoesNotExist: # Else return False return False @@ -1010,6 +1269,94 @@ def persistent_store_exists(cls, name): ps_database_setting.persistent_store_database_exists() return True + def sync_with_tethys_db(self): + """ + Sync installed apps with database. + """ + from django.conf import settings + from tethys_apps.models import TethysApp + + try: + # Make pass to add apps to db that are newly installed + # Query to see if installed app is in the database + db_apps = TethysApp.objects.\ + filter(package__exact=self.package).all() + + # If the app is not in the database, then add it + if len(db_apps) == 0: + db_app = TethysApp( + name=self.name, + package=self.package, + description=self.description, + enable_feedback=self.enable_feedback, + feedback_emails=self.feedback_emails, + index=self.index, + icon=self.icon, + root_url=self.root_url, + color=self.color, + tags=self.tags + ) + db_app.save() + + # custom settings + db_app.add_settings(self.custom_settings()) + # dataset services settings + db_app.add_settings(self.dataset_service_settings()) + # spatial dataset services settings + db_app.add_settings(self.spatial_dataset_service_settings()) + # wps settings + db_app.add_settings(self.web_processing_service_settings()) + # persistent store settings + db_app.add_settings(self.persistent_store_settings()) + + db_app.save() + + # If the app is in the database, update developer-first attributes + elif len(db_apps) == 1: + db_app = db_apps[0] + db_app.index = self.index + db_app.icon = self.icon + db_app.root_url = self.root_url + db_app.color = self.color + + # custom settings + db_app.add_settings(self.custom_settings()) + # dataset services settings + db_app.add_settings(self.dataset_service_settings()) + # spatial dataset services settings + db_app.add_settings(self.spatial_dataset_service_settings()) + # wps settings + db_app.add_settings(self.web_processing_service_settings()) + # persistent store settings + db_app.add_settings(self.persistent_store_settings()) + db_app.save() + + if hasattr(settings, 'DEBUG') and settings.DEBUG: + db_app.name = self.name + db_app.description = self.description + db_app.tags = self.tags + db_app.enable_feedback = self.enable_feedback + db_app.feedback_emails = self.feedback_emails + db_app.save() + + # More than one instance of the app in db... (what to do here?) + elif len(db_apps) >= 2: + pass + except Exception as e: + tethys_log.error(e) + + def remove_from_db(self): + """ + Remove the instance from the db. + """ + from tethys_apps.models import TethysApp + + try: + # Attempt to delete the object + TethysApp.objects.filter(package__exact=self.package).delete() + except Exception as e: + tethys_log.error(e) + @classmethod def _log_tethys_app_setting_not_assigned_error(cls, setting_type, setting_name): """ @@ -1025,6 +1372,6 @@ def _log_tethys_app_setting_not_assigned_error(cls, setting_type, setting_name): tethys_log.warn('Tethys app setting is not assigned.\nTraceback (most recent call last):\n{0} ' 'TethysAppSettingNotAssigned: {1} named "{2}" has not been assigned. ' 'Please visit the setting page for the app {3} and assign all required settings.' - .format(traceback.format_stack(limit=3)[0], setting_type, setting_name, cls.name) + .format(traceback.format_stack(limit=3)[0], setting_type, setting_name, + cls.name.encode('utf-8')) ) - diff --git a/tethys_apps/base/controller.py b/tethys_apps/base/controller.py index f55f18e64..fcfad377d 100644 --- a/tethys_apps/base/controller.py +++ b/tethys_apps/base/controller.py @@ -7,7 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ - +from django.views.generic import View from .url_map import UrlMapBase @@ -19,3 +19,11 @@ def app_controller_maker(root_url): return type('UrlMap', (UrlMapBase,), properties) +class TethysController(View): + + @classmethod + def as_controller(cls, **kwargs): + """ + Thin veneer around the as_view method to make interface more consistent with Tethys terminology. + """ + return cls.as_view(**kwargs) diff --git a/tethys_apps/base/function_extractor.py b/tethys_apps/base/function_extractor.py index 55027ae87..06784c2a6 100644 --- a/tethys_apps/base/function_extractor.py +++ b/tethys_apps/base/function_extractor.py @@ -6,7 +6,7 @@ class TethysFunctionExtractor(object): Base class for PersistentStore and HandoffHandler that returns a function handle from a string path to the function. Attributes: - path (str): The path to a function in the form "app_name.module_name.function_name". + path (str): The path to a function in the form "app_name.module_name.function_name" or the function object. """ PATH_PREFIX = 'tethys_apps.tethysapp' @@ -17,28 +17,13 @@ def __init__(self, path, prefix=PATH_PREFIX, throw=False): self._valid = None self._function = None + # Handle function object case if not isinstance(path, basestring): - if path.callable(): - self.valid = True - self.function = path - - @property - def valid(self): - """ - True if function is valid otherwise False. - """ - if self._valid is None: - self.function - return self._valid - - @property - def function(self): - """ - The function pointed to by the path_str attribute. + if callable(path): + self._valid = True + self._function = path - Returns: - A handle to a Python function or None if function is not valid. - """ + def _extract_function(self): if not self._function and self._valid is None: try: # Split into parts and extract function name @@ -48,7 +33,7 @@ def function(self): full_module_path = '.'.join((self.prefix, module_path)) if self.prefix else module_path # Import module - module = __import__(full_module_path, fromlist=[function_name]) + module = __import__(full_module_path, fromlist=[str(function_name)]) except (ValueError, ImportError) as e: self._valid = False @@ -59,4 +44,23 @@ def function(self): self._function = getattr(module, function_name) self._valid = True - return self._function \ No newline at end of file + @property + def valid(self): + """ + True if function is valid otherwise False. + """ + if self._valid is None: + self._extract_function() + return self._valid + + @property + def function(self): + """ + The function pointed to by the path_str attribute. + + Returns: + A handle to a Python function or None if function is not valid. + """ + if self._function is None: + self._extract_function() + return self._function diff --git a/tethys_apps/base/handoff.py b/tethys_apps/base/handoff.py index 0ef8a2254..3fb48ddd9 100644 --- a/tethys_apps/base/handoff.py +++ b/tethys_apps/base/handoff.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ +from __future__ import print_function import inspect import json from django.shortcuts import redirect @@ -52,7 +53,7 @@ def get_capabilities(self, app_name=None, external_only=False, jsonify=False): Returns: A list of valid HandoffHandler objects (or a JSON string if jsonify=True) representing the capabilities of app_name, or None if no app with app_name is found. - """ + """ # noqa: E501 manager = self._get_handoff_manager_for_app(app_name) if manager: @@ -62,7 +63,7 @@ def get_capabilities(self, app_name=None, external_only=False, jsonify=False): handlers = [handler for handler in handlers if not handler.internal] if jsonify: - handlers = json.dumps([handler.__json__() for handler in handlers]) + handlers = json.dumps([handler.__dict__ for handler in handlers]) return handlers @@ -76,7 +77,7 @@ def get_handler(self, handler_name, app_name=None): Returns: A HandoffHandler object where the name attribute is equal to handler_name or None if no HandoffHandler with that name is found or no app with app_name is found. - """ + """ # noqa: E501 manager = self._get_handoff_manager_for_app(app_name) if manager: @@ -96,7 +97,7 @@ def handoff(self, request, handler_name, app_name=None, external_only=True, **kw Returns: HttpResponse object. - """ + """ # noqa: E501 error = {"message": "", "code": 400, @@ -113,10 +114,11 @@ def handoff(self, request, handler_name, app_name=None, external_only=True, **kw urlish = handler(request, **kwargs) return redirect(urlish) except TypeError as e: - error['message'] = "HTTP 400 Bad Request: {0}. ".format(e.message) + error['message'] = "HTTP 400 Bad Request: {0}. ".format(str(e)) return HttpResponseBadRequest(json.dumps(error), content_type='application/javascript') - error['message'] = "HTTP 400 Bad Request: No handoff handler '{0}' for app '{1}' found.".format(manager.app.name, handler_name) + error['message'] = "HTTP 400 Bad Request: No handoff handler '{0}' for app '{1}' found.".\ + format(manager.app.name, handler_name) return HttpResponseBadRequest(json.dumps(error), content_type='application/javascript') def _get_handoff_manager_for_app(self, app_name): @@ -128,13 +130,12 @@ def _get_handoff_manager_for_app(self, app_name): Returns: A HandoffManager object for the app with the name app_name or None if no app with that name is found. - """ - + """ # noqa: E501 if not app_name: return self # Get the app - harvester = tethys_apps.app_harvester.SingletonAppHarvester() + harvester = tethys_apps.harvester.SingletonAppHarvester() apps = harvester.apps for app in apps: @@ -156,10 +157,12 @@ def _get_valid_handlers(self): else: handler_str = handler.handler if ':' in handler_str: - print('DEPRECATION WARNING: The handler attribute of a HandoffHandler should now be in the form: "my_first_app.controllers.my_handler". The form "handoff:my_handler" is now deprecated.') + print('DEPRECATION WARNING: The handler attribute of a HandoffHandler should now be in the ' + 'form: "my_first_app.controllers.my_handler". The form "handoff:my_handler" ' + 'is now deprecated.') # Split into module name and function name - module_path, function_name = handler_str.split(':') + module_path, function_name = handler_str.split(':') # Pre-process handler path full_module_path = '.'.join(('tethys_apps.tethysapp', self.app.package, module_path)) @@ -185,7 +188,7 @@ class HandoffHandler(TethysFunctionExtractor): name(str): Name of the handoff handler. handler(str): Path to the handler function for the handoff interaction. Use dot-notation (e.g.: "foo.bar.function"). internal(bool, optional): Specifies that the handler is only for internal (i.e. within the same Tethys server) purposes. - """ + """ # noqa: E501 def __init__(self, name, handler, internal=False): """ @@ -209,7 +212,7 @@ def __repr__(self): """ return ''.format(self.name, self.handler) - def __json__(self): + def __dict__(self): """ JSON representation """ @@ -233,4 +236,4 @@ def json_arguments(self): if 'request' in args: index = args.index('request') args.pop(index) - return args \ No newline at end of file + return args diff --git a/tethys_apps/base/mixins.py b/tethys_apps/base/mixins.py new file mode 100644 index 000000000..2790a9287 --- /dev/null +++ b/tethys_apps/base/mixins.py @@ -0,0 +1,18 @@ +class TethysBaseMixin(object): + """ + Provides methods and properties common to the TethysBase and model classes. + """ + root_url = '' + + @property + def namespace(self): + """ + Get the namespace for the app or extension. + """ + if not hasattr(self, '_namespace'): + self._namespace = None + + if self._namespace is None: + self._namespace = self.root_url.replace('-', '_') + + return self._namespace diff --git a/tethys_apps/base/permissions.py b/tethys_apps/base/permissions.py index d9b2373e9..bf9925888 100644 --- a/tethys_apps/base/permissions.py +++ b/tethys_apps/base/permissions.py @@ -4,7 +4,7 @@ * Author: nswain * Created On: May 09, 2016 * Copyright: (c) Aquaveo 2016 -* License: +* License: ******************************************************************************** """ @@ -40,7 +40,7 @@ def __init__(self, name, description): def _repr(self): return ''.format(self.name, self.description) - def __unicode__(self): + def __str__(self): return self._repr() def __repr__(self): @@ -88,7 +88,7 @@ def __init__(self, name, permissions=[]): def _repr(self): return ''.format(self.name) - def __unicode__(self): + def __str__(self): return self._repr() def __repr__(self): @@ -120,7 +120,7 @@ def my_controller(request): if can_create_projects: ... - """ + """ # noqa: E501 from tethys_apps.utilities import get_active_app app = get_active_app(request) diff --git a/tethys_apps/base/testing/environment.py b/tethys_apps/base/testing/environment.py index 5ed8c5464..790975a09 100644 --- a/tethys_apps/base/testing/environment.py +++ b/tethys_apps/base/testing/environment.py @@ -1,8 +1,6 @@ from os import environ, unsetenv - -def is_testing_environment(): - return environ.get('TETHYS_TESTING_IN_PROGRESS') +TESTING_DB_FLAG = 'tethys-testing_' def set_testing_environment(val): @@ -11,4 +9,17 @@ def set_testing_environment(val): else: environ['TETHYS_TESTING_IN_PROGRESS'] = '' del environ['TETHYS_TESTING_IN_PROGRESS'] - unsetenv('TETHYS_TESTING_IN_PROGRESS') \ No newline at end of file + unsetenv('TETHYS_TESTING_IN_PROGRESS') + + +def get_test_db_name(orig_name): + if TESTING_DB_FLAG not in orig_name: + test_db_name = '{0}{1}'.format(TESTING_DB_FLAG, orig_name) + else: + test_db_name = orig_name + + return test_db_name + + +def is_testing_environment(): + return environ.get('TETHYS_TESTING_IN_PROGRESS') diff --git a/tethys_apps/base/testing/testing.py b/tethys_apps/base/testing/testing.py index 342a6a60c..7a75a67db 100644 --- a/tethys_apps/base/testing/testing.py +++ b/tethys_apps/base/testing/testing.py @@ -1,8 +1,10 @@ +import logging + from django.test import Client from django.test import TestCase from tethys_apps.base.app_base import TethysAppBase -from tethys_apps.base.testing.environment import set_testing_environment +from tethys_apps.base.testing.environment import is_testing_environment, get_test_db_name class TethysTestCase(TestCase): @@ -14,13 +16,15 @@ class TethysTestCase(TestCase): def setUp(self): # Resets the apps database and app permissions (workaround since Django's testing framework refreshes the # core db after each individual test) - from tethys_apps.utilities import sync_tethys_app_db, register_app_permissions - sync_tethys_app_db() - register_app_permissions() + from tethys_apps.harvester import SingletonHarvester + harvester = SingletonHarvester() + harvester.harvest() + logging.disable(logging.CRITICAL) self.set_up() def tearDown(self): self.tear_down() + logging.disable(logging.NOTSET) def set_up(self): """ @@ -54,38 +58,27 @@ def create_test_persistent_stores_for_app(app_class): Return: None """ - set_testing_environment(True) + from tethys_apps.models import TethysApp + + if not is_testing_environment(): + raise EnvironmentError('This function will only execute properly if executed in the testing environment.') if not issubclass(app_class, TethysAppBase): raise TypeError('The app_class argument was not of the correct type. ' 'It must be a class that inherits from .') - for store in app_class().list_persistent_store_databases(static_only=True): - if app_class.persistent_store_exists(store.name): - app_class.drop_persistent_store(store.name) - - create_store_success = app_class.create_persistent_store(store.name, spatial=store.spatial) - - error = False - if create_store_success: - retry_counter = 0 - while True: - if retry_counter < 5: - try: - store.initializer_function(True) - break - except Exception as e: - if 'terminating connection due to administrator command' in str(e): - pass - else: - error = True - else: - error = True - break - else: - error = True - - if error: + db_app = TethysApp.objects.get(package=app_class.package) + + ps_database_settings = db_app.persistent_store_database_settings + for db_setting in ps_database_settings: + create_store_success = app_class.create_persistent_store(db_name=db_setting.name, + connection_name=None, + spatial=db_setting.spatial, + initializer=db_setting.initializer, + refresh=True, + force_first_time=True) + + if not create_store_success: raise SystemError('The test store was not able to be created') @staticmethod @@ -98,15 +91,16 @@ def destroy_test_persistent_stores_for_app(app_class): Return: None """ - set_testing_environment(True) + if not is_testing_environment(): + raise EnvironmentError('This function will only execute properly if executed in the testing environment.') if not issubclass(app_class, TethysAppBase): raise TypeError('The app_class argument was not of the correct type. ' 'It must be a class that inherits from .') - for store in app_class().list_persistent_store_databases(static_only=True): - test_store_name = 'tethys-testing_{0}'.format(store.name) - app_class.drop_persistent_store(test_store_name) + for db_name in app_class.list_persistent_store_databases(static_only=True): + test_db_name = get_test_db_name(db_name) + app_class.drop_persistent_store(test_db_name) @staticmethod def create_test_user(username, password, email=None): @@ -147,5 +141,3 @@ def get_test_client(): Client object """ return Client() - - diff --git a/tethys_apps/base/url_map.py b/tethys_apps/base/url_map.py index 7ca3a08bc..6d6cdd18b 100644 --- a/tethys_apps/base/url_map.py +++ b/tethys_apps/base/url_map.py @@ -28,14 +28,15 @@ def __init__(self, name, url, controller, regex=None): url (str): Url pattern to map to the controller. controller (str): Dot-notation path to the controller. regex (str or iterable, optional): Custom regex pattern(s) for url variables. If a string is provided, it will be applied to all variables. If a list or tuple is provided, they will be applied in variable order. - """ + """ # noqa: E501 # Validate - if regex and (not isinstance(regex, basestring) and not isinstance(regex, tuple) and not isinstance(regex, list)): + if regex and (not isinstance(regex, basestring) and not isinstance(regex, tuple) + and not isinstance(regex, list)): raise ValueError('Value for "regex" must be either a string, list, or tuple.') self.name = name self.url = django_url_preprocessor(url, self.root_url, regex) - self.controller = '.'.join(['tethys_apps.tethysapp', controller]) + self.controller = controller self.custom_match_regex = regex def __repr__(self): @@ -60,7 +61,7 @@ def django_url_preprocessor(url, root_url, custom_regex=None): e.g.: '/example/resource/{variable_name}/' - r'^/example/resource/?P[1-9A-Za-z\-]+/$' + r'^/example/resource/(?P[0-9A-Za-z\-]+)//$' """ # Split the url into parts @@ -87,13 +88,9 @@ def django_url_preprocessor(url, root_url, custom_regex=None): elif (isinstance(custom_regex, list) or isinstance(custom_regex, tuple)) and len(custom_regex) > 0: try: expression = custom_regex[url_variable_count] - except IndexError: expression = custom_regex[0] - except: - raise - else: expression = DEFAULT_EXPRESSION diff --git a/tethys_apps/base/workspace.py b/tethys_apps/base/workspace.py index adbb6ec14..de8f3a579 100644 --- a/tethys_apps/base/workspace.py +++ b/tethys_apps/base/workspace.py @@ -70,7 +70,8 @@ def files(self, full_path=False): """ if full_path: - files = [os.path.join(self._path, f) for f in os.listdir(self._path) if os.path.isfile(os.path.join(self._path, f))] + files = [os.path.join(self._path, f) for f in os.listdir(self._path) if + os.path.isfile(os.path.join(self._path, f))] else: files = [f for f in os.listdir(self._path) if os.path.isfile(os.path.join(self._path, f))] return files @@ -97,7 +98,8 @@ def directories(self, full_path=False): """ if full_path: - directories = [os.path.join(self._path, d) for d in os.listdir(self._path) if os.path.isdir(os.path.join(self._path, d))] + directories = [os.path.join(self._path, d) for d in os.listdir(self._path) if + os.path.isdir(os.path.join(self._path, d))] else: directories = [d for d in os.listdir(self._path) if os.path.isdir(os.path.join(self._path, d))] return directories @@ -163,11 +165,10 @@ def remove(self, item): **Note:** Though you can specify relative paths, the ``remove()`` method will not allow you to back into other directories using "../" or similar notation. Futhermore, absolute paths given must contain the path of the workspace to be valid. - """ + """ # noqa: E501 # Sanitize to prevent backing into other directories or entering the home directory - full_path = item.replace('../', '').replace('./', '').\ - replace('..\\', '').replace('.\\', '').\ - replace('~/', '').replace('~\\', '') + full_path = item.replace('../', '').replace('./', '').replace('..\\', '').\ + replace('.\\', '').replace('~/', '').replace('~\\', '') if self._path not in full_path: full_path = os.path.join(self._path, full_path) @@ -175,4 +176,4 @@ def remove(self, item): if os.path.isdir(full_path): shutil.rmtree(full_path) elif os.path.isfile(full_path): - os.remove(full_path) \ No newline at end of file + os.remove(full_path) diff --git a/tethys_apps/cli/__init__.py b/tethys_apps/cli/__init__.py index ae5a9bbe6..ce4e7c990 100644 --- a/tethys_apps/cli/__init__.py +++ b/tethys_apps/cli/__init__.py @@ -9,177 +9,31 @@ """ # Commandline interface for Tethys import argparse -import os -import subprocess -import webbrowser - -from builtins import input +from tethys_apps.cli.docker_commands import docker_command +from tethys_apps.cli.list_command import list_command as lc from tethys_apps.cli.scaffold_commands import scaffold_command -from tethys_apps.terminal_colors import TerminalColors -from .docker_commands import * -from .gen_commands import GEN_SETTINGS_OPTION, GEN_APACHE_OPTION, generate_command -from .manage_commands import (manage_command, get_manage_path, run_process, - MANAGE_START, MANAGE_SYNCDB, - MANAGE_COLLECTSTATIC, MANAGE_COLLECTWORKSPACES, - MANAGE_COLLECT, MANAGE_CREATESUPERUSER, TETHYS_SRC_DIRECTORY) -from .gen_commands import VALID_GEN_OBJECTS, generate_command -from tethys_apps.helpers import get_installed_tethys_apps +from tethys_apps.cli.syncstores_command import syncstores_command as syc +from tethys_apps.cli.test_command import test_command as tstc +from tethys_apps.cli.uninstall_command import uninstall_command as uc +from tethys_apps.cli.docker_commands import N52WPS_INPUT, GEOSERVER_INPUT, POSTGIS_INPUT +from tethys_apps.cli.manage_commands import (manage_command, MANAGE_START, MANAGE_SYNCDB, + MANAGE_COLLECTSTATIC, MANAGE_COLLECTWORKSPACES, MANAGE_SYNC, + MANAGE_COLLECT, MANAGE_CREATESUPERUSER) +from tethys_apps.cli.services_commands import (services_create_persistent_command, services_create_spatial_command, + services_list_command, services_remove_persistent_command, + services_remove_spatial_command) +from tethys_apps.cli.link_commands import link_command +from tethys_apps.cli.app_settings_commands import (app_settings_list_command, app_settings_create_ps_database_command, + app_settings_remove_command) +from tethys_apps.cli.scheduler_commands import (scheduler_create_command, schedulers_list_command, + schedulers_remove_command) +from tethys_apps.cli.gen_commands import VALID_GEN_OBJECTS, generate_command # Module level variables PREFIX = 'tethysapp' -def uninstall_command(args): - """ - Uninstall an app command. - """ - # Get the path to manage.py - manage_path = get_manage_path(args) - app_name = args.app - process = ['python', manage_path, 'tethys_app_uninstall', app_name] - try: - subprocess.call(process) - except KeyboardInterrupt: - pass - - -def list_apps_command(args): - """ - List installed apps. - """ - installed_apps = get_installed_tethys_apps() - - for app in installed_apps: - print(app) - - -def docker_command(args): - """ - Docker management commands. - """ - if args.command == 'init': - docker_init(containers=args.containers, defaults=args.defaults) - - elif args.command == 'start': - docker_start(containers=args.containers) - - elif args.command == 'stop': - docker_stop(containers=args.containers, boot2docker=args.boot2docker) - - elif args.command == 'status': - docker_status() - - elif args.command == 'update': - docker_update(containers=args.containers, defaults=args.defaults) - - elif args.command == 'remove': - docker_remove(containers=args.containers) - - elif args.command == 'ip': - docker_ip() - - elif args.command == 'restart': - docker_restart(containers=args.containers) - - -def syncstores_command(args): - """ - Sync persistent stores. - """ - # Get the path to manage.py - manage_path = get_manage_path(args) - - # This command is a wrapper for a custom Django manage.py method called syncstores. - # See tethys_apps.mangement.commands.syncstores - process = ['python', manage_path, 'syncstores'] - - if args.refresh: - valid_inputs = ('y', 'n', 'yes', 'no') - no_inputs = ('n', 'no') - proceed = input('{1}WARNING:{2} You have specified the database refresh option. This will drop all of the ' - 'databases for the following apps: {0}. This could result in significant data loss and ' - 'cannot be undone. Do you wish to continue? (y/n): '.format(', '.join(args.app), - TerminalColors.WARNING, - TerminalColors.ENDC)).lower() - - while proceed not in valid_inputs: - proceed = input('Invalid option. Do you wish to continue? (y/n): ').lower() - - if proceed not in no_inputs: - process.extend(['-r']) - else: - print('Operation cancelled by user.') - exit(0) - - if args.firsttime: - process.extend(['-f']) - - if args.database: - process.extend(['-d', args.database]) - - if args.app: - process.extend(args.app) - - try: - subprocess.call(process) - except KeyboardInterrupt: - pass - - -def test_command(args): - args.manage = False - # Get the path to manage.py - manage_path = get_manage_path(args) - tests_path = os.path.join(TETHYS_SRC_DIRECTORY, 'tests') - - # Define the process to be run - primary_process = ['python', manage_path, 'test'] - - # Tag to later check if tests are being run on a specific app - app_package_tag = 'tethys_apps.tethysapp.' - - if args.coverage or args.coverage_html: - os.environ['TETHYS_TEST_DIR'] = tests_path - if args.file and app_package_tag in args.file: - app_package_parts = args.file.split(app_package_tag) - app_package = app_package_tag + app_package_parts[1].split('.')[0] - config_opt = '--source={0}'.format(app_package) - else: - config_opt = '--rcfile={0}'.format(os.path.join(tests_path, 'coverage.cfg')) - primary_process = ['coverage', 'run', config_opt, manage_path, 'test'] - - if args.file: - primary_process.append(args.file) - elif args.unit: - primary_process.append(os.path.join(tests_path, 'unit_tests')) - elif args.gui: - primary_process.append(os.path.join(tests_path, 'gui_tests')) - - # print(primary_process) - run_process(primary_process) - if args.coverage: - if args.file and app_package_tag in args.file: - run_process(['coverage', 'report']) - else: - run_process(['coverage', 'report', config_opt]) - if args.coverage_html: - report_dirname = 'coverage_html_report' - index_fname = 'index.html' - - if args.file and app_package_tag in args.file: - run_process(['coverage', 'html', '--directory={0}'.format(os.path.join(tests_path, report_dirname))]) - else: - run_process(['coverage', 'html', config_opt]) - - try: - status = run_process(['open', os.path.join(tests_path, report_dirname, index_fname)]) - if status != 0: - raise Exception - except: - webbrowser.open_new_tab(os.path.join(tests_path, report_dirname, index_fname)) - - def tethys_command(): """ Tethys commandline interface function. @@ -192,13 +46,13 @@ def tethys_command(): scaffold_parser = subparsers.add_parser('scaffold', help='Create a new Tethys app project from a scaffold.') scaffold_parser.add_argument('name', help='The name of the new Tethys app project to create. Only lowercase ' 'letters, numbers, and underscores allowed.') - scaffold_parser.add_argument('-t', '--template', dest='template', help="Name of app template to use.") - scaffold_parser.add_argument('-e', '--extension', dest='extension', help="Name of extension template to use.") + scaffold_parser.add_argument('-t', '--template', dest='template', help="Name of template to use.") + scaffold_parser.add_argument('-e', '--extension', dest='extension', action="store_true") scaffold_parser.add_argument('-d', '--defaults', dest='use_defaults', action='store_true', help="Run command, accepting default values automatically.") scaffold_parser.add_argument('-o', '--overwrite', dest='overwrite', action="store_true", help="Attempt to overwrite project automatically if it already exists.") - scaffold_parser.set_defaults(func=scaffold_command, template='default', extension=None) + scaffold_parser.set_defaults(func=scaffold_command, template='default', extension=False) # Setup generate command gen_parser = subparsers.add_parser('gen', help='Aids the installation of Tethys by automating the ' @@ -206,7 +60,15 @@ def tethys_command(): gen_parser.add_argument('type', help='The type of object to generate.', choices=VALID_GEN_OBJECTS) gen_parser.add_argument('-d', '--directory', help='Destination directory for the generated object.') gen_parser.add_argument('--allowed-host', dest='allowed_host', - help='Hostname or IP address to add to allowed hosts in the settings file.') + help='Single hostname or IP address to add to allowed hosts in the settings file. ' + 'e.g.: 127.0.0.1') + gen_parser.add_argument('--allowed-hosts', dest='allowed_hosts', + help='A list of hostnames or IP addresses to add to allowed hosts in the settings file. ' + 'e.g.: "[\'127.0.0.1\', \'localhost\']"') + gen_parser.add_argument('--client-max-body-size', dest='client_max_body_size', + help='Populates the client_max_body_size parameter for nginx config. Defaults to "75M".') + gen_parser.add_argument('--uwsgi-processes', dest='uwsgi_processes', + help='The maximum number of uwsgi worker processes. Defaults to 10.') gen_parser.add_argument('--db-username', dest='db_username', help='Username for the Tethys Database server to be set in the settings file.') gen_parser.add_argument('--db-password', dest='db_password', @@ -217,18 +79,170 @@ def tethys_command(): help='Generate a new settings file for a production server.') gen_parser.add_argument('--overwrite', dest='overwrite', action='store_true', help='Overwrite existing file without prompting.') - gen_parser.set_defaults(func=generate_command, allowed_host=None, db_username='tethys_default', - db_password='pass', db_port=5436, production=False, overwrite=False) + gen_parser.set_defaults(func=generate_command, allowed_host=None, allowed_hosts=None, client_max_body_size='75M', + uwsgi_processes=10, db_username='tethys_default', db_password='pass', db_port=5436, + production=False, overwrite=False) # Setup start server command manage_parser = subparsers.add_parser('manage', help='Management commands for Tethys Platform.') manage_parser.add_argument('command', help='Management command to run.', - choices=[MANAGE_START, MANAGE_SYNCDB, MANAGE_COLLECTSTATIC, MANAGE_COLLECTWORKSPACES, MANAGE_COLLECT, MANAGE_CREATESUPERUSER]) + choices=[MANAGE_START, MANAGE_SYNCDB, MANAGE_COLLECTSTATIC, MANAGE_COLLECTWORKSPACES, + MANAGE_COLLECT, MANAGE_CREATESUPERUSER, MANAGE_SYNC]) manage_parser.add_argument('-m', '--manage', help='Absolute path to manage.py for Tethys Platform installation.') - manage_parser.add_argument('-p', '--port', type=str, help='Host and/or port on which to bind the development server.') - manage_parser.add_argument('--noinput', action='store_true', help='Pass the --noinput argument to the manage.py command.') + manage_parser.add_argument('-p', '--port', type=str, + help='Host and/or port on which to bind the development server.') + manage_parser.add_argument('--noinput', action='store_true', + help='Pass the --noinput argument to the manage.py command.') + manage_parser.add_argument('-f', '--force', required=False, action='store_true', + help='Used only with {} to force the overwrite the app directory into its collect-to ' + 'location.') manage_parser.set_defaults(func=manage_command) + # SCHEDULERS COMMANDS + scheduler_parser = subparsers.add_parser('schedulers', help='Scheduler commands for Tethys Platform.') + scheduler_subparsers = scheduler_parser.add_subparsers(title='Commands') + + # tethys schedulers create + schedulers_create = scheduler_subparsers.add_parser('create', help='Create a Scheduler that can be ' + 'accessed by Tethys Apps.') + schedulers_create.add_argument('-n', '--name', required=True, help='A unique name for the Scheduler', type=str) + schedulers_create.add_argument('-e', '--endpoint', required=True, type=str, + help='The endpoint of the service in the form //"') + schedulers_create.add_argument('-u', '--username', required=True, help='The username to connect to the host with', + type=str) + group = schedulers_create.add_mutually_exclusive_group(required=True) + group.add_argument('-p', '--password', required=False, type=str, + help='The password associated with the provided username') + group.add_argument('-f', '--private-key-path', required=False, help='The path to the private ssh key file', + type=str) + schedulers_create.add_argument('-k', '--private-key-pass', required=False, type=str, + help='The password to the private ssh key file') + schedulers_create.set_defaults(func=scheduler_create_command) + + # tethys schedulers list + schedulers_list = scheduler_subparsers.add_parser('list', help='List the existing Schedulers.') + schedulers_list.set_defaults(func=schedulers_list_command) + + # tethys schedulers remove + schedulers_remove = scheduler_subparsers.add_parser('remove', help='Remove a Scheduler.') + schedulers_remove.add_argument('scheduler_name', help='The unique name of the Scheduler that you are removing.') + schedulers_remove.add_argument('-f', '--force', action='store_true', help='Force removal without confirming.') + schedulers_remove.set_defaults(func=schedulers_remove_command) + + # SERVICES COMMANDS + services_parser = subparsers.add_parser('services', help='Services commands for Tethys Platform.') + services_subparsers = services_parser.add_subparsers(title='Commands') + + # tethys services remove + services_remove_parser = services_subparsers.add_parser('remove', help='Remove a Tethys Service.') + services_remove_subparsers = services_remove_parser.add_subparsers(title='Service Type') + + # tethys services remove persistent + services_remove_persistent = services_remove_subparsers.add_parser('persistent', + help='Remove a Persistent Store Service.') + services_remove_persistent.add_argument('service_uid', help='The ID or name of the Persistent Store Service ' + 'that you are removing.') + services_remove_persistent.add_argument('-f', '--force', action='store_true', + help='Force removal without confirming.') + services_remove_persistent.set_defaults(func=services_remove_persistent_command) + + # tethys services remove spatial + services_remove_spatial = services_remove_subparsers.add_parser('spatial', + help='Remove a Spatial Dataset Service.') + services_remove_spatial.add_argument('service_uid', help='The ID or name of the Spatial Dataset Service ' + 'that you are removing.') + services_remove_spatial.add_argument('-f', '--force', action='store_true', help='Force removal without confirming.') + services_remove_spatial.set_defaults(func=services_remove_spatial_command) + + # tethys services create + services_create_parser = services_subparsers.add_parser('create', help='Create a Tethys Service.') + services_create_subparsers = services_create_parser.add_subparsers(title='Service Type') + + # tethys services create persistent + services_create_ps = services_create_subparsers.add_parser('persistent', + help='Create a Persistent Store Service.') + services_create_ps.add_argument('-n', '--name', required=True, help='A unique name for the Service', type=str) + services_create_ps.add_argument('-c', '--connection', required=True, type=str, + help='The connection of the Service in the form ' + '":@:"') + services_create_ps.set_defaults(func=services_create_persistent_command) + + # tethys services create spatial + services_create_sd = services_create_subparsers.add_parser('spatial', + help='Create a Spatial Dataset Service.') + services_create_sd.add_argument('-n', '--name', required=True, help='A unique name for the Service', type=str) + services_create_sd.add_argument('-c', '--connection', required=True, type=str, + help='The connection of the Service in the form ' + '":@//:"') + services_create_sd.add_argument('-p', '--public-endpoint', required=False, type=str, + help='The public-facing endpoint, if different than what was provided with the ' + '--connection argument, of the form ":"') + services_create_sd.add_argument('-k', '--apikey', required=False, type=str, + help='The API key, if any, required to establish a connection.') + services_create_sd.set_defaults(func=services_create_spatial_command) + + # tethys services list + services_list_parser = services_subparsers.add_parser('list', help='List all existing Tethys Services.') + group = services_list_parser.add_mutually_exclusive_group() + group.add_argument('-p', '--persistent', action='store_true', help='Only list Persistent Store Services.') + group.add_argument('-s', '--spatial', action='store_true', help='Only list Spatial Dataset Services.') + services_list_parser.set_defaults(func=services_list_command) + + # APP_SETTINGS COMMANDS + app_settings_parser = subparsers.add_parser('app_settings', help='Interact with Tethys App Settings.') + app_settings_subparsers = app_settings_parser.add_subparsers(title='Options') + + # tethys app_settings list + app_settings_list_parser = app_settings_subparsers.add_parser('list', help='List all settings for a specified app') + app_settings_list_parser.add_argument('app', help='The app ("") to list the Settings for.') + app_settings_list_parser.set_defaults(func=app_settings_list_command) + + # tethys app_settings create + app_settings_create_cmd = app_settings_subparsers.add_parser('create', help='Create a Setting for an app.') + + asc_subparsers = app_settings_create_cmd.add_subparsers(title='Create Options') + app_settings_create_cmd.add_argument('-a', '--app', required=True, + help='The app ("") to create the Setting for.') + app_settings_create_cmd.add_argument('-n', '--name', required=True, help='The name of the Setting to create.') + app_settings_create_cmd.add_argument('-d', '--description', required=False, + help='A description for the Setting to create.') + app_settings_create_cmd.add_argument('-r', '--required', required=False, action='store_true', + help='Include this flag if the Setting is required for the app.') + app_settings_create_cmd.add_argument('-i', '--initializer', required=False, + help='The function that initializes the PersistentStoreSetting database.') + app_settings_create_cmd.add_argument('-z', '--initialized', required=False, action='store_true', + help='Include this flag if the database is already initialized.') + + # tethys app_settings create ps_database + app_settings_create_psdb_cmd = asc_subparsers.add_parser('ps_database', + help='Create a PersistentStoreDatabaseSetting') + app_settings_create_psdb_cmd.add_argument('-s', '--spatial', required=False, action='store_true', + help='Include this flag if the database requires spatial capabilities.') + app_settings_create_psdb_cmd.add_argument('-y', '--dynamic', action='store_true', required=False, + help='Include this flag if the database should be considered to be ' + 'dynamically created.') + app_settings_create_psdb_cmd.set_defaults(func=app_settings_create_ps_database_command) + + # tethys app_settings remove + app_settings_remove_cmd = app_settings_subparsers.add_parser('remove', help='Remove a Setting for an app.') + app_settings_remove_cmd.add_argument('app', help='The app ("") to remove the Setting from.') + app_settings_remove_cmd.add_argument('-n', '--name', help='The name of the Setting to remove.', required=True) + app_settings_remove_cmd.add_argument('-f', '--force', action='store_true', help='Force removal without confirming.') + app_settings_remove_cmd.set_defaults(func=app_settings_remove_command) + + # LINK COMMANDS + link_parser = subparsers.add_parser('link', help='Link a Service to a Tethys app Setting.') + + # tethys link + link_parser.add_argument('service', help='Service to link to a target app. Of the form ' + '":" ' + '(i.e. "persistent_connection:super_conn")') + link_parser.add_argument('setting', help='Setting of an app with which to link the specified service.' + 'Of the form ":' + ':' + '" (i.e. "epanet:database:epanet_2")') + link_parser.set_defaults(func=link_command) + # Setup test command test_parser = subparsers.add_parser('test', help='Testing commands for Tethys Platform.') test_parser.add_argument('-c', '--coverage', help='Run coverage with tests and output report to console.', @@ -240,16 +254,18 @@ def tethys_command(): 'If both flags are set then -u takes precedence.', action='store_true') test_parser.add_argument('-f', '--file', type=str, help='File to run tests in. Overrides -g and -u.') - test_parser.set_defaults(func=test_command) + test_parser.set_defaults(func=tstc) # Setup uninstall command uninstall_parser = subparsers.add_parser('uninstall', help='Uninstall an app.') - uninstall_parser.add_argument('app', help='Name of the app to uninstall.') - uninstall_parser.set_defaults(func=uninstall_command) + uninstall_parser.add_argument('app_or_extension', help='Name of the app or extension to uninstall.') + uninstall_parser.add_argument('-e', '--extension', dest='is_extension', default=False, action='store_true', + help='Flag to denote an extension is being uninstalled') + uninstall_parser.set_defaults(func=uc) # Setup list command - list_parser = subparsers.add_parser('list', help='List installed apps.') - list_parser.set_defaults(func=list_apps_command) + list_parser = subparsers.add_parser('list', help='List installed apps and extensions.') + list_parser.set_defaults(func=lc) # Sync stores command syncstores_parser = subparsers.add_parser('syncstores', help='Management command for App Persistent Stores.') @@ -267,8 +283,9 @@ def tethys_command(): action='store_true', dest='firsttime') syncstores_parser.add_argument('-d', '--database', help='Name of database to sync.') - syncstores_parser.add_argument('-m', '--manage', help='Absolute path to manage.py for Tethys Platform installation.') - syncstores_parser.set_defaults(func=syncstores_command, refresh=False, firstime=False) + syncstores_parser.add_argument('-m', '--manage', help='Absolute path to manage.py for ' + 'Tethys Platform installation.') + syncstores_parser.set_defaults(func=syc, refresh=False, firstime=False) # Setup the docker commands docker_parser = subparsers.add_parser('docker', help="Management commands for the Tethys Docker containers.") @@ -291,4 +308,8 @@ def tethys_command(): # Parse the args and call the default function args = parser.parse_args() - args.func(args) \ No newline at end of file + try: + args.func(args) + except AttributeError: + parser.print_help() + exit(2) diff --git a/tethys_apps/cli/app_settings_commands.py b/tethys_apps/cli/app_settings_commands.py new file mode 100644 index 000000000..82f0c5de8 --- /dev/null +++ b/tethys_apps/cli/app_settings_commands.py @@ -0,0 +1,111 @@ +from django.core.exceptions import ObjectDoesNotExist + +from tethys_apps.cli.cli_colors import pretty_output, BOLD, FG_RED + + +def app_settings_list_command(args): + from tethys_apps.models import (TethysApp, PersistentStoreConnectionSetting, PersistentStoreDatabaseSetting, + SpatialDatasetServiceSetting) + + setting_type_dict = { + PersistentStoreConnectionSetting: 'ps_connection', + PersistentStoreDatabaseSetting: 'ps_database', + SpatialDatasetServiceSetting: 'ds_spatial' + } + + app_package = args.app + try: + app = TethysApp.objects.get(package=app_package) + + app_settings = [] + for setting in PersistentStoreConnectionSetting.objects.filter(tethys_app=app): + app_settings.append(setting) + for setting in PersistentStoreDatabaseSetting.objects.filter(tethys_app=app): + app_settings.append(setting) + for setting in SpatialDatasetServiceSetting.objects.filter(tethys_app=app): + app_settings.append(setting) + + unlinked_settings = [] + linked_settings = [] + + for setting in app_settings: + if hasattr(setting, 'spatial_dataset_service') and setting.spatial_dataset_service \ + or hasattr(setting, 'persistent_store_service') and setting.persistent_store_service: + linked_settings.append(setting) + else: + unlinked_settings.append(setting) + + with pretty_output(BOLD) as p: + p.write("\nUnlinked Settings:") + + if len(unlinked_settings) == 0: + with pretty_output() as p: + p.write('None') + else: + is_first_row = True + for setting in unlinked_settings: + if is_first_row: + with pretty_output(BOLD) as p: + p.write('{0: <10}{1: <40}{2: <15}'.format('ID', 'Name', 'Type')) + is_first_row = False + with pretty_output() as p: + p.write('{0: <10}{1: <40}{2: <15}'.format(setting.pk, setting.name, + setting_type_dict[type(setting)])) + + with pretty_output(BOLD) as p: + p.write("\nLinked Settings:") + + if len(linked_settings) == 0: + with pretty_output() as p: + p.write('None') + else: + is_first_row = True + for setting in linked_settings: + if is_first_row: + with pretty_output(BOLD) as p: + p.write('{0: <10}{1: <40}{2: <15}{3: <20}'.format('ID', 'Name', 'Type', 'Linked With')) + is_first_row = False + service_name = setting.spatial_dataset_service.name if hasattr(setting, 'spatial_dataset_service') \ + else setting.persistent_store_service.name + print('{0: <10}{1: <40}{2: <15}{3: <20}'.format(setting.pk, setting.name, + setting_type_dict[type(setting)], service_name)) + except ObjectDoesNotExist: + with pretty_output(FG_RED) as p: + p.write('The app you specified ("{0}") does not exist. Command aborted.'.format(app_package)) + except Exception as e: + with pretty_output(FG_RED) as p: + p.write(e) + p.write('Something went wrong. Please try again.') + + +def app_settings_create_ps_database_command(args): + from tethys_apps.utilities import create_ps_database_setting + app_package = args.app + setting_name = args.name + setting_description = args.description + required = args.required + initializer = args.initializer + initialized = args.initialized + spatial = args.spatial + dynamic = args.dynamic + + success = create_ps_database_setting(app_package, setting_name, setting_description or '', + required, initializer or '', initialized, spatial, dynamic) + + if not success: + exit(1) + + exit(0) + + +def app_settings_remove_command(args): + from tethys_apps.utilities import remove_ps_database_setting + app_package = args.app + setting_name = args.name + force = args.force + success = remove_ps_database_setting(app_package, setting_name, force) + + if not success: + exit(1) + + exit(0) diff --git a/tethys_apps/cli/cli_colors.py b/tethys_apps/cli/cli_colors.py new file mode 100644 index 000000000..9acbe5978 --- /dev/null +++ b/tethys_apps/cli/cli_colors.py @@ -0,0 +1,87 @@ +# encoding: utf-8 + +# Copyright 2013 Diego Navarro Mellén. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other materials +# provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY DIEGO NAVARRO MELLÉN ''AS IS'' AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIEGO NAVARRO MELLÉN OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those of the +# authors and should not be interpreted as representing official policies, either expressed +# or implied, of Diego Navarro Mellén. + +from __future__ import print_function + + +# Special END separator +END = '0e8ed89a-47ba-4cdb-938e-b8af8e084d5c' + +# Text attributes +ALL_OFF = '\033[0m' +BOLD = '\033[1m' +UNDERSCORE = '\033[4m' +BLINK = '\033[5m' +REVERSE = '\033[7m' +CONCEALED = '\033[7m' + +# Foreground colors +FG_BLACK = '\033[30m' +FG_RED = '\033[31m' +FG_GREEN = '\033[32m' +FG_YELLOW = '\033[33m' +FG_BLUE = '\033[34m' +FG_MAGENTA = '\033[35m' +FG_CYAN = '\033[36m' +FG_WHITE = '\033[37m' + +# Background colors +BG_BLACK = '\033[40m' +BG_RED = '\033[41m' +BG_GREEN = '\033[42m' +BG_YELLOW = '\033[43m' +BG_BLUE = '\033[44m' +BG_MAGENTA = '\033[45m' +BG_CYAN = '\033[46m' +BG_WHITE = '\033[47m' + +# TerminalColors colors +TC_BLUE = '\033[94m' +TC_GREEN = '\033[92m' +TC_WARNING = '\033[93m' +TC_FAIL = '\033[91m' +TC_ENDC = '\033[0m' + + +class pretty_output: + ''' + Context manager for pretty terminal prints + ''' + + def __init__(self, *attr): + self.attributes = attr + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + pass + + def write(self, msg): + style = ''.join(self.attributes) + print('{}{}{}'.format(style, msg.replace(END, ALL_OFF + style), ALL_OFF)) diff --git a/tethys_apps/cli/cli_helpers.py b/tethys_apps/cli/cli_helpers.py new file mode 100644 index 000000000..693e68778 --- /dev/null +++ b/tethys_apps/cli/cli_helpers.py @@ -0,0 +1,11 @@ + + +def add_geoserver_rest_to_endpoint(endpoint): + parts = endpoint.split('//') + protocol = parts[0] + parts2 = parts[1].split(':') + host = parts2[0] + port_and_path = parts2[1] + port = port_and_path.split('/')[0] + + return '{0}//{1}:{2}/geoserver/rest/'.format(protocol, host, port) diff --git a/tethys_apps/cli/docker_commands.py b/tethys_apps/cli/docker_commands.py index 345b57793..18821cbb5 100644 --- a/tethys_apps/cli/docker_commands.py +++ b/tethys_apps/cli/docker_commands.py @@ -9,7 +9,7 @@ """ try: import curses -except: +except Exception: pass # curses not available on Windows from builtins import input import platform @@ -22,6 +22,7 @@ from docker.utils import kwargs_from_env, compare_version, create_host_config from docker.client import Client as DockerClient from docker.constants import DEFAULT_DOCKER_API_VERSION as MAX_CLIENT_DOCKER_API_VERSION +from tethys_apps.cli.cli_colors import pretty_output, FG_WHITE __all__ = ['docker_init', 'docker_start', @@ -112,7 +113,7 @@ def validate_numeric_cli_input(value, default=None, max=None): continue if max is not None: - if float(value) > max: + if float(value) > float(max): if default is not None: value = input('Maximum allowed value is {0} [{1}]: '.format(max, default)) else: @@ -151,7 +152,8 @@ def validate_directory_cli_input(value, default=None): try: os.makedirs(value) except OSError as e: - print ('{0}: {1}'.format(repr(e), value)) + with pretty_output(FG_WHITE) as p: + p.write('{0}: {1}'.format(repr(e), value)) prompt = 'Please provide a valid directory' prompt = add_default_to_prompt(prompt, default) prompt = close_prompt(prompt) @@ -192,12 +194,14 @@ def get_docker_client(): # Start the boot2docker VM if it is not already running if boot2docker_info['State'] != "running": - print('Starting Boot2Docker VM:') + with pretty_output(FG_WHITE) as p: + p.write('Starting Boot2Docker VM:') # Start up the Docker VM process = ['boot2docker', 'start'] subprocess.call(process) - if ('DOCKER_HOST' not in os.environ) or ('DOCKER_CERT_PATH' not in os.environ) or ('DOCKER_TLS_VERIFY' not in os.environ): + if ('DOCKER_HOST' not in os.environ) or ('DOCKER_CERT_PATH' not in os.environ) or \ + ('DOCKER_TLS_VERIFY' not in os.environ): # Get environmental variable values process = ['boot2docker', 'shellinit'] p = subprocess.Popen(process, stdout=PIPE) @@ -227,7 +231,8 @@ def get_docker_client(): # Then test to see if the Docker is running a later version than the minimum # See: https://github.com/docker/docker-py/issues/439 version_client = DockerClient(**client_kwargs) - client_kwargs['version'] = get_api_version(MAX_CLIENT_DOCKER_API_VERSION, version_client.version()['ApiVersion']) + client_kwargs['version'] = get_api_version(MAX_CLIENT_DOCKER_API_VERSION, + version_client.version()['ApiVersion']) # Create Real Docker client docker_client = DockerClient(**client_kwargs) @@ -249,9 +254,6 @@ def get_docker_client(): return docker_client - except: - raise - def stop_boot2docker(): """ @@ -260,13 +262,11 @@ def stop_boot2docker(): try: process = ['boot2docker', 'stop'] subprocess.call(process) - print('Boot2Docker VM Stopped') + with pretty_output(FG_WHITE) as p: + p.write('Boot2Docker VM Stopped') except OSError: pass - except: - raise - def get_images_to_install(docker_client, containers=ALL_DOCKER_INPUTS): """ @@ -341,8 +341,12 @@ def log_pull_stream(stream): current_status = json_line['status'] if 'status' in json_line else '' current_progress = json_line['progress'] if 'progress' in json_line else '' - print("{id}{status} {progress}".format(id=current_id, status=current_status, - progress=current_progress)) + with pretty_output(FG_WHITE) as p: + p.write("{id}{status} {progress}".format( + id=current_id, + status=current_status, + progress=current_progress + )) else: TERMINAL_STATUSES = ['Already exists', 'Download complete', 'Pull complete'] @@ -385,8 +389,11 @@ def log_pull_stream(stream): 'progress': current_progress} else: # add all other messages to list to show above progress messages - message_log.append("{id}: {status} {progress}".format(id=current_id, status=current_status, - progress=current_progress)) + message_log.append("{id}: {status} {progress}".format( + id=current_id, + status=current_status, + progress=current_progress + )) # remove messages from progress that have completed if current_id in progress_messages: @@ -424,7 +431,8 @@ def log_pull_stream(stream): curses.nocbreak() curses.endwin() - print('\n'.join(messages_to_print)) + with pretty_output(FG_WHITE) as p: + p.write('\n'.join(messages_to_print)) def get_docker_container_dicts(docker_client): @@ -514,7 +522,8 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ # PostGIS if POSTGIS_CONTAINER in containers_to_create or force: - print("\nInstalling the PostGIS Docker container...") + with pretty_output(FG_WHITE) as p: + p.write("\nInstalling the PostGIS Docker container...") # Default environmental vars tethys_default_pass = 'pass' @@ -523,8 +532,9 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ # User environmental variables if not defaults: - print("Provide passwords for the three Tethys database users or press enter to accept the default " - "passwords shown in square brackets:") + with pretty_output(FG_WHITE) as p: + p.write("Provide passwords for the three Tethys database users or press enter to accept the default " + "passwords shown in square brackets:") # tethys_default tethys_default_pass_1 = getpass.getpass('Password for "tethys_default" database user [pass]: ') @@ -533,7 +543,8 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ tethys_default_pass_2 = getpass.getpass('Confirm password for "tethys_default" database user: ') while tethys_default_pass_1 != tethys_default_pass_2: - print('Passwords do not match, please try again: ') + with pretty_output(FG_WHITE) as p: + p.write('Passwords do not match, please try again: ') tethys_default_pass_1 = getpass.getpass('Password for "tethys_default" database user [pass]: ') tethys_default_pass_2 = getpass.getpass('Confirm password for "tethys_default" database user: ') @@ -548,9 +559,12 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ tethys_db_manager_pass_2 = getpass.getpass('Confirm password for "tethys_db_manager" database user: ') while tethys_db_manager_pass_1 != tethys_db_manager_pass_2: - print('Passwords do not match, please try again: ') - tethys_db_manager_pass_1 = getpass.getpass('Password for "tethys_db_manager" database user [pass]: ') - tethys_db_manager_pass_2 = getpass.getpass('Confirm password for "tethys_db_manager" database user: ') + with pretty_output(FG_WHITE) as p: + p.write('Passwords do not match, please try again: ') + tethys_db_manager_pass_1 = getpass.getpass('Password for "tethys_db_manager" database user ' + '[pass]: ') + tethys_db_manager_pass_2 = getpass.getpass('Confirm password for "tethys_db_manager" database ' + 'user: ') tethys_db_manager_pass = tethys_db_manager_pass_1 else: @@ -563,7 +577,8 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ tethys_super_pass_2 = getpass.getpass('Confirm password for "tethys_super" database user: ') while tethys_super_pass_1 != tethys_super_pass_2: - print('Passwords do not match, please try again: ') + with pretty_output(FG_WHITE) as p: + p.write('Passwords do not match, please try again: ') tethys_super_pass_1 = getpass.getpass('Password for "tethys_super" database user [pass]: ') tethys_super_pass_2 = getpass.getpass('Confirm password for "tethys_super" database user: ') @@ -581,7 +596,8 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ # GeoServer if GEOSERVER_CONTAINER in containers_to_create or force: - print("\nInstalling the GeoServer Docker container...") + with pretty_output(FG_WHITE) as p: + p.write("\nInstalling the GeoServer Docker container...") if "cluster" in GEOSERVER_IMAGE: @@ -589,8 +605,9 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ # Environmental variables from user input environment = dict() - print("The GeoServer docker can be configured to run in a clustered mode (multiple instances of " - "GeoServer running in the docker container) for better performance.\n") + with pretty_output(FG_WHITE) as p: + p.write("The GeoServer docker can be configured to run in a clustered mode (multiple instances of " + "GeoServer running in the docker container) for better performance.\n") enabled_nodes = input('Number of GeoServer Instances Enabled (max 4) [1]: ') environment['ENABLED_NODES'] = validate_numeric_cli_input(enabled_nodes, 1, 4) @@ -600,9 +617,11 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ max_rest_nodes)) environment['REST_NODES'] = validate_numeric_cli_input(rest_nodes, 1, max_rest_nodes) - print("\nGeoServer can be configured with limits to certain types of requests to prevent it from " - "becoming overwhelmed. This can be done automatically based on a number of processors or each " - "limit can be set explicitly.\n") + with pretty_output(FG_WHITE) as p: + p.write("\nGeoServer can be configured with limits to certain types of requests to prevent it from " + "becoming overwhelmed. This can be done automatically based on a number of processors or " + "each " + "limit can be set explicitly.\n") flow_control_mode = input('Would you like to specify number of Processors (c) OR set ' 'limits explicitly (e) [C/e]: ') @@ -614,7 +633,7 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ else: max_ows_global = input('Maximum number of simultaneous OGC web service requests ' - '(e.g.: WMS, WCS, WFS) [100]: ') + '(e.g.: WMS, WCS, WFS) [100]: ') environment['MAX_OWS_GLOBAL'] = validate_numeric_cli_input(max_ows_global, '100') max_wms_getmap = input('Maximum number of simultaneous GetMap requests [8]: ') @@ -676,10 +695,10 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ } host_config = create_host_config( - binds=[ - '/usr/lib/tethys/geoserver/data:/var/geoserver/data' - ] - ) + binds=[ + '/usr/lib/tethys/geoserver/data:/var/geoserver/data' + ] + ) docker_client.create_container( name=GEOSERVER_CONTAINER, @@ -697,7 +716,8 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ # 52 North WPS if N52WPS_CONTAINER in containers_to_create or force: - print("\nInstalling the 52 North WPS Docker container...") + with pretty_output(FG_WHITE) as p: + p.write("\nInstalling the 52 North WPS Docker container...") # Default environmental vars name = 'NONE' @@ -714,8 +734,9 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ password = 'wps' if not defaults: - print("Provide contact information for the 52 North Web Processing Service or press enter to accept the " - "defaults shown in square brackets: ") + with pretty_output(FG_WHITE) as p: + p.write("Provide contact information for the 52 North Web Processing Service or press enter to accept " + "the defaults shown in square brackets: ") name = input('Name [NONE]: ') if name == '': @@ -771,7 +792,8 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ password_2 = getpass.getpass('Confirm Password: ') while password_1 != password_2: - print('Passwords do not match, please try again.') + with pretty_output(FG_WHITE) as p: + p.write('Passwords do not match, please try again.') password_1 = getpass.getpass('Admin Password [wps]: ') password_2 = getpass.getpass('Confirm Password: ') @@ -794,7 +816,8 @@ def install_docker_containers(docker_client, force=False, containers=ALL_DOCKER_ 'PASSWORD': password} ) - print("Finished installing Docker containers.") + with pretty_output(FG_WHITE) as p: + p.write("Finished installing Docker containers.") def start_docker_containers(docker_client, containers=ALL_DOCKER_INPUTS): @@ -810,22 +833,24 @@ def start_docker_containers(docker_client, containers=ALL_DOCKER_INPUTS): # Start PostGIS try: if not container_status[POSTGIS_CONTAINER] and container == POSTGIS_INPUT: - print('Starting PostGIS container...') + with pretty_output(FG_WHITE) as p: + p.write('Starting PostGIS container...') docker_client.start(container=POSTGIS_CONTAINER, # restart_policy='always', port_bindings={5432: DEFAULT_POSTGIS_PORT}) elif container == POSTGIS_INPUT: - print('PostGIS container already running...') + with pretty_output(FG_WHITE) as p: + p.write('PostGIS container already running...') except KeyError: if container == POSTGIS_INPUT: - print('PostGIS container not installed...') - except: - raise + with pretty_output(FG_WHITE) as p: + p.write('PostGIS container not installed...') try: if not container_status[GEOSERVER_CONTAINER] and container == GEOSERVER_INPUT: # Start GeoServer - print('Starting GeoServer container...') + with pretty_output(FG_WHITE) as p: + p.write('Starting GeoServer container...') if 'cluster' in container_images[GEOSERVER_CONTAINER]: docker_client.start(container=GEOSERVER_CONTAINER, # restart_policy='always', @@ -839,27 +864,28 @@ def start_docker_containers(docker_client, containers=ALL_DOCKER_INPUTS): # restart_policy='always', port_bindings={8080: DEFAULT_GEOSERVER_PORT}) elif not container or container == GEOSERVER_INPUT: - print('GeoServer container already running...') + with pretty_output(FG_WHITE) as p: + p.write('GeoServer container already running...') except KeyError: if container == GEOSERVER_INPUT: - print('GeoServer container not installed...') - except: - raise + with pretty_output(FG_WHITE) as p: + p.write('GeoServer container not installed...') try: if not container_status[N52WPS_CONTAINER] and container == N52WPS_INPUT: # Start 52 North WPS - print('Starting 52 North WPS container...') + with pretty_output(FG_WHITE) as p: + p.write('Starting 52 North WPS container...') docker_client.start(container=N52WPS_CONTAINER, # restart_policy='always', port_bindings={8080: DEFAULT_N52WPS_PORT}) elif container == N52WPS_INPUT: - print('52 North WPS container already running...') + with pretty_output(FG_WHITE) as p: + p.write('52 North WPS container already running...') except KeyError: if not container or container == N52WPS_INPUT: - print('52 North WPS container not installed...') - except: - raise + with pretty_output(FG_WHITE) as p: + p.write('52 North WPS container not installed...') def stop_docker_containers(docker_client, silent=False, containers=ALL_DOCKER_INPUTS): @@ -874,49 +900,52 @@ def stop_docker_containers(docker_client, silent=False, containers=ALL_DOCKER_IN try: if container_status[POSTGIS_CONTAINER] and container == POSTGIS_INPUT: if not silent: - print('Stopping PostGIS container...') + with pretty_output(FG_WHITE) as p: + p.write('Stopping PostGIS container...') docker_client.stop(container=POSTGIS_CONTAINER) elif not silent and container == POSTGIS_INPUT: - print('PostGIS container already stopped.') + with pretty_output(FG_WHITE) as p: + p.write('PostGIS container already stopped.') except KeyError: if not container or container == POSTGIS_INPUT: - print('PostGIS container not installed...') - except: - raise + with pretty_output(FG_WHITE) as p: + p.write('PostGIS container not installed...') # Stop GeoServer try: if container_status[GEOSERVER_CONTAINER] and container == GEOSERVER_INPUT: if not silent: - print('Stopping GeoServer container...') + with pretty_output(FG_WHITE) as p: + p.write('Stopping GeoServer container...') docker_client.stop(container=GEOSERVER_CONTAINER) elif not silent and container == GEOSERVER_INPUT: - print('GeoServer container already stopped.') + with pretty_output(FG_WHITE) as p: + p.write('GeoServer container already stopped.') except KeyError: if not container or container == GEOSERVER_INPUT: - print('GeoServer container not installed...') - except: - raise + with pretty_output(FG_WHITE) as p: + p.write('GeoServer container not installed...') # Stop 52 North WPS try: if container_status[N52WPS_CONTAINER] and container == N52WPS_INPUT: if not silent: - print('Stopping 52 North WPS container...') + with pretty_output(FG_WHITE) as p: + p.write('Stopping 52 North WPS container...') docker_client.stop(container=N52WPS_CONTAINER) elif not silent and container == N52WPS_INPUT: - print('52 North WPS container already stopped.') + with pretty_output(FG_WHITE) as p: + p.write('52 North WPS container already stopped.') except KeyError: if not container or container == N52WPS_INPUT: - print('52 North WPS container not installed...') - except: - raise + with pretty_output(FG_WHITE) as p: + p.write('52 North WPS container not installed...') def remove_docker_containers(docker_client, containers=ALL_DOCKER_INPUTS): @@ -929,17 +958,20 @@ def remove_docker_containers(docker_client, containers=ALL_DOCKER_INPUTS): for container in containers: # Remove PostGIS if container == POSTGIS_INPUT and POSTGIS_CONTAINER not in containers_not_installed: - print('Removing PostGIS...') + with pretty_output(FG_WHITE) as p: + p.write('Removing PostGIS...') docker_client.remove_container(container=POSTGIS_CONTAINER) # Remove GeoServer if container == GEOSERVER_INPUT and GEOSERVER_CONTAINER not in containers_not_installed: - print('Removing GeoServer...') + with pretty_output(FG_WHITE) as p: + p.write('Removing GeoServer...') docker_client.remove_container(container=GEOSERVER_CONTAINER, v=True) # Remove 52 North WPS if container == N52WPS_INPUT and N52WPS_CONTAINER not in containers_not_installed: - print('Removing 52 North WPS...') + with pretty_output(FG_WHITE) as p: + p.write('Removing 52 North WPS...') docker_client.remove_container(container=N52WPS_CONTAINER) @@ -955,9 +987,11 @@ def docker_init(containers=None, defaults=False): images_to_install = get_images_to_install(docker_client, containers=containers) if len(images_to_install) < 1: - print("Docker images already pulled.") + with pretty_output(FG_WHITE) as p: + p.write("Docker images already pulled.") else: - print("Pulling Docker images...") + with pretty_output(FG_WHITE) as p: + p.write("Pulling Docker images...") # Pull the Docker images for image in images_to_install: @@ -1038,27 +1072,36 @@ def docker_status(): # PostGIS if POSTGIS_CONTAINER in container_status and container_status[POSTGIS_CONTAINER]: - print('PostGIS/Database: Running') + with pretty_output(FG_WHITE) as p: + p.write('PostGIS/Database: Running') elif POSTGIS_CONTAINER in container_status and not container_status[POSTGIS_CONTAINER]: - print('PostGIS/Database: Stopped') + with pretty_output(FG_WHITE) as p: + p.write('PostGIS/Database: Stopped') else: - print('PostGIS/Database: Not Installed') + with pretty_output(FG_WHITE) as p: + p.write('PostGIS/Database: Not Installed') # GeoServer if GEOSERVER_CONTAINER in container_status and container_status[GEOSERVER_CONTAINER]: - print('GeoServer: Running') + with pretty_output(FG_WHITE) as p: + p.write('GeoServer: Running') elif GEOSERVER_CONTAINER in container_status and not container_status[GEOSERVER_CONTAINER]: - print('GeoServer: Stopped') + with pretty_output(FG_WHITE) as p: + p.write('GeoServer: Stopped') else: - print('GeoServer: Not Installed') + with pretty_output(FG_WHITE) as p: + p.write('GeoServer: Not Installed') # 52 North WPS if N52WPS_CONTAINER in container_status and container_status[N52WPS_CONTAINER]: - print('52 North WPS: Running') + with pretty_output(FG_WHITE) as p: + p.write('52 North WPS: Running') elif N52WPS_CONTAINER in container_status and not container_status[N52WPS_CONTAINER]: - print('52 North WPS: Stopped') + with pretty_output(FG_WHITE) as p: + p.write('52 North WPS: Stopped') else: - print('52 North WPS: Not Installed') + with pretty_output(FG_WHITE) as p: + p.write('52 North WPS: Not Installed') def docker_update(containers=None, defaults=False): @@ -1113,50 +1156,91 @@ def docker_ip(): if container_status[POSTGIS_CONTAINER]: postgis_container = containers[POSTGIS_CONTAINER] postgis_port = postgis_container['Ports'][0]['PublicPort'] - print('\nPostGIS/Database:') - print(' Host: {0}'.format(docker_host)) - print(' Port: {0}'.format(postgis_port)) + with pretty_output(FG_WHITE) as p: + p.write('\nPostGIS/Database:') + p.write(' Host: {0}'.format(docker_host)) + p.write(' Port: {0}'.format(postgis_port)) + p.write(' Endpoint: postgresql://:@{}:{}/'.format( + docker_host, postgis_port + )) else: - print('\nPostGIS/Database: Not Running.') + with pretty_output(FG_WHITE) as p: + p.write('\nPostGIS/Database: Not Running.') except KeyError: # If key error is raised, it is likely not installed. - print('\nPostGIS/Database: Not Installed.') - except: - raise + with pretty_output(FG_WHITE) as p: + p.write('\nPostGIS/Database: Not Installed.') # GeoServer try: if container_status[GEOSERVER_CONTAINER]: geoserver_container = containers[GEOSERVER_CONTAINER] - geoserver_port = geoserver_container['Ports'][0]['PublicPort'] - print('\nGeoServer:') - print(' Host: {0}'.format(docker_host)) - print(' Port: {0}'.format(geoserver_port)) - print(' Endpoint: http://{0}:{1}/geoserver/rest'.format(docker_host, geoserver_port)) + + node_ports = [] + for port in geoserver_container['Ports']: + if port['PublicPort'] != 8181: + node_ports.append(str(port['PublicPort'])) + + with pretty_output(FG_WHITE) as p: + p.write('\nGeoServer:') + p.write(' Host: {}'.format(docker_host)) + p.write(' Primary Port: 8181') + p.write(' Node Ports: {}'.format(', '.join(node_ports))) + p.write(' Endpoint: http://{}:8181/geoserver/rest'.format(docker_host)) else: - print('\nGeoServer: Not Running.') + with pretty_output(FG_WHITE) as p: + p.write('\nGeoServer: Not Running.') except KeyError: # If key error is raised, it is likely not installed. - print('\nGeoServer: Not Installed.') - except: - raise + with pretty_output(FG_WHITE) as p: + p.write('\nGeoServer: Not Installed.') # 52 North WPS try: if container_status[N52WPS_CONTAINER]: n52wps_container = containers[N52WPS_CONTAINER] n52wps_port = n52wps_container['Ports'][0]['PublicPort'] - print('\n52 North WPS:') - print(' Host: {0}'.format(docker_host)) - print(' Port: {0}'.format(n52wps_port)) - print(' Endpoint: http://{0}:{1}/wps/WebProcessingService\n'.format(docker_host, n52wps_port)) + with pretty_output(FG_WHITE) as p: + p.write('\n52 North WPS:') + p.write(' Host: {}'.format(docker_host)) + p.write(' Port: {}'.format(n52wps_port)) + p.write(' Endpoint: http://{}:{}/wps/WebProcessingService\n'.format(docker_host, n52wps_port)) else: - print('\n52 North WPS: Not Running.') + with pretty_output(FG_WHITE) as p: + p.write('\n52 North WPS: Not Running.') except KeyError: # If key error is raised, it is likely not installed. - print('\n52 North WPS: Not Installed.') - except: - raise + with pretty_output(FG_WHITE) as p: + p.write('\n52 North WPS: Not Installed.') + + +def docker_command(args): + """ + Docker management commands. + """ + if args.command == 'init': + docker_init(containers=args.containers, defaults=args.defaults) + + elif args.command == 'start': + docker_start(containers=args.containers) + + elif args.command == 'stop': + docker_stop(containers=args.containers, boot2docker=args.boot2docker) + + elif args.command == 'status': + docker_status() + + elif args.command == 'update': + docker_update(containers=args.containers, defaults=args.defaults) + + elif args.command == 'remove': + docker_remove(containers=args.containers) + + elif args.command == 'ip': + docker_ip() + + elif args.command == 'restart': + docker_restart(containers=args.containers) diff --git a/tethys_apps/cli/gen_commands.py b/tethys_apps/cli/gen_commands.py index 00c1f44f2..67f761be1 100644 --- a/tethys_apps/cli/gen_commands.py +++ b/tethys_apps/cli/gen_commands.py @@ -7,29 +7,31 @@ * License: BSD 2-Clause ******************************************************************************** """ +from __future__ import print_function from builtins import input import os import string import random -from .manage_commands import TETHYS_HOME +from tethys_apps.utilities import get_tethys_home_dir, get_tethys_src_dir from platform import linux_distribution from django.template import Template, Context -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tethys_portal.settings") from django.conf import settings +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tethys_portal.settings") + # Initialize settings try: __import__(os.environ['DJANGO_SETTINGS_MODULE']) -except: +except Exception: # Initialize settings with templates variable to allow gen to work properly settings.configure(TEMPLATES=[ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', } ]) -import django +import django # noqa: E402 django.setup() @@ -77,6 +79,10 @@ def generate_command(args): """ Generate a settings file for a new installation. """ + # Consts + TETHYS_HOME = get_tethys_home_dir() + TETHYS_SRC = get_tethys_src_dir() + # Setup variables context = Context() @@ -119,6 +125,7 @@ def generate_command(args): secret_key = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(50)]) context.update({'secret_key': secret_key, 'allowed_host': args.allowed_host, + 'allowed_hosts': args.allowed_hosts, 'db_username': args.db_username, 'db_password': args.db_password, 'db_port': args.db_port, @@ -127,29 +134,33 @@ def generate_command(args): }) if args.type == GEN_NGINX_OPTION: - hostname = ''. join(settings.ALLOWED_HOSTS) + hostname = str(settings.ALLOWED_HOSTS[0]) if len(settings.ALLOWED_HOSTS) > 0 else '127.0.0.1' workspaces_root = get_settings_value('TETHYS_WORKSPACES_ROOT') static_root = get_settings_value('STATIC_ROOT') context.update({'hostname': hostname, 'workspaces_root': workspaces_root, 'static_root': static_root, + 'client_max_body_size': args.client_max_body_size }) if args.type == GEN_UWSGI_SERVICE_OPTION: conda_home = get_environment_value('CONDA_HOME') conda_env_name = get_environment_value('CONDA_ENV_NAME') - linux_distro = linux_distribution(full_distribution_name=0)[0] user_option_prefix = '' - if linux_distro in ['redhat', 'centos']: - user_option_prefix = 'http-' + + try: + linux_distro = linux_distribution(full_distribution_name=0)[0] + if linux_distro in ['redhat', 'centos']: + user_option_prefix = 'http-' + except Exception: + pass context.update({'nginx_user': nginx_user, 'conda_home': conda_home, 'conda_env_name': conda_env_name, - 'tethys_home': TETHYS_HOME, - 'linux_distribution': linux_distro, + 'tethys_src': TETHYS_SRC, 'user_option_prefix': user_option_prefix }) @@ -158,7 +169,8 @@ def generate_command(args): conda_env_name = get_environment_value('CONDA_ENV_NAME') context.update({'conda_home': conda_home, - 'conda_env_name': conda_env_name}) + 'conda_env_name': conda_env_name, + 'uwsgi_processes': args.uwsgi_processes}) if args.directory: if os.path.isdir(args.directory): diff --git a/tethys_apps/cli/gen_templates/nginx b/tethys_apps/cli/gen_templates/nginx index 7e1ac145b..c37425624 100644 --- a/tethys_apps/cli/gen_templates/nginx +++ b/tethys_apps/cli/gen_templates/nginx @@ -13,11 +13,11 @@ server { charset utf-8; # max upload size - client_max_body_size 75M; # adjust to taste + client_max_body_size {{ client_max_body_size }}; # adjust to taste # Tethys Workspaces location /workspaces { - internal; + internal; alias {{ workspaces_root }}; # your Tethys workspaces files - amend as required } diff --git a/tethys_apps/cli/gen_templates/settings b/tethys_apps/cli/gen_templates/settings index dacdadca8..a275bd7d1 100644 --- a/tethys_apps/cli/gen_templates/settings +++ b/tethys_apps/cli/gen_templates/settings @@ -45,24 +45,41 @@ SESSION_SECURITY_EXPIRE_AFTER = 900 LOGGING = { 'version': 1, 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s:%(name)s:%(message)s' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, 'handlers': { - 'console': { + 'console_simple': { + 'class': 'logging.StreamHandler', + 'formatter': 'simple' + }, + 'console_verbose': { 'class': 'logging.StreamHandler', + 'formatter': 'verbose' }, }, 'loggers': { 'django': { - 'handlers': ['console'], + 'handlers': ['console_simple'], 'level': os.getenv('DJANGO_LOG_LEVEL', 'WARNING'), }, 'tethys': { - 'handlers': ['console'], + 'handlers': ['console_verbose'], 'level': 'INFO', } }, } +{% if allowed_hosts %} +ALLOWED_HOSTS = {{ allowed_hosts|safe }} +{% else %} ALLOWED_HOSTS = [{% if allowed_host %}'{{ allowed_host }}'{% endif %}] +{% endif %} # Application definition @@ -199,7 +216,7 @@ TEMPLATES = [ 'loaders': [ 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', - 'tethys_apps.template_loaders.TethysAppsTemplateLoader' + 'tethys_apps.template_loaders.TethysTemplateLoader' ], 'debug': DEBUG } @@ -216,7 +233,7 @@ STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'), ) STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'tethys_apps.utilities.TethysAppsStaticFinder' + 'tethys_apps.static_finders.TethysStaticFinder' ) # Uncomment the next line for production installation diff --git a/tethys_apps/cli/gen_templates/uwsgi_service b/tethys_apps/cli/gen_templates/uwsgi_service index e97801170..54b0e00ff 100644 --- a/tethys_apps/cli/gen_templates/uwsgi_service +++ b/tethys_apps/cli/gen_templates/uwsgi_service @@ -4,7 +4,7 @@ After=syslog.target [Service] ExecStartPre=/bin/bash -c 'mkdir -p /run/uwsgi; chown {{ nginx_user }}:{{ nginx_user }} /run/uwsgi' -ExecStart={{ conda_home }}/envs/{{ conda_env_name }}/bin/uwsgi --yaml {{ tethys_home }}/src/tethys_portal/tethys_uwsgi.yml --{{ user_option_prefix }}uid {{ nginx_user }} --{{ user_option_prefix }}gid {{ nginx_user }} +ExecStart={{ conda_home }}/envs/{{ conda_env_name }}/bin/uwsgi --yaml {{ tethys_src }}/tethys_portal/tethys_uwsgi.yml --{{ user_option_prefix }}uid {{ nginx_user }} --{{ user_option_prefix }}gid {{ nginx_user }} ExecStartPost=/bin/bash -c 'chown -R {{ nginx_user }}:{{ nginx_user }} /run/uwsgi' Restart=always KillSignal=SIGQUIT diff --git a/tethys_apps/cli/gen_templates/uwsgi_settings b/tethys_apps/cli/gen_templates/uwsgi_settings index 852c5be2b..85391a3a8 100644 --- a/tethys_apps/cli/gen_templates/uwsgi_settings +++ b/tethys_apps/cli/gen_templates/uwsgi_settings @@ -14,7 +14,7 @@ uwsgi: master: true pidfile2: /run/uwsgi/tethys.pid # maximum number of worker processes - processes: 10 + processes: {{ uwsgi_processes }} # the socket file with correct permissions socket: /run/uwsgi/tethys.sock chmod-socket: 600 diff --git a/tethys_apps/cli/link_commands.py b/tethys_apps/cli/link_commands.py new file mode 100644 index 000000000..0fc835843 --- /dev/null +++ b/tethys_apps/cli/link_commands.py @@ -0,0 +1,47 @@ +from tethys_apps.utilities import link_service_to_app_setting +from tethys_apps.cli.cli_colors import pretty_output, FG_RED + + +def link_command(args): + """ + Interact with Tethys Services (Spatial/Persistent Stores) to create them and/or link them to existing apps + """ + try: + service = args.service + setting = args.setting + + service_parts = service.split(':') + setting_parts = setting.split(':') + service_type = None + service_uid = None + setting_app_package = None + setting_type = None + setting_uid = None + + try: + service_type = service_parts[0] + service_uid = service_parts[1] + + setting_app_package = setting_parts[0] + setting_type = setting_parts[1] + setting_uid = setting_parts[2] + except IndexError: + with pretty_output(FG_RED) as p: + p.write( + 'Incorrect argument format. \nUsage: "tethys link : ' + ':"' + '\nCommand aborted.') + exit(1) + + success = link_service_to_app_setting(service_type, service_uid, setting_app_package, setting_type, setting_uid) + + if not success: + exit(1) + + exit(0) + + except Exception as e: + with pretty_output(FG_RED) as p: + p.write(e) + p.write('An unexpected error occurred. Please try again.') + exit(1) diff --git a/tethys_apps/cli/list_command.py b/tethys_apps/cli/list_command.py new file mode 100644 index 000000000..c5258c012 --- /dev/null +++ b/tethys_apps/cli/list_command.py @@ -0,0 +1,20 @@ +from __future__ import print_function +from tethys_apps.helpers import get_installed_tethys_apps, get_installed_tethys_extensions + + +def list_command(args): + """ + List installed apps. + """ + installed_apps = get_installed_tethys_apps() + installed_extensions = get_installed_tethys_extensions() + + if installed_apps: + print('Apps:') + for item in installed_apps: + print(' {}'.format(item)) + + if installed_extensions: + print('Extensions:') + for item in installed_extensions: + print(' {}'.format(item)) diff --git a/tethys_apps/cli/manage_commands.py b/tethys_apps/cli/manage_commands.py index 19e728ebd..4d542f31c 100644 --- a/tethys_apps/cli/manage_commands.py +++ b/tethys_apps/cli/manage_commands.py @@ -11,17 +11,18 @@ import os import subprocess +from tethys_apps.cli.cli_colors import pretty_output, FG_RED from tethys_apps.base.testing.environment import set_testing_environment +from tethys_apps.utilities import get_tethys_src_dir + -CURRENT_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -TETHYS_HOME = os.sep.join(CURRENT_SCRIPT_DIR.split(os.sep)[:-3]) -TETHYS_SRC_DIRECTORY = os.sep.join(CURRENT_SCRIPT_DIR.split(os.sep)[:-2]) MANAGE_START = 'start' MANAGE_SYNCDB = 'syncdb' MANAGE_COLLECTSTATIC = 'collectstatic' MANAGE_COLLECTWORKSPACES = 'collectworkspaces' MANAGE_COLLECT = 'collectall' MANAGE_CREATESUPERUSER = 'createsuperuser' +MANAGE_SYNC = 'sync' def get_manage_path(args): @@ -29,7 +30,7 @@ def get_manage_path(args): Validate user defined manage path, use default, or throw error """ # Determine path to manage.py file - manage_path = os.path.join(TETHYS_SRC_DIRECTORY, 'manage.py') + manage_path = os.path.join(get_tethys_src_dir(), 'manage.py') # Check for path option if hasattr(args, 'manage'): @@ -37,7 +38,8 @@ def get_manage_path(args): # Throw error if path is not valid if not os.path.isfile(manage_path): - print('ERROR: Can\'t open file "{0}", no such file.'.format(manage_path)) + with pretty_output(FG_RED) as p: + p.write('ERROR: Can\'t open file "{0}", no such file.'.format(manage_path)) exit(1) return manage_path @@ -77,15 +79,18 @@ def manage_command(args): elif args.command == MANAGE_COLLECTWORKSPACES: # Run collectworkspaces command - primary_process = ['python', manage_path, 'collectworkspaces'] + if args.force: + primary_process = ['python', manage_path, 'collectworkspaces', '--force'] + else: + primary_process = ['python', manage_path, 'collectworkspaces'] elif args.command == MANAGE_COLLECT: # Convenience command to run collectstatic and collectworkspaces - ## Run pre_collectstatic + # Run pre_collectstatic intermediate_process = ['python', manage_path, 'pre_collectstatic'] run_process(intermediate_process) - ## Setup for main collectstatic + # Setup for main collectstatic intermediate_process = ['python', manage_path, 'collectstatic'] if args.noinput: @@ -93,12 +98,16 @@ def manage_command(args): run_process(intermediate_process) - ## Run collectworkspaces command + # Run collectworkspaces command primary_process = ['python', manage_path, 'collectworkspaces'] elif args.command == MANAGE_CREATESUPERUSER: primary_process = ['python', manage_path, 'createsuperuser'] + elif args.command == MANAGE_SYNC: + from tethys_apps.harvester import SingletonHarvester + harvester = SingletonHarvester() + harvester.harvest() if primary_process: run_process(primary_process) @@ -109,7 +118,7 @@ def run_process(process): try: if 'test' in process: set_testing_environment(True) - subprocess.call(process) + return subprocess.call(process) except KeyboardInterrupt: pass finally: diff --git a/tethys_apps/cli/scaffold_commands.py b/tethys_apps/cli/scaffold_commands.py index e007f8a00..30b52902b 100644 --- a/tethys_apps/cli/scaffold_commands.py +++ b/tethys_apps/cli/scaffold_commands.py @@ -1,11 +1,12 @@ -from builtins import * import os import re import logging import random import shutil +from builtins import input from django.template import Template, Context +from tethys_apps.cli.cli_colors import pretty_output, FG_RED, FG_YELLOW, FG_WHITE # Constants APP_PREFIX = 'tethysapp' @@ -38,11 +39,13 @@ def proper_name_validator(value, default): value = value.replace('-', ' ') value = value.replace('"', '') value = value.replace("'", "") - print('Warning: Illegal characters were detected in proper name "{0}". They have been replaced or ' - 'removed with valid characters: "{1}"'.format(before, value)) + with pretty_output(FG_YELLOW) as p: + p.write('Warning: Illegal characters were detected in proper name "{0}". They have been replaced or ' + 'removed with valid characters: "{1}"'.format(before, value)) # Otherwise, throw error else: - print('Error: Proper name can only contain letters and numbers and spaces.') + with pretty_output(FG_RED) as p: + p.write('Error: Proper name can only contain letters and numbers and spaces.') return False, value return True, value @@ -83,7 +86,8 @@ def theme_color_validator(value, default): value = '#' + value return True, value except ValueError: - print("Error: Value given is not a valid hexadecimal color.") + with pretty_output(FG_RED) as p: + p.write("Error: Value given is not a valid hexadecimal color.") return False, value @@ -118,8 +122,8 @@ def scaffold_command(args): if args.extension: is_extension = True - template_name = args.extension - template_root = os.path.join(EXTENSION_PATH, args.extension) + template_name = args.template + template_root = os.path.join(EXTENSION_PATH, args.template) else: template_name = args.template template_root = os.path.join(APP_PATH, args.template) @@ -128,7 +132,8 @@ def scaffold_command(args): # Validate template if not os.path.isdir(template_root): - print('Error: "{}" is not a valid template.'.format(template_name)) + with pretty_output(FG_WHITE) as p: + p.write('Error: "{}" is not a valid template.'.format(template_name)) exit(1) # Validate project name @@ -144,8 +149,9 @@ def scaffold_command(args): if contains_uppers: before = project_name project_name = project_name.lower() - print('Warning: Uppercase characters in project name "{0}" ' - 'changed to lowercase: "{1}".'.format(before, project_name)) + with pretty_output(FG_YELLOW) as p: + p.write('Warning: Uppercase characters in project name "{0}" ' + 'changed to lowercase: "{1}".'.format(before, project_name)) # Check for valid characters name project_error_regex = re.compile(r'^[a-zA-Z0-9_]+$') @@ -157,12 +163,14 @@ def scaffold_command(args): if project_warning_regex.match(project_name): before = project_name project_name = project_name.replace('-', '_') - print('Warning: Dashes in project name "{0}" have been replaced ' - 'with underscores "{1}"'.format(before, project_name)) + with pretty_output(FG_YELLOW) as p: + p.write('Warning: Dashes in project name "{0}" have been replaced ' + 'with underscores "{1}"'.format(before, project_name)) # Otherwise, throw error else: - print('Error: Invalid characters in project name "{0}". ' - 'Only letters, numbers, and underscores.'.format(project_name)) + with pretty_output(FG_YELLOW) as p: + p.write('Error: Invalid characters in project name "{0}". ' + 'Only letters, numbers, and underscores.'.format(project_name)) exit(1) # Project name derivatives @@ -170,64 +178,99 @@ def scaffold_command(args): split_project_name = project_name.split('_') title_case_project_name = [x.title() for x in split_project_name] default_proper_name = ' '.join(title_case_project_name) - app_class_name = ''.join(title_case_project_name) + class_name = ''.join(title_case_project_name) default_theme_color = get_random_color() - print('Creating new Tethys project named "{0}".'.format(project_dir)) + with pretty_output(FG_WHITE) as p: + p.write('Creating new Tethys project named "{0}".'.format(project_dir)) # Get metadata from user - metadata_input = ( - { - 'name': 'proper_name', - 'prompt': 'Proper name for the app (e.g.: "My First App")', - 'default': default_proper_name, - 'validator': proper_name_validator - }, - { - 'name': 'description', - 'prompt': 'Brief description of the app', - 'default': '', - 'validator': None - }, - { - 'name': 'color', - 'prompt': 'App theme color (e.g.: "#27AE60")', - 'default': default_theme_color, - 'validator': theme_color_validator - }, - { - 'name': 'tags', - 'prompt': 'Tags: Use commas to delineate tags and ' - 'quotes around each tag (e.g.: "Hydrology","Reference Timeseries")', - 'default': '', - 'validator': None - }, - { - 'name': 'author', - 'prompt': 'Author name', - 'default': '', - 'validator': None - }, - { - 'name': 'author_email', - 'prompt': 'Author email', - 'default': '', - 'validator': None - }, - { - 'name': 'license_name', - 'prompt': 'License name', - 'default': '', - 'validator': None - }, - ) + if not is_extension: + metadata_input = ( + { + 'name': 'proper_name', + 'prompt': 'Proper name for the app (e.g.: "My First App")', + 'default': default_proper_name, + 'validator': proper_name_validator + }, + { + 'name': 'description', + 'prompt': 'Brief description of the app', + 'default': '', + 'validator': None + }, + { + 'name': 'color', + 'prompt': 'App theme color (e.g.: "#27AE60")', + 'default': default_theme_color, + 'validator': theme_color_validator + }, + { + 'name': 'tags', + 'prompt': 'Tags: Use commas to delineate tags and ' + 'quotes around each tag (e.g.: "Hydrology","Reference Timeseries")', + 'default': '', + 'validator': None + }, + { + 'name': 'author', + 'prompt': 'Author name', + 'default': '', + 'validator': None + }, + { + 'name': 'author_email', + 'prompt': 'Author email', + 'default': '', + 'validator': None + }, + { + 'name': 'license_name', + 'prompt': 'License name', + 'default': '', + 'validator': None + }, + ) + else: + metadata_input = ( + { + 'name': 'proper_name', + 'prompt': 'Proper name for the extension (e.g.: "My First Extension")', + 'default': default_proper_name, + 'validator': proper_name_validator + }, + { + 'name': 'description', + 'prompt': 'Brief description of the extension', + 'default': '', + 'validator': None + }, + { + 'name': 'author', + 'prompt': 'Author name', + 'default': '', + 'validator': None + }, + { + 'name': 'author_email', + 'prompt': 'Author email', + 'default': '', + 'validator': None + }, + { + 'name': 'license_name', + 'prompt': 'License name', + 'default': '', + 'validator': None + }, + ) # Build up template context context = { 'project': project_name, 'project_dir': project_dir, 'project_url': project_name.replace('_', '-'), - 'app_class_name': app_class_name, + 'class_name': class_name, 'proper_name': default_proper_name, 'description': '', 'color': default_theme_color, @@ -247,7 +290,8 @@ def scaffold_command(args): try: response = input('{0} ["{1}"]: '.format(item['prompt'], item['default'])) or item['default'] except (KeyboardInterrupt, SystemExit): - print('\nScaffolding cancelled.') + with pretty_output(FG_YELLOW) as p: + p.write('\nScaffolding cancelled.') exit(1) if callable(item['validator']): @@ -256,7 +300,8 @@ def scaffold_command(args): valid = True if not valid: - print('Invalid response: {}'.format(response)) + with pretty_output(FG_RED) as p: + p.write('Invalid response: {}'.format(response)) context[item['name']] = response @@ -279,20 +324,24 @@ def scaffold_command(args): response = input('Directory "{}" already exists. ' 'Would you like to overwrite it? [Y/n]: '.format(project_root)) or default except (KeyboardInterrupt, SystemExit): - print('\nScaffolding cancelled.') + with pretty_output(FG_YELLOW) as p: + p.write('\nScaffolding cancelled.') exit(1) if response.lower() in valid_choices: valid = True if response.lower() in negative_choices: - print('Scaffolding cancelled.') + with pretty_output(FG_YELLOW) as p: + p.write('Scaffolding cancelled.') exit(0) try: shutil.rmtree(project_root) except OSError: - print('Error: Unable to overwrite "{}". Please remove the directory and try again.'.format(project_root)) + with pretty_output(FG_YELLOW) as p: + p.write('Error: Unable to overwrite "{}". ' + 'Please remove the directory and try again.'.format(project_root)) exit(1) # Walk the template directory, creating the templates and directories in the new project as we go @@ -305,7 +354,8 @@ def scaffold_command(args): # Create Root Directory os.makedirs(curr_project_root) - print('Created: "{}"'.format(curr_project_root)) + with pretty_output(FG_WHITE) as p: + p.write('Created: "{}"'.format(curr_project_root)) # Create Files for template_file in template_files: @@ -331,6 +381,8 @@ def scaffold_command(args): if template: with open(project_file_path, 'w') as pfp: pfp.write(template.render(template_context)) - print('Created: "{}"'.format(project_file_path)) + with pretty_output(FG_WHITE) as p: + p.write('Created: "{}"'.format(project_file_path)) - print('Successfully scaffolded new project "{}"'.format(project_name)) + with pretty_output(FG_WHITE) as p: + p.write('Successfully scaffolded new project "{}"'.format(project_name)) diff --git a/tethys_apps/cli/scaffold_templates/app_templates/default/setup.py_tmpl b/tethys_apps/cli/scaffold_templates/app_templates/default/setup.py_tmpl index 25997cc2f..7207ada4f 100644 --- a/tethys_apps/cli/scaffold_templates/app_templates/default/setup.py_tmpl +++ b/tethys_apps/cli/scaffold_templates/app_templates/default/setup.py_tmpl @@ -3,13 +3,13 @@ import sys from setuptools import setup, find_packages from tethys_apps.app_installation import custom_develop_command, custom_install_command -### Apps Definition ### +# -- Apps Definition -- # app_package = '{{project}}' release_package = 'tethysapp-' + app_package -app_class = '{{project}}.app:{{app_class_name}}' +app_class = '{{project}}.app:{{class_name}}' app_package_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tethysapp', app_package) -### Python Dependencies ### +# -- Python Dependencies -- # dependencies = [] setup( diff --git a/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/__init__.py b/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/__init__.py index 62a094218..2e2033b3c 100644 --- a/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/__init__.py +++ b/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/__init__.py @@ -4,4 +4,4 @@ pkg_resources.declare_namespace(__name__) except ImportError: import pkgutil - __path__ = pkgutil.extend_path(__path__, __name__) \ No newline at end of file + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/app.py_tmpl b/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/app.py_tmpl index 081b22672..a35a150fb 100644 --- a/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/app.py_tmpl +++ b/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/app.py_tmpl @@ -1,7 +1,7 @@ from tethys_sdk.base import TethysAppBase, url_map_maker -class {{app_class_name}}(TethysAppBase): +class {{class_name}}(TethysAppBase): """ Tethys app class for {{proper_name}}. """ diff --git a/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/model.py b/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/model.py index f8f1bf485..770e9b4cb 100644 --- a/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/model.py +++ b/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/model.py @@ -1 +1 @@ -# Put your persistent store models in this file \ No newline at end of file +# Put your persistent store models in this file diff --git a/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/tests/tests.py_tmpl b/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/tests/tests.py_tmpl index b7d6a6bc1..b428c0949 100644 --- a/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/tests/tests.py_tmpl +++ b/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/+project+/tests/tests.py_tmpl @@ -4,7 +4,7 @@ from tethys_sdk.testing import TethysTestCase # Use if your app has persistent stores that will be tested against. # Your app class from app.py must be passed as an argument to the TethysTestCase functions to both # create and destroy the temporary persistent stores for your app used during testing -# from ..app import {{app_class_name}} +# from ..app import {{class_name}} # Use if you'd like a simplified way to test rendered HTML templates. # You likely need to install BeautifulSoup, as it is not included by default in Tethys Platform @@ -39,11 +39,11 @@ To run any tests: To run all tests in this file: Test command: "tethys test -f tethys_apps.tethysapp.{{project}}.tests.tests" - To run tests in the {{app_class_name}}TestCase class: - Test command: "tethys test -f tethys_apps.tethysapp.{{project}}.tests.tests.{{app_class_name}}TestCase" + To run tests in the {{class_name}}TestCase class: + Test command: "tethys test -f tethys_apps.tethysapp.{{project}}.tests.tests.{{class_name}}TestCase" - To run only the test_if_tethys_platform_is_great function in the {{app_class_name}}TestCase class: - Test command: "tethys test -f tethys_apps.tethysapp.{{project}}.tests.tests.{{app_class_name}}TestCase.test_if_tethys_platform_is_great" + To run only the test_if_tethys_platform_is_great function in the {{class_name}}TestCase class: + Test command: "tethys test -f tethys_apps.tethysapp.{{project}}.tests.tests.{{class_name}}TestCase.test_if_tethys_platform_is_great" To learn more about writing tests, see: https://docs.djangoproject.com/en/1.9/topics/testing/overview/#writing-tests @@ -51,7 +51,7 @@ To learn more about writing tests, see: """ -class {{app_class_name}}TestCase(TethysTestCase): +class {{class_name}}TestCase(TethysTestCase): """ In this class you may define as many functions as you'd like to test different aspects of your app. Each function must start with the word "test" for it to be recognized and executed during testing. @@ -65,7 +65,7 @@ class {{app_class_name}}TestCase(TethysTestCase): place that code here. For example, if you are testing against any persistent stores, you should call the test database creation function here, like so: - self.create_test_persistent_stores_for_app({{app_class_name}}) + self.create_test_persistent_stores_for_app({{class_name}}) If you are testing against a controller that check for certain user info, you can create a fake test user and get a test client, like so: @@ -95,7 +95,7 @@ class {{app_class_name}}TestCase(TethysTestCase): that took place before execution of the test functions. If you are testing against any persistent stores, you should call the test database destruction function from here, like so: - self.destroy_test_persistent_stores_for_app({{app_class_name}}) + self.destroy_test_persistent_stores_for_app({{class_name}}) NOTE: You do not have to set these functions up here, but if they are not placed here and are needed then they must be placed at the very end of your individual test functions. Also, if certain @@ -114,17 +114,17 @@ class {{app_class_name}}TestCase(TethysTestCase): It is required that the function name begins with the word "test" or it will not be executed. Generally, the code written here will consist of many assert methods. A list of assert methods is included here for reference or to get you started: - assertEqual(a, b) a == b - assertNotEqual(a, b) a != b - assertTrue(x) bool(x) is True - assertFalse(x) bool(x) is False - assertIs(a, b) a is b - assertIsNot(a, b) a is not b - assertIsNone(x) x is None - assertIsNotNone(x) x is not None - assertIn(a, b) a in b - assertNotIn(a, b) a not in b - assertIsInstance(a, b) isinstance(a, b) + assertEqual(a, b) a == b + assertNotEqual(a, b) a != b + assertTrue(x) bool(x) is True + assertFalse(x) bool(x) is False + assertIs(a, b) a is b + assertIsNot(a, b) a is not b + assertIsNone(x) x is None + assertIsNotNone(x) x is not None + assertIn(a, b) a in b + assertNotIn(a, b) a not in b + assertIsInstance(a, b) isinstance(a, b) assertNotIsInstance(a, b) !isinstance(a, b) Learn more about assert methods here: https://docs.python.org/2.7/library/unittest.html#assert-methods diff --git a/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/__init__.py b/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/__init__.py index 62a094218..2e2033b3c 100644 --- a/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/__init__.py +++ b/tethys_apps/cli/scaffold_templates/app_templates/default/tethysapp/__init__.py @@ -4,4 +4,4 @@ pkg_resources.declare_namespace(__name__) except ImportError: import pkgutil - __path__ = pkgutil.extend_path(__path__, __name__) \ No newline at end of file + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/.gitignore b/tethys_apps/cli/scaffold_templates/extension_templates/default/.gitignore new file mode 100644 index 000000000..b0419c5d9 --- /dev/null +++ b/tethys_apps/cli/scaffold_templates/extension_templates/default/.gitignore @@ -0,0 +1,9 @@ +*.pydevproject +*.project +*.egg-info +*.class +*.pyo +*.pyc +*.db +*.sqlite +*.DS_Store \ No newline at end of file diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/setup.py_tmpl b/tethys_apps/cli/scaffold_templates/extension_templates/default/setup.py_tmpl new file mode 100644 index 000000000..1095ef23f --- /dev/null +++ b/tethys_apps/cli/scaffold_templates/extension_templates/default/setup.py_tmpl @@ -0,0 +1,36 @@ +import os +from setuptools import setup, find_packages +from tethys_apps.app_installation import find_resource_files + +# -- Extension Definition -- # +ext_package = '{{project}}' +release_package = 'tethysext-' + ext_package +ext_class = '{{project}}.ext:{{class_name}}' +ext_package_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tethysext', ext_package) + +# -- Python Dependencies -- # +dependencies = [] + +# -- Get Resource File -- # +resource_files = find_resource_files('tethysext/' + ext_package + '/templates') +resource_files += find_resource_files('tethysext/' + ext_package + '/public') + +setup( + name=release_package, + version='0.0.0', + description='{{description|default:''}}', + long_description='', + keywords='', + author='{{author|default:''}}', + author_email='{{author_email|default:''}}', + url='', + license='{{license_name|default:''}}', + packages=find_packages( + exclude=['ez_setup', 'examples', 'tethysext/' + ext_package + '/tests', 'tethysext/' + ext_package + '/tests.*'] + ), + package_data={'': resource_files}, + namespace_packages=['tethysext', 'tethysext.' + ext_package], + include_package_data=True, + zip_safe=False, + install_requires=dependencies, +) diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/__init__.py b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/__init__.py new file mode 100644 index 000000000..2e2033b3c --- /dev/null +++ b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/__init__.py @@ -0,0 +1,7 @@ +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/controllers.py_tmpl b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/controllers.py_tmpl new file mode 100644 index 000000000..e69de29bb diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/ext.py_tmpl b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/ext.py_tmpl new file mode 100644 index 000000000..66f6f25d5 --- /dev/null +++ b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/ext.py_tmpl @@ -0,0 +1,12 @@ +from tethys_sdk.base import TethysExtensionBase + + +class {{class_name}}(TethysExtensionBase): + """ + Tethys extension class for {{proper_name}}. + """ + + name = '{{proper_name}}' + package = '{{project}}' + root_url = '{{project_url}}' + description = '{{description|default:"Place a brief description of your extension here."}}' diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/gizmos/__init__.py b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/gizmos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/model.py b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/model.py new file mode 100644 index 000000000..770e9b4cb --- /dev/null +++ b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/model.py @@ -0,0 +1 @@ +# Put your persistent store models in this file diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/public/css/main.css b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/public/css/main.css new file mode 100644 index 000000000..e69de29bb diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/public/js/main.js b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/public/js/main.js new file mode 100644 index 000000000..e69de29bb diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/templates/+project+/.gitkeep b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/templates/+project+/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/templates/gizmos/.gitkeep b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/templates/gizmos/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/tests/__init__.py b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/tests/tests.py_tmpl b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/tests/tests.py_tmpl new file mode 100644 index 000000000..46d9d4aa3 --- /dev/null +++ b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/+project+/tests/tests.py_tmpl @@ -0,0 +1,155 @@ +# Most of your test classes should inherit from TethysTestCase +from tethys_sdk.testing import TethysTestCase + +# Use if you'd like a simplified way to test rendered HTML templates. +# You likely need to install BeautifulSoup, as it is not included by default in Tethys Platform +# 1. Open a terminal +# 2. Enter command ". /usr/lib/tethys/bin/activate" to activate the Tethys python environment +# 3. Enter command "pip install beautifulsoup4" +# For help, see https://www.crummy.com/software/BeautifulSoup/bs4/doc/ +# from bs4 import BeautifulSoup + +""" +To run any tests: + 1. Open a terminal + 2. Enter command "t" to activate the Tethys python environment + 3. In settings.py make sure that the tethys_default database user is set to tethys_super or is a super user of the database + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'tethys_default', + 'USER': 'tethys_super', + 'PASSWORD': 'pass', + 'HOST': '127.0.0.1', + 'PORT': '5435' + } + } + 4. Enter tethys test command. + The general form is: "tethys test -f tethysext....." + See below for specific examples + + To run all tests across this extension: + Test command: "tethys test -f tethysext.{{project}}" + + To run all tests in this file: + Test command: "tethys test -f tethysext.{{project}}.tests.tests" + + To run tests in the {{class_name}}TestCase class: + Test command: "tethys test -f tethysext.{{project}}.tests.tests.{{class_name}}TestCase" + + To run only the test_if_tethys_platform_is_great function in the {{class_name}}TestCase class: + Test command: "tethys test -f tethysext.{{project}}.tests.tests.{{class_name}}TestCase.test_if_tethys_platform_is_great" + +To learn more about writing tests, see: + https://docs.djangoproject.com/en/1.9/topics/testing/overview/#writing-tests + https://docs.python.org/2.7/library/unittest.html#module-unittest +""" + + +class {{class_name}}TestCase(TethysTestCase): + """ + In this class you may define as many functions as you'd like to test different aspects of your extension. + Each function must start with the word "test" for it to be recognized and executed during testing. + You could also create multiple TethysTestCase classes within this or other python files to organize your tests. + """ + + def set_up(self): + """ + This function is not required, but can be used if any environmental setup needs to take place before + execution of each test function. Thus, if you have multiple test that require the same setup to run, + place that code here. + + If you are testing against a controller that check for certain user info, you can create a fake test user and + get a test client, like so: + + #The test client simulates a browser that can navigate your extension's url endpoints + self.c = self.get_test_client() + self.user = self.create_test_user(username="joe", password="secret", email="joe@some_site.com") + # To create a super_user, use "self.create_test_superuser(*params)" with the same params + + # To force a login for the test user + self.c.force_login(self.user) + + # If for some reason you do not want to force a login, you can use the following: + login_success = self.c.login(username="joe", password="secret") + + NOTE: You do not have place these functions here, but if they are not placed here and are needed + then they must be placed at the beginning of your individual test functions. Also, if a certain + setup does not apply to all of your functions, you should either place it directly in each + function it applies to, or maybe consider creating a new test file or test class to group similar + tests. + """ + pass + + def tear_down(self): + """ + This function is not required, but should be used if you need to tear down any environmental setup + that took place before execution of the test functions. + + NOTE: You do not have to set these functions up here, but if they are not placed here and are needed + then they must be placed at the very end of your individual test functions. Also, if certain + tearDown code does not apply to all of your functions, you should either place it directly in each + function it applies to, or maybe consider creating a new test file or test class to group similar + tests. + """ + pass + + def is_tethys_platform_great(self): + return True + + def test_if_tethys_platform_is_great(self): + """ + This is an example test function that can be modified to test a specific aspect of your extension. + It is required that the function name begins with the word "test" or it will not be executed. + Generally, the code written here will consist of many assert methods. + A list of assert methods is included here for reference or to get you started: + assertEqual(a, b) a == b + assertNotEqual(a, b) a != b + assertTrue(x) bool(x) is True + assertFalse(x) bool(x) is False + assertIs(a, b) a is b + assertIsNot(a, b) a is not b + assertIsNone(x) x is None + assertIsNotNone(x) x is not None + assertIn(a, b) a in b + assertNotIn(a, b) a not in b + assertIsInstance(a, b) isinstance(a, b) + assertNotIsInstance(a, b) !isinstance(a, b) + Learn more about assert methods here: + https://docs.python.org/2.7/library/unittest.html#assert-methods + """ + + self.assertEqual(self.is_tethys_platform_great(), True) + self.assertNotEqual(self.is_tethys_platform_great(), False) + self.assertTrue(self.is_tethys_platform_great()) + self.assertFalse(not self.is_tethys_platform_great()) + self.assertIs(self.is_tethys_platform_great(), True) + self.assertIsNot(self.is_tethys_platform_great(), False) + + def test_a_controller(self): + """ + This is an example test function of how you might test a controller that returns an HTML template rendered + with context variables. + """ + + # If all test functions were testing controllers or required a test client for another reason, the following + # 3 lines of code could be placed once in the set_up function. Note that in that case, each variable should be + # prepended with "self." (i.e. self.c = ...) to make those variables "global" to this test class and able to be + # used in each separate test function. + c = self.get_test_client() + user = self.create_test_user(username="joe", password="secret", email="joe@some_site.com") + c.force_login(user) + + # Have the test client "browse" to your page + response = c.get('/extensions/{{project_url}}/foo/') # The final '/' is essential for all pages/controllers + + # Test that the request processed correctly (with a 200 status code) + self.assertEqual(response.status_code, 200) + + ''' + NOTE: Next, you would likely test that your context variables returned as expected. That would look + something like the following: + ''' + + context = response.context + self.assertEqual(context['my_integer'], 10) diff --git a/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/__init__.py b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/__init__.py new file mode 100644 index 000000000..2e2033b3c --- /dev/null +++ b/tethys_apps/cli/scaffold_templates/extension_templates/default/tethysext/__init__.py @@ -0,0 +1,7 @@ +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/tethys_apps/cli/scheduler_commands.py b/tethys_apps/cli/scheduler_commands.py new file mode 100644 index 000000000..133763a9a --- /dev/null +++ b/tethys_apps/cli/scheduler_commands.py @@ -0,0 +1,90 @@ +from .cli_colors import FG_RED, FG_GREEN, FG_YELLOW, BOLD, pretty_output +from django.core.exceptions import ObjectDoesNotExist +from builtins import input + + +def scheduler_create_command(args): + from tethys_compute.models import Scheduler + + name = args.name + host = args.endpoint + username = args.username + password = args.password + private_key_path = args.private_key_path + private_key_pass = args.private_key_pass + + existing_scheduler = Scheduler.objects.filter(name=name).first() + if existing_scheduler: + with pretty_output(FG_YELLOW) as p: + p.write('A Scheduler with name "{}" already exists. Command aborted.'.format(name)) + exit(0) + + scheduler = Scheduler( + name=name, + host=host, + username=username, + password=password, + private_key_path=private_key_path, + private_key_pass=private_key_pass + ) + + scheduler.save() + + with pretty_output(FG_GREEN) as p: + p.write('Scheduler created successfully!') + exit(0) + + +def schedulers_list_command(args): + from tethys_compute.models import Scheduler + schedulers = Scheduler.objects.all() + + num_schedulers = len(schedulers) + + if num_schedulers > 0: + with pretty_output(BOLD) as p: + p.write('{0: <30}{1: <25}{2: <10}{3: <10}{4: <50}{5: <10}'.format( + 'Name', 'Host', 'Username', 'Password', 'Private Key Path', 'Private Key Pass' + )) + for scheduler in schedulers: + p.write('{0: <30}{1: <25}{2: <10}{3: <10}{4: <50}{5: <10}'.format( + scheduler.name, scheduler.host, scheduler.username, '******' if scheduler.password else 'None', + scheduler.private_key_path, '******' if scheduler.private_key_pass else 'None' + )) + else: + with pretty_output(BOLD) as p: + p.write('There are no Schedulers registered in Tethys.') + + +def schedulers_remove_command(args): + from tethys_compute.models import Scheduler + scheduler = None + name = args.scheduler_name + force = args.force + + try: + scheduler = Scheduler.objects.get(name=name) + except ObjectDoesNotExist: + with pretty_output(FG_RED) as p: + p.write('Scheduler with name "{}" does not exist.\nCommand aborted.'.format(name)) + exit(0) + + if force: + scheduler.delete() + with pretty_output(FG_GREEN) as p: + p.write('Successfully removed Scheduler "{0}"!'.format(name)) + exit(0) + else: + proceed = input('Are you sure you want to delete this Scheduler? [y/n]: ') + while proceed not in ['y', 'n', 'Y', 'N']: + proceed = input('Please enter either "y" or "n": ') + + if proceed in ['y', 'Y']: + scheduler.delete() + with pretty_output(FG_GREEN) as p: + p.write('Successfully removed Scheduler "{0}"!'.format(name)) + exit(0) + else: + with pretty_output(FG_RED) as p: + p.write('Aborted. Scheduler not removed.') + exit(1) diff --git a/tethys_apps/cli/services_commands.py b/tethys_apps/cli/services_commands.py new file mode 100644 index 000000000..7888949bd --- /dev/null +++ b/tethys_apps/cli/services_commands.py @@ -0,0 +1,228 @@ +from __future__ import print_function +from django.core.exceptions import ObjectDoesNotExist +from django.db.utils import IntegrityError +from django.forms.models import model_to_dict + +from .cli_colors import BOLD, pretty_output, FG_RED, FG_GREEN +from .cli_helpers import add_geoserver_rest_to_endpoint + +from builtins import input + +SERVICES_CREATE = 'create' +SERVICES_CREATE_PERSISTENT = 'persistent' +SERVICES_CREATE_SPATIAL = 'spatial' +SERVICES_LINK = 'link' +SERVICES_LIST = 'list' + + +class FormatError(Exception): + def __init__(self): + Exception.__init__(self) + + +def services_create_persistent_command(args): + """ + Interact with Tethys Services (Spatial/Persistent Stores) to create them and/or link them to existing apps + """ + from tethys_services.models import PersistentStoreService + name = None + + try: + name = args.name + connection = args.connection + parts = connection.split('@') + cred_parts = parts[0].split(':') + store_username = cred_parts[0] + store_password = cred_parts[1] + url_parts = parts[1].split(':') + host = url_parts[0] + port = url_parts[1] + + new_persistent_service = PersistentStoreService(name=name, host=host, port=port, + username=store_username, password=store_password) + new_persistent_service.save() + + with pretty_output(FG_GREEN) as p: + p.write('Successfully created new Persistent Store Service!') + except IndexError: + with pretty_output(FG_RED) as p: + p.write('The connection argument (-c) must be of the form ":@:".') + except IntegrityError: + with pretty_output(FG_RED) as p: + p.write('Persistent Store Service with name "{0}" already exists. Command aborted.'.format(name)) + + +def services_remove_persistent_command(args): + from tethys_services.models import PersistentStoreService + persistent_service_id = None + + try: + persistent_service_id = args.service_uid + force = args.force + + try: + persistent_service_id = int(persistent_service_id) + service = PersistentStoreService.objects.get(pk=persistent_service_id) + except ValueError: + service = PersistentStoreService.objects.get(name=persistent_service_id) + + if force: + service.delete() + with pretty_output(FG_GREEN) as p: + p.write('Successfully removed Persistent Store Service {0}!'.format(persistent_service_id)) + exit(0) + else: + proceed = input('Are you sure you want to delete this Persistent Store Service? [y/n]: ') + while proceed not in ['y', 'n', 'Y', 'N']: + proceed = input('Please enter either "y" or "n": ') + + if proceed in ['y', 'Y']: + service.delete() + with pretty_output(FG_GREEN) as p: + p.write('Successfully removed Persistent Store Service {0}!'.format(persistent_service_id)) + exit(0) + else: + with pretty_output(FG_RED) as p: + p.write('Aborted. Persistent Store Service not removed.') + exit(0) + except ObjectDoesNotExist: + with pretty_output(FG_RED) as p: + p.write('A Persistent Store Service with ID/Name "{0}" does not exist.'.format(persistent_service_id)) + exit(0) + + +def services_create_spatial_command(args): + """ + Interact with Tethys Services (Spatial/Persistent Stores) to create them and/or link them to existing apps + """ + from tethys_services.models import SpatialDatasetService + name = None + + try: + name = args.name + connection = args.connection + parts = connection.split('@') + cred_parts = parts[0].split(':') + service_username = cred_parts[0] + service_password = cred_parts[1] + endpoint = parts[1] + public_endpoint = args.public_endpoint or '' + apikey = args.apikey or '' + + if 'http' not in endpoint or '://' not in endpoint: + raise IndexError() + if public_endpoint and 'http' not in public_endpoint or '://' not in public_endpoint: + raise FormatError() + + endpoint = add_geoserver_rest_to_endpoint(endpoint) + if public_endpoint: + public_endpoint = add_geoserver_rest_to_endpoint(public_endpoint) + + new_persistent_service = SpatialDatasetService(name=name, endpoint=endpoint, public_endpoint=public_endpoint, + apikey=apikey, username=service_username, + password=service_password) + new_persistent_service.save() + + with pretty_output(FG_GREEN) as p: + p.write('Successfully created new Spatial Dataset Service!') + except IndexError: + with pretty_output(FG_RED) as p: + p.write('The connection argument (-c) must be of the form ' + '":@//:".') + except FormatError: + with pretty_output(FG_RED) as p: + p.write('The public_endpoint argument (-p) must be of the form ' + '"//:".') + except IntegrityError: + with pretty_output(FG_RED) as p: + p.write('Spatial Dataset Service with name "{0}" already exists. Command aborted.'.format(name)) + + +def services_remove_spatial_command(args): + from tethys_services.models import SpatialDatasetService + spatial_service_id = None + + try: + spatial_service_id = args.service_uid + force = args.force + + try: + spatial_service_id = int(spatial_service_id) + service = SpatialDatasetService.objects.get(pk=spatial_service_id) + except ValueError: + service = SpatialDatasetService.objects.get(name=spatial_service_id) + + if force: + service.delete() + with pretty_output(FG_GREEN) as p: + p.write('Successfully removed Spatial Dataset Service {0}!'.format(spatial_service_id)) + exit(0) + else: + proceed = input('Are you sure you want to delete this Persistent Store Service? [y/n]: ') + while proceed not in ['y', 'n', 'Y', 'N']: + proceed = input('Please enter either "y" or "n": ') + + if proceed in ['y', 'Y']: + service.delete() + with pretty_output(FG_GREEN) as p: + p.write('Successfully removed Spatial Dataset Service {0}!'.format(spatial_service_id)) + exit(0) + else: + with pretty_output(FG_RED) as p: + p.write('Aborted. Spatial Dataset Service not removed.') + exit(0) + except ObjectDoesNotExist: + with pretty_output(FG_RED) as p: + p.write('A Spatial Dataset Service with ID/Name "{0}" does not exist.'.format(spatial_service_id)) + exit(0) + + +def services_list_command(args): + """ + Interact with Tethys Services (Spatial/Persistent Stores) to create them and/or link them to existing apps + """ + from tethys_services.models import SpatialDatasetService, PersistentStoreService + list_persistent = False + list_spatial = False + + if not args.spatial and not args.persistent: + list_persistent = True + list_spatial = True + elif args.spatial: + list_spatial = True + elif args.persistent: + list_persistent = True + + if list_persistent: + persistent_entries = PersistentStoreService.objects.order_by('id').all() + if len(persistent_entries) > 0: + with pretty_output(BOLD) as p: + p.write('\nPersistent Store Services:') + is_first_entry = True + for entry in persistent_entries: + model_dict = model_to_dict(entry) + if is_first_entry: + with pretty_output(BOLD) as p: + p.write('{0: <3}{1: <50}{2: <25}{3: <6}'.format('ID', 'Name', 'Host', 'Port')) + is_first_entry = False + print('{0: <3}{1: <50}{2: <25}{3: <6}'.format(model_dict['id'], model_dict['name'], + model_dict['host'], model_dict['port'])) + + if list_spatial: + spatial_entries = SpatialDatasetService.objects.order_by('id').all() + if len(spatial_entries) > 0: + with pretty_output(BOLD) as p: + p.write('\nSpatial Dataset Services:') + is_first_entry = True + for entry in spatial_entries: + model_dict = model_to_dict(entry) + if is_first_entry: + with pretty_output(BOLD) as p: + p.write('{0: <3}{1: <50}{2: <50}{3: <50}{4: <30}'.format('ID', 'Name', 'Endpoint', + 'Public Endpoint', 'API Key')) + is_first_entry = False + print('{0: <3}{1: <50}{2: <50}{3: <50}{4: <30}'.format(model_dict['id'], model_dict['name'], + model_dict['endpoint'], + model_dict['public_endpoint'], + model_dict['apikey'] if model_dict['apikey'] + else "None")) diff --git a/tethys_apps/cli/syncstores_command.py b/tethys_apps/cli/syncstores_command.py new file mode 100644 index 000000000..854f59dd8 --- /dev/null +++ b/tethys_apps/cli/syncstores_command.py @@ -0,0 +1,51 @@ +from __future__ import print_function +import subprocess + +from builtins import input + +from tethys_apps.cli.manage_commands import get_manage_path +from tethys_apps.cli.cli_colors import TC_WARNING, TC_ENDC + + +def syncstores_command(args): + """ + Sync persistent stores. + """ + # Get the path to manage.py + manage_path = get_manage_path(args) + + # This command is a wrapper for a custom Django manage.py method called syncstores. + # See tethys_apps.mangement.commands.syncstores + process = ['python', manage_path, 'syncstores'] + + if args.refresh: + valid_inputs = ('y', 'n', 'yes', 'no') + no_inputs = ('n', 'no') + proceed = input('{1}WARNING:{2} You have specified the database refresh option. This will drop all of the ' + 'databases for the following apps: {0}. This could result in significant data loss and ' + 'cannot be undone. Do you wish to continue? (y/n): '.format(', '.join(args.app), + TC_WARNING, + TC_ENDC)).lower() + + while proceed not in valid_inputs: + proceed = input('Invalid option. Do you wish to continue? (y/n): ').lower() + + if proceed not in no_inputs: + process.extend(['-r']) + else: + print('Operation cancelled by user.') + exit(0) + + if args.firsttime: + process.extend(['-f']) + + if args.database: + process.extend(['-d', args.database]) + + if args.app: + process.extend(args.app) + + try: + subprocess.call(process) + except KeyboardInterrupt: + pass diff --git a/tethys_apps/cli/test_command.py b/tethys_apps/cli/test_command.py new file mode 100644 index 000000000..32f03ffce --- /dev/null +++ b/tethys_apps/cli/test_command.py @@ -0,0 +1,73 @@ +import os +import webbrowser + +from tethys_apps.cli.manage_commands import get_manage_path, run_process +from tethys_apps.utilities import get_tethys_src_dir + + +TETHYS_SRC_DIRECTORY = get_tethys_src_dir() + + +def test_command(args): + args.manage = False + # Get the path to manage.py + manage_path = get_manage_path(args) + tests_path = os.path.join(TETHYS_SRC_DIRECTORY, 'tests') + + # Define the process to be run + primary_process = ['python', manage_path, 'test'] + + # Tag to later check if tests are being run on a specific app or extension + app_package_tag = 'tethys_apps.tethysapp.' + extension_package_tag = 'tethysext.' + + if args.coverage or args.coverage_html: + os.environ['TETHYS_TEST_DIR'] = tests_path + if args.file and app_package_tag in args.file: + app_package_parts = args.file.split(app_package_tag) + app_name = app_package_parts[1].split('.')[0] + core_app_package = '{}{}'.format(app_package_tag, app_name) + app_package = 'tethysapp.{}'.format(app_name) + config_opt = '--source={},{}'.format(core_app_package, app_package) + elif args.file and extension_package_tag in args.file: + extension_package_parts = args.file.split(extension_package_tag) + extension_name = extension_package_parts[1].split('.')[0] + core_extension_package = '{}{}'.format(extension_package_tag, extension_name) + extension_package = 'tethysext.{}'.format(extension_name) + config_opt = '--source={},{}'.format(core_extension_package, extension_package) + else: + config_opt = '--rcfile={0}'.format(os.path.join(tests_path, 'coverage.cfg')) + primary_process = ['coverage', 'run', config_opt, manage_path, 'test'] + + if args.file: + primary_process.append(args.file) + elif args.unit: + primary_process.append(os.path.join(tests_path, 'unit_tests')) + elif args.gui: + primary_process.append(os.path.join(tests_path, 'gui_tests')) + + test_status = run_process(primary_process) + + if args.coverage: + if args.file and (app_package_tag in args.file or extension_package_tag in args.file): + run_process(['coverage', 'report']) + else: + run_process(['coverage', 'report', config_opt]) + + if args.coverage_html: + report_dirname = 'coverage_html_report' + index_fname = 'index.html' + + if args.file and (app_package_tag in args.file or extension_package_tag in args.file): + run_process(['coverage', 'html', '--directory={0}'.format(os.path.join(tests_path, report_dirname))]) + else: + run_process(['coverage', 'html', config_opt]) + + try: + status = run_process(['open', os.path.join(tests_path, report_dirname, index_fname)]) + if status != 0: + raise Exception + except Exception: + webbrowser.open_new_tab(os.path.join(tests_path, report_dirname, index_fname)) + + exit(test_status) diff --git a/tethys_apps/cli/uninstall_command.py b/tethys_apps/cli/uninstall_command.py new file mode 100644 index 000000000..e3d556572 --- /dev/null +++ b/tethys_apps/cli/uninstall_command.py @@ -0,0 +1,19 @@ +import subprocess + +from tethys_apps.cli.manage_commands import get_manage_path + + +def uninstall_command(args): + """ + Uninstall an app command. + """ + # Get the path to manage.py + manage_path = get_manage_path(args) + item_name = args.app_or_extension + process = ['python', manage_path, 'tethys_app_uninstall', item_name] + if args.is_extension: + process.append('-e') + try: + subprocess.call(process) + except KeyboardInterrupt: + pass diff --git a/tethys_apps/context_processors.py b/tethys_apps/context_processors.py index 5a1c89847..8a7bcb33e 100644 --- a/tethys_apps/context_processors.py +++ b/tethys_apps/context_processors.py @@ -31,7 +31,8 @@ def tethys_apps_context(request): 'icon': app.icon, 'color': app.color, 'tags': app.tags, - 'description': app.description + 'description': app.description, + 'namespace': app.namespace } if hasattr(app, 'feedback_emails') and len(app.feedback_emails) > 0: diff --git a/tethys_apps/decorators.py b/tethys_apps/decorators.py index 0e421bc60..4d4a0789b 100644 --- a/tethys_apps/decorators.py +++ b/tethys_apps/decorators.py @@ -4,22 +4,24 @@ * Author: nswain * Created On: May 09, 2016 * Copyright: (c) Aquaveo 2016 -* License: +* License: ******************************************************************************** """ -try: - from urlparse import urlparse -except ImportError: - from urllib.parse import urlparse +from future.standard_library import install_aliases +from urllib.parse import urlparse + +from django.core.handlers.wsgi import WSGIRequest from django.contrib import messages -from django.core.urlresolvers import reverse +from django.urls import reverse from django.shortcuts import redirect from django.utils.functional import wraps from past.builtins import basestring from tethys_portal.views import error as tethys_portal_error from tethys_apps.base import has_permission +install_aliases() + def permission_required(*args, **kwargs): """ @@ -81,24 +83,38 @@ def my_controller(request): \""" ... - """ + """ # noqa: E501 use_or = kwargs.pop('use_or', False) message = kwargs.pop('message', "We're sorry, but you are not allowed to perform this operation.") raise_exception = kwargs.pop('raise_exception', False) + perms = [arg for arg in args if isinstance(arg, basestring)] - for arg in args: - if not isinstance(arg, basestring): - raise ValueError("Arguments must be a string and the name of a permission for the app.") - - perms = args + if not perms: + raise ValueError('Must supply at least one permission to test.') def decorator(controller_func): - def _wrapped_controller(request, *args, **kwargs): + def _wrapped_controller(*args, **kwargs): # With OR check, we assume the permission test passes upfront + # Find request (varies position if class method is wrapped) + # e.g.: func(request, *args, **kwargs) vs. method(self, request, *args, **kwargs) + request_args_index = None + the_self = None + + for index, arg in enumerate(args): + if isinstance(arg, WSGIRequest): + request_args_index = index + + # Args are everything after the request object + if request_args_index is not None: + request = args[request_args_index] + else: + raise ValueError("No WSGIRequest object provided.") + + if request_args_index > 0: + the_self = args[0] - # Check permission - pass_permission_test = True + args = args[request_args_index+1:] # OR Loop if use_or: @@ -123,7 +139,7 @@ def _wrapped_controller(request, *args, **kwargs): if not pass_permission_test: if not raise_exception: # If user is authenticated... - if request.user.is_authenticated(): + if request.user.is_authenticated: # User feedback messages.add_message(request, messages.WARNING, message) @@ -159,6 +175,12 @@ def _wrapped_controller(request, *args, **kwargs): else: return tethys_portal_error.handler_403(request) - return controller_func(request, *args, **kwargs) + # Call the controller + if the_self is not None: + response = controller_func(the_self, request, *args, **kwargs) + else: + response = controller_func(request, *args, **kwargs) + + return response return wraps(controller_func)(_wrapped_controller) - return decorator \ No newline at end of file + return decorator diff --git a/tethys_apps/exceptions.py b/tethys_apps/exceptions.py index a550498ac..fa5e5a742 100644 --- a/tethys_apps/exceptions.py +++ b/tethys_apps/exceptions.py @@ -10,7 +10,10 @@ class TethysAppSettingDoesNotExist(Exception): - pass + def __init__(self, setting_type, setting_name, app_name, *args, **kwargs): + msg = 'A {0} named "{1}" does not exist in the {2} app.'\ + .format(setting_type, setting_name, app_name.encode('utf-8')) + super(TethysAppSettingDoesNotExist, self).__init__(msg, *args, **kwargs) class TethysAppSettingNotAssigned(Exception): diff --git a/tethys_apps/harvester.py b/tethys_apps/harvester.py new file mode 100644 index 000000000..802c23455 --- /dev/null +++ b/tethys_apps/harvester.py @@ -0,0 +1,277 @@ +""" +******************************************************************************** +* Name: app_harvester.py +* Author: Nathan Swain and Scott Christensen +* Created On: August 19, 2013 +* Copyright: (c) Brigham Young University 2013 +* License: BSD 2-Clause +******************************************************************************** +""" +from __future__ import (absolute_import, division, + print_function, unicode_literals) +from builtins import * # noqa: F401, F403 + +import os +import sys +import inspect +import logging +import pkgutil + +from django.db.utils import ProgrammingError +from django.core.exceptions import ObjectDoesNotExist +from tethys_apps.base import TethysAppBase, TethysExtensionBase +from tethys_apps.base.testing.environment import is_testing_environment + +tethys_log = logging.getLogger('tethys.' + __name__) + + +class SingletonHarvester(object): + """ + Collects information for initiating apps + """ + extensions = [] + extension_modules = {} + apps = [] + _instance = None + BLUE = '\033[94m' + GREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + + def harvest(self): + """ + Harvest apps and extensions. + """ + if sys.version_info.major == 2: + print(self.WARNING + 'WARNING: Support for Python 2 is deprecated ' + 'and will be dropped in Tethys version 3.0.' + self.ENDC) + + self.harvest_extensions() + self.harvest_apps() + + def harvest_extensions(self): + """ + Searches for and loads Tethys extensions. + """ + try: + if not is_testing_environment(): + print(self.BLUE + 'Loading Tethys Extensions...' + self.ENDC) + + import tethysext + tethys_extensions = dict() + for _, modname, ispkg in pkgutil.iter_modules(tethysext.__path__): + if ispkg: + tethys_extensions[modname] = 'tethysext.{}'.format(modname) + + self._harvest_extension_instances(tethys_extensions) + except Exception: + '''DO NOTHING''' + + def harvest_apps(self): + """ + Searches the apps package for apps + """ + # Notify user harvesting is taking place + if not is_testing_environment(): + print(self.BLUE + 'Loading Tethys Apps...' + self.ENDC) + + # List the apps packages in directory + apps_dir = os.path.join(os.path.dirname(__file__), 'tethysapp') + app_packages_list = [app_package for app_package in os.listdir(apps_dir) if app_package != '__pycache__'] + + # Harvest App Instances + self._harvest_app_instances(app_packages_list) + + def get_url_patterns(self): + """ + Generate the url pattern lists for each app and namespace them accordingly. + """ + app_url_patterns = dict() + extension_url_patterns = dict() + + for app in self.apps: + app_url_patterns.update(app.url_patterns) + + for extension in self.extensions: + extension_url_patterns.update(extension.url_patterns) + + return app_url_patterns, extension_url_patterns + + def __new__(cls): + """ + Make App Harvester a Singleton + """ + if not cls._instance: + cls._instance = super(SingletonHarvester, cls).__new__(cls) + + return cls._instance + + @staticmethod + def _validate_extension(extension): + """ + Validate the given extension. + Args: + extension(module_obj): ext module object of the Tethys extension. + + Returns: + module_obj or None: returns validated module object or None if not valid. + """ + return extension + + @staticmethod + def _validate_app(app): + """ + Validate the app data that needs to be validated. Returns either the app if valid or None if not valid. + """ + # Remove prepended slash if included + if app.icon != '' and app.icon[0] == '/': + app.icon = app.icon[1:] + + # Validate color + if app.color != '' and app.color[0] != '#': + # Add hash + app.color = '#{0}'.format(app.color) + + # Must be 6 or 3 digit hex color (7 or 4 with hash symbol) + if len(app.color) != 7 and len(app.color) != 4: + app.color = '' + + return app + + def _harvest_extension_instances(self, extension_packages): + """ + Locate the extension class, instantiate it, and save for later use. + + Arg: + extension_packages(dict): Dictionary where keys are the name of the extension and value is the extension package module object. + """ # noqa:E501 + valid_ext_instances = [] + valid_extension_modules = {} + loaded_extensions = [] + + for extension_name, extension_package in extension_packages.items(): + + try: + # Import the "ext" module from the extension package + ext_module = __import__(extension_package + ".ext", fromlist=['']) + + # Retrieve the members of the ext_module and iterate through + # them to find the the class that inherits from TethysExtensionBase. + for name, obj in inspect.getmembers(ext_module): + try: + # issubclass() will fail if obj is not a class + if (issubclass(obj, TethysExtensionBase)) and (obj is not TethysExtensionBase): + # Assign a handle to the class + ExtensionClass = getattr(ext_module, name) + + # Instantiate app and validate + ext_instance = ExtensionClass() + validated_ext_instance = self._validate_extension(ext_instance) + + # sync app with Tethys db + ext_instance.sync_with_tethys_db() + + # compile valid apps + if validated_ext_instance: + valid_ext_instances.append(validated_ext_instance) + valid_extension_modules[extension_name] = extension_package + + # Notify user that the app has been loaded + loaded_extensions.append(extension_name) + + # We found the extension class so we're done + break + + except TypeError: + continue + except Exception: + tethys_log.exception( + 'Extension {0} not loaded because of the following error:'.format(extension_package)) + continue + + # Save valid apps + self.extensions = valid_ext_instances + self.extension_modules = valid_extension_modules + + # Update user + if not is_testing_environment(): + print(self.BLUE + 'Tethys Extensions Loaded: ' + + self.ENDC + '{0}'.format(', '.join(loaded_extensions)) + '\n') + + def _harvest_app_instances(self, app_packages_list): + """ + Search each app package for the app.py module. Find the AppBase class in the app.py + module and instantiate it. Save the list of instantiated AppBase classes. + """ + valid_app_instance_list = [] + loaded_apps = [] + + for app_package in app_packages_list: + # Skip these things + if app_package in ['__init__.py', '__init__.pyc', '.gitignore', '.DS_Store']: + continue + + # Create the path to the app module in the custom app package + app_module_name = '.'.join(['tethys_apps.tethysapp', app_package, 'app']) + + try: + # Import the app.py module from the custom app package programmatically + # (e.g.: apps.apps..app) + app_module = __import__(app_module_name, fromlist=['']) + + for name, obj in inspect.getmembers(app_module): + # Retrieve the members of the app_module and iterate through + # them to find the the class that inherits from AppBase. + try: + # issubclass() will fail if obj is not a class + if (issubclass(obj, TethysAppBase)) and (obj is not TethysAppBase): + # Assign a handle to the class + AppClass = getattr(app_module, name) + + # Instantiate app and validate + app_instance = AppClass() + validated_app_instance = self._validate_app(app_instance) + + # sync app with Tethys db + app_instance.sync_with_tethys_db() + + # load/validate app url patterns + try: + app_instance.url_patterns + except Exception: + tethys_log.exception( + 'App {0} not loaded because of an issue with loading urls:'.format(app_package)) + app_instance.remove_from_db() + continue + + # register app permissions + try: + app_instance.register_app_permissions() + except (ProgrammingError, ObjectDoesNotExist) as e: + tethys_log.warning(e) + + # compile valid apps + if validated_app_instance: + valid_app_instance_list.append(validated_app_instance) + + # Notify user that the app has been loaded + loaded_apps.append(app_package) + + # We found the app class so we're done + break + + except TypeError: + continue + except Exception: + tethys_log.exception( + 'App {0} not loaded because of the following error:'.format(app_package)) + continue + + # Save valid apps + self.apps = valid_app_instance_list + + # Update user + if not is_testing_environment(): + print(self.BLUE + 'Tethys Apps Loaded: ' + + self.ENDC + '{0}'.format(', '.join(loaded_apps)) + '\n') diff --git a/tethys_apps/helpers.py b/tethys_apps/helpers.py index ca95aa697..28ecbce59 100644 --- a/tethys_apps/helpers.py +++ b/tethys_apps/helpers.py @@ -8,6 +8,7 @@ ******************************************************************************** """ import os +from tethys_apps.harvester import SingletonHarvester def get_tethysapp_dir(): @@ -32,4 +33,22 @@ def get_installed_tethys_apps(): if os.path.isdir(item_path): tethys_apps[item] = item_path - return tethys_apps \ No newline at end of file + return tethys_apps + + +def get_installed_tethys_extensions(): + """ + Get a list of installed extensions + """ + harvester = SingletonHarvester() + install_extensions = harvester.extension_modules + extension_paths = {} + + for extension_name, extension_module in install_extensions.items(): + try: + extension = __import__(extension_module, fromlist=['']) + extension_paths[extension_name] = extension.__path__[0] + except (IndexError, ImportError): + '''DO NOTHING''' + + return extension_paths diff --git a/tethys_apps/management/__init__.py b/tethys_apps/management/__init__.py index b5eeb48b2..3ce78d55d 100644 --- a/tethys_apps/management/__init__.py +++ b/tethys_apps/management/__init__.py @@ -6,4 +6,4 @@ * Copyright: (c) Brigham Young University 2014 * License: BSD 2-Clause ******************************************************************************** -""" \ No newline at end of file +""" diff --git a/tethys_apps/management/commands/__init__.py b/tethys_apps/management/commands/__init__.py index 1b643a91c..2facb9e16 100644 --- a/tethys_apps/management/commands/__init__.py +++ b/tethys_apps/management/commands/__init__.py @@ -6,4 +6,4 @@ * Copyright: (c) Brigham Young University 2015 * License: BSD 2-Clause ******************************************************************************** -""" \ No newline at end of file +""" diff --git a/tethys_apps/management/commands/collectworkspaces.py b/tethys_apps/management/commands/collectworkspaces.py index 77b100a28..a918a4ac0 100644 --- a/tethys_apps/management/commands/collectworkspaces.py +++ b/tethys_apps/management/commands/collectworkspaces.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ +from __future__ import print_function import os import shutil @@ -18,18 +19,26 @@ class Command(BaseCommand): """ - Command class that handles the syncstores command. Provides persistent store management functionality. + Command class that handles the collectworkspaces command. """ + def add_arguments(self, parser): + parser.add_argument('-f', '--force', action='store_true', default=False, + help='Force the overwrite the app directory into its collected-to location.') + def handle(self, *args, **options): """ - Symbolically link the static directories of each app into the static/public directory specified by the STATIC_ROOT - parameter of the settings.py. Do this prior to running Django's collectstatic method. + Symbolically link the static directories of each app into the static/public directory specified by the + STATIC_ROOT parameter of the settings.py. Do this prior to running Django's collectstatic method. """ - if not hasattr(settings, 'TETHYS_WORKSPACES_ROOT') or (hasattr(settings, 'TETHYS_WORKSPACES_ROOT') and not settings.TETHYS_WORKSPACES_ROOT): + if not hasattr(settings, 'TETHYS_WORKSPACES_ROOT') or (hasattr(settings, 'TETHYS_WORKSPACES_ROOT') + and not settings.TETHYS_WORKSPACES_ROOT): print('WARNING: Cannot find the TETHYS_WORKSPACES_ROOT setting in the settings.py file. ' - 'Please provide the path to the static directory using the TETHYS_WORKSPACES_ROOT setting and try again.') + 'Please provide the path to the static directory using the TETHYS_WORKSPACES_ROOT ' + 'setting and try again.') exit(1) + # Get optional force arg + force = options['force'] # Read settings workspaces_root = settings.TETHYS_WORKSPACES_ROOT @@ -42,28 +51,38 @@ def handle(self, *args, **options): for app, path in installed_apps.items(): # Check for both variants of the static directory (public and static) - workspaces_path = os.path.join(path, 'workspaces') - workspaces_root_path = os.path.join(workspaces_root, app) + app_ws_path = os.path.join(path, 'workspaces') + tethys_ws_root_path = os.path.join(workspaces_root, app) # Only perform if workspaces_path is a directory - if os.path.isdir(workspaces_path) and not os.path.islink(workspaces_path): - # Clear out old symbolic links/directories in workspace root if necessary - try: - # Remove link - os.remove(workspaces_root_path) - except OSError: - try: - # Remove directory - shutil.rmtree(workspaces_root_path) - except OSError: - # No file - pass + if not os.path.isdir(app_ws_path): + print('WARNING: The workspace_path for app "{}" is not a directory. Skipping...'.format(app)) + continue - # Move the directory to workspace root path - shutil.move(workspaces_path, workspaces_root_path) + if not os.path.islink(app_ws_path): + if not os.path.exists(tethys_ws_root_path): + # Move the directory to workspace root path + shutil.move(app_ws_path, tethys_ws_root_path) + else: + if force: + # Clear out old symbolic links/directories in workspace root if necessary + try: + # Remove link + os.remove(tethys_ws_root_path) + except OSError: + shutil.rmtree(tethys_ws_root_path, ignore_errors=True) - # Create appropriate symbolic link - if os.path.isdir(workspaces_root_path): - os.symlink(workspaces_root_path, workspaces_path) - print('INFO: Successfully linked "workspaces" directory to TETHYS_WORKSPACES_ROOT for app "{0}".'.format(app)) + # Move the directory to workspace root path + shutil.move(app_ws_path, tethys_ws_root_path) + else: + print('WARNING: Workspace directory for app "{}" already exists in the TETHYS_WORKSPACES_ROOT ' + 'directory. A symbolic link is being created to the existing directory. ' + 'To force overwrite ' + 'the existing directory, re-run the command with the "-f" argument.'.format(app)) + shutil.rmtree(app_ws_path, ignore_errors=True) + # Create appropriate symbolic link + if os.path.isdir(tethys_ws_root_path): + os.symlink(tethys_ws_root_path, app_ws_path) + print('INFO: Successfully linked "workspaces" directory to TETHYS_WORKSPACES_ROOT for app ' + '"{0}".'.format(app)) diff --git a/tethys_apps/management/commands/pre_collectstatic.py b/tethys_apps/management/commands/pre_collectstatic.py index abf46fc13..224e9f78b 100644 --- a/tethys_apps/management/commands/pre_collectstatic.py +++ b/tethys_apps/management/commands/pre_collectstatic.py @@ -7,13 +7,14 @@ * License: BSD 2-Clause ******************************************************************************** """ +from __future__ import print_function import os import shutil from django.core.management.base import BaseCommand from django.conf import settings -from tethys_apps.helpers import get_installed_tethys_apps +from tethys_apps.helpers import get_installed_tethys_apps, get_installed_tethys_extensions class Command(BaseCommand): @@ -25,7 +26,7 @@ def handle(self, *args, **options): """ Symbolically link the static directories of each app into the static/public directory specified by the STATIC_ROOT parameter of the settings.py. Do this prior to running Django's collectstatic method. - """ + """ # noqa: E501 if not settings.STATIC_ROOT: print('WARNING: Cannot find the STATIC_ROOT setting in the settings.py file. ' 'Please provide the path to the static directory using the STATIC_ROOT setting and try again.') @@ -35,16 +36,17 @@ def handle(self, *args, **options): static_root = settings.STATIC_ROOT # Get a list of installed apps - installed_apps = get_installed_tethys_apps() + installed_apps_and_extensions = get_installed_tethys_apps() + installed_apps_and_extensions.update(get_installed_tethys_extensions()) # Provide feedback to user - print('INFO: Linking static and public directories of apps to "{0}".'.format(static_root)) + print('INFO: Linking static and public directories of apps and extensions to "{0}".'.format(static_root)) - for app, path in installed_apps.items(): + for item, path in installed_apps_and_extensions.items(): # Check for both variants of the static directory (public and static) public_path = os.path.join(path, 'public') static_path = os.path.join(path, 'static') - static_root_path = os.path.join(static_root, app) + static_root_path = os.path.join(static_root, item) # Clear out old symbolic links/directories if necessary try: @@ -61,9 +63,8 @@ def handle(self, *args, **options): # Create appropriate symbolic link if os.path.isdir(public_path): os.symlink(public_path, static_root_path) - print('INFO: Successfully linked public directory to STATIC_ROOT for app "{0}".'.format(app)) + print('INFO: Successfully linked public directory to STATIC_ROOT for app "{0}".'.format(item)) elif os.path.isdir(static_path): os.symlink(static_path, static_root_path) - print('INFO: Successfully linked static directory to STATIC_ROOT for app "{0}".'.format(app)) - + print('INFO: Successfully linked static directory to STATIC_ROOT for app "{0}".'.format(item)) diff --git a/tethys_apps/management/commands/syncstores.py b/tethys_apps/management/commands/syncstores.py index 2e71266ab..8f2dd4d29 100644 --- a/tethys_apps/management/commands/syncstores.py +++ b/tethys_apps/management/commands/syncstores.py @@ -8,11 +8,14 @@ ******************************************************************************** """ from django.core.management.base import BaseCommand -from tethys_apps.terminal_colors import TerminalColors +from tethys_apps.cli.cli_colors import TC_BLUE, TC_WARNING, TC_ENDC ALL_APPS = 'all' -#TODO: remove syncstores interface and update documentation once able to initialize/create persistent stores from app admin interface +# TODO: remove syncstores interface and update documentation once able to initialize/create persistent stores from app +# admin interface + + class Command(BaseCommand): """ Command class that handles the syncstores command. Provides persistent store management functionality. @@ -58,11 +61,12 @@ def provision_persistent_stores(self, app_names, options): target_app_names = [a.package for a in target_apps] for app_name in app_names: if app_name not in target_app_names: - self.stdout.write('{0}WARNING:{1} The app named "{2}" cannot be found. Please make sure it is installed ' - 'and try again.'.format(TerminalColors.WARNING, TerminalColors.ENDC, app_name)) + self.stdout.write('{0}WARNING:{1} The app named "{2}" cannot be found. ' + 'Please make sure it is installed ' + 'and try again.'.format(TC_WARNING, TC_ENDC, app_name)) # Notify user of database provisioning - self.stdout.write(TerminalColors.BLUE + '\nProvisioning Persistent Stores...' + TerminalColors.ENDC) + self.stdout.write(TC_BLUE + '\nProvisioning Persistent Stores...' + TC_ENDC) # Get apps and provision persistent stores if not already created for app in target_apps: diff --git a/tethys_apps/management/commands/tethys_app_uninstall.py b/tethys_apps/management/commands/tethys_app_uninstall.py index d9b9b1451..6b316377a 100644 --- a/tethys_apps/management/commands/tethys_app_uninstall.py +++ b/tethys_apps/management/commands/tethys_app_uninstall.py @@ -1,6 +1,6 @@ """ ******************************************************************************** -* Name: collectworkspaces.py +* Name: tethys_app_uninstall.py * Author: Nathan Swain * Created On: August 6, 2015 * Copyright: (c) Brigham Young University 2015 @@ -9,10 +9,14 @@ """ import os import shutil +import site import subprocess +import warnings from django.core.management.base import BaseCommand -from tethys_apps.helpers import get_installed_tethys_apps +from tethys_apps.helpers import get_installed_tethys_apps, get_installed_tethys_extensions + +from builtins import input class Command(BaseCommand): @@ -20,58 +24,85 @@ class Command(BaseCommand): Command class that handles the uninstall command for uninstall Tethys apps. """ def add_arguments(self, parser): - parser.add_argument('app_name', nargs='+', type=str) + parser.add_argument('app_or_extension', nargs='+', type=str) + parser.add_argument('-e', '--extension', dest='is_extension', default=False, action='store_true') def handle(self, *args, **options): """ Remove the app from disk and in the database """ - PREFIX = 'tethysapp' - app_name = options['app_name'][0] - installed_apps = get_installed_tethys_apps() + from tethys_apps.models import TethysApp, TethysExtension + app_or_extension = "App" if not options['is_extension'] else 'Extension' + PREFIX = 'tethysapp' if not options['is_extension'] else 'tethysext' + item_name = options['app_or_extension'][0] + + # Check for app files installed + installed_items = get_installed_tethys_extensions() if options['is_extension'] else get_installed_tethys_apps() - if PREFIX in app_name: + if PREFIX in item_name: prefix_length = len(PREFIX) + 1 - app_name = app_name[prefix_length:] + item_name = item_name[prefix_length:] - if app_name not in installed_apps: - self.stdout.write('WARNING: App with name "{0}" cannot be uninstalled, because it is not installed.'.format(app_name)) - exit(0) + module_found = True + if item_name not in installed_items: + module_found = False + + # Check for app/extension in database + TethysModel = TethysApp if not options['is_extension'] else TethysExtension + db_app = None + db_found = True + + try: + db_app = TethysModel.objects.get(package=item_name) + except TethysModel.DoesNotExist: + db_found = False - app_with_prefix = '{0}-{1}'.format(PREFIX, app_name) + if not module_found and not db_found: + warnings.warn('WARNING: {0} with name "{1}" cannot be uninstalled, because it is not installed or' + ' not an {0}.'.format(app_or_extension, item_name)) + exit(0) # Confirm + item_with_prefix = '{0}-{1}'.format(PREFIX, item_name) + valid_inputs = ('y', 'n', 'yes', 'no') no_inputs = ('n', 'no') - overwrite_input = raw_input('Are you sure you want to uninstall "{0}"? (y/n): '.format(app_with_prefix)).lower() + overwrite_input = input('Are you sure you want to uninstall "{0}"? (y/n): '.format(item_with_prefix)).lower() while overwrite_input not in valid_inputs: - overwrite_input = raw_input('Invalid option. Are you sure you want to ' - 'uninstall "{0}"? (y/n): '.format(app_with_prefix)).lower() + overwrite_input = input('Invalid option. Are you sure you want to ' + 'uninstall "{0}"? (y/n): '.format(item_with_prefix)).lower() if overwrite_input in no_inputs: self.stdout.write('Uninstall cancelled by user.') exit(0) # Remove app from database - from tethys_apps.models import TethysApp - db_app = TethysApp.objects.get(package=app_name) - db_app.delete() + if db_found and db_app: + db_app.delete() - try: - # Remove directory - shutil.rmtree(installed_apps[app_name]) - except OSError: - # Remove symbolic link - os.remove(installed_apps[app_name]) + if module_found and not options['is_extension']: + try: + # Remove directory + shutil.rmtree(installed_items[item_name]) + except OSError: + # Remove symbolic link + os.remove(installed_items[item_name]) # Uninstall using pip - process = ['pip', 'uninstall', '-y', '{0}-{1}'.format(PREFIX, app_name)] + process = ['pip', 'uninstall', '-y', '{0}-{1}'.format(PREFIX, item_name)] try: subprocess.Popen(process, stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0] except KeyboardInterrupt: pass - self.stdout.write('App "{0}" successfully uninstalled.'.format(app_with_prefix)) + # Remove the namespace package file if applicable. + for site_package in site.getsitepackages(): + try: + os.remove(os.path.join(site_package, "{}-{}-nspkg.pth".format(PREFIX, item_name.replace('_', '-')))) + except Exception: + continue + + self.stdout.write('{} "{}" successfully uninstalled.'.format(app_or_extension, item_with_prefix)) diff --git a/tethys_apps/migrations/0001_initial.py b/tethys_apps/migrations/0001_initial.py deleted file mode 100644 index 7243dafef..000000000 --- a/tethys_apps/migrations/0001_initial.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.5 on 2016-05-11 17:10 -from __future__ import unicode_literals - -from django.db import migrations, models -import tethys_compute.utilities - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='TethysApp', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('package', models.CharField(default=b'', max_length=200, unique=True)), - ('name', models.CharField(default=b'', max_length=200)), - ('description', models.TextField(blank=True, default=b'', max_length=1000)), - ('enable_feedback', models.BooleanField(default=False)), - ('feedback_emails', tethys_compute.utilities.ListField(blank=True, default=b'')), - ('index', models.CharField(default=b'', max_length=200)), - ('icon', models.CharField(default=b'', max_length=200)), - ('root_url', models.CharField(default=b'', max_length=200)), - ('color', models.CharField(default=b'', max_length=10)), - ('enabled', models.BooleanField(default=True)), - ('show_in_apps_library', models.BooleanField(default=True)), - ], - options={ - 'verbose_name': 'Tethys App', - 'verbose_name_plural': 'Installed Apps', - 'permissions': (('view_app', 'Can see app in library'), ('access_app', 'Can access app')), - }, - ), - ] diff --git a/tethys_apps/migrations/0001_initial_20.py b/tethys_apps/migrations/0001_initial_20.py index f20d515d8..a58f880ba 100644 --- a/tethys_apps/migrations/0001_initial_20.py +++ b/tethys_apps/migrations/0001_initial_20.py @@ -9,31 +9,29 @@ class Migration(migrations.Migration): - replaces = [(b'tethys_apps', '0001_initial'), (b'tethys_apps', '0002_tethysapp_tags'), (b'tethys_apps', '0003_auto_20170505_0350')] - initial = True - # dependencies = [ - # ('tethys_services', '0009_persistentstoreservice'), - # ] + dependencies = [ + ('tethys_services', '0001_initial_20'), + ] operations = [ migrations.CreateModel( name='TethysApp', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('package', models.CharField(default=b'', max_length=200, unique=True)), - ('name', models.CharField(default=b'', max_length=200)), - ('description', models.TextField(blank=True, default=b'', max_length=1000)), + ('package', models.CharField(default='', max_length=200, unique=True)), + ('name', models.CharField(default='', max_length=200)), + ('description', models.TextField(blank=True, default='', max_length=1000)), ('enable_feedback', models.BooleanField(default=False)), - ('feedback_emails', tethys_compute.utilities.ListField(blank=True, default=b'')), - ('index', models.CharField(default=b'', max_length=200)), - ('icon', models.CharField(default=b'', max_length=200)), - ('root_url', models.CharField(default=b'', max_length=200)), - ('color', models.CharField(default=b'', max_length=10)), + ('feedback_emails', tethys_compute.utilities.ListField(blank=True, default='')), + ('index', models.CharField(default='', max_length=200)), + ('icon', models.CharField(default='', max_length=200)), + ('root_url', models.CharField(default='', max_length=200)), + ('color', models.CharField(default='', max_length=10)), ('enabled', models.BooleanField(default=True)), ('show_in_apps_library', models.BooleanField(default=True)), - ('tags', models.CharField(blank=True, default=b'', max_length=200)), + ('tags', models.CharField(blank=True, default='', max_length=200)), ], options={ 'verbose_name': 'Tethys App', @@ -45,69 +43,106 @@ class Migration(migrations.Migration): name='TethysAppSetting', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(default=b'', max_length=200)), - ('description', models.TextField(blank=True, default=b'', max_length=1000)), + ('name', models.CharField(default='', max_length=200)), + ('description', models.TextField(blank=True, default='', max_length=1000)), ('required', models.BooleanField(default=True)), - ('initializer', models.CharField(default=b'', max_length=1000)), + ('initializer', models.CharField(default='', max_length=1000)), ('initialized', models.BooleanField(default=False)), ], ), migrations.CreateModel( name='CustomSetting', fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), + ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='tethys_apps.TethysAppSetting')), ('value', models.CharField(blank=True, max_length=1024)), - ('type', models.CharField(choices=[(b'STRING', b'String'), (b'INTEGER', b'Integer'), (b'FLOAT', b'Float'), (b'BOOLEAN', b'Boolean')], default=b'STRING', max_length=200)), + ('type', models.CharField(choices=[('STRING', 'String'), ('INTEGER', 'Integer'), ('FLOAT', 'Float'), + ('BOOLEAN', 'Boolean')], default='STRING', max_length=200)), ], bases=('tethys_apps.tethysappsetting',), ), migrations.CreateModel( name='DatasetServiceSetting', fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), - ('engine', models.CharField(choices=[(b'tethys_dataset_services.engines.CkanDatasetEngine', b'CKAN'), (b'tethys_dataset_services.engines.HydroShareDatasetEngine', b'HydroShare')], default=b'tethys_dataset_services.engines.CkanDatasetEngine', max_length=200)), - ('dataset_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tethys_services.DatasetService')), + ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, + serialize=False, to='tethys_apps.TethysAppSetting')), + ('engine', models.CharField(choices=[('tethys_dataset_services.engines.CkanDatasetEngine', 'CKAN'), + ('tethys_dataset_services.engines.HydroShareDatasetEngine', + 'HydroShare')], + default='tethys_dataset_services.engines.CkanDatasetEngine', + max_length=200)), + ('dataset_service', models.ForeignKey(blank=True, null=True, + on_delete=django.db.models.deletion.CASCADE, + to='tethys_services.DatasetService')), ], bases=('tethys_apps.tethysappsetting',), ), migrations.CreateModel( name='PersistentStoreConnectionSetting', fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), - ('persistent_store_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tethys_services.PersistentStoreService')), + ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='tethys_apps.TethysAppSetting')), + ('persistent_store_service', models.ForeignKey(blank=True, null=True, + on_delete=django.db.models.deletion.CASCADE, + to='tethys_services.PersistentStoreService')), ], bases=('tethys_apps.tethysappsetting',), ), migrations.CreateModel( name='PersistentStoreDatabaseSetting', fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), + ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='tethys_apps.TethysAppSetting')), ('spatial', models.BooleanField(default=False)), ('dynamic', models.BooleanField(default=False)), - ('persistent_store_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tethys_services.PersistentStoreService')), + ('persistent_store_service', models.ForeignKey(blank=True, null=True, + on_delete=django.db.models.deletion.CASCADE, + to='tethys_services.PersistentStoreService')), ], bases=('tethys_apps.tethysappsetting',), ), migrations.CreateModel( name='SpatialDatasetServiceSetting', fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), - ('engine', models.CharField(choices=[(b'tethys_dataset_services.engines.GeoServerSpatialDatasetEngine', b'GeoServer')], default=b'tethys_dataset_services.engines.GeoServerSpatialDatasetEngine', max_length=200)), - ('spatial_dataset_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tethys_services.SpatialDatasetService')), + ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='tethys_apps.TethysAppSetting')), + ('engine', models.CharField(choices=[('tethys_dataset_services.engines.GeoServerSpatialDatasetEngine', + 'GeoServer')], + default='tethys_dataset_services.engines.GeoServerSpatialDatasetEngine', + max_length=200)), + ('spatial_dataset_service', models.ForeignKey(blank=True, null=True, + on_delete=django.db.models.deletion.CASCADE, + to='tethys_services.SpatialDatasetService')), ], bases=('tethys_apps.tethysappsetting',), ), migrations.CreateModel( name='WebProcessingServiceSetting', fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), - ('web_processing_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tethys_services.WebProcessingService')), + ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='tethys_apps.TethysAppSetting')), + ('web_processing_service', models.ForeignKey(blank=True, null=True, + on_delete=django.db.models.deletion.CASCADE, + to='tethys_services.WebProcessingService')), ], bases=('tethys_apps.tethysappsetting',), ), migrations.AddField( model_name='tethysappsetting', name='tethys_app', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='settings_set', to='tethys_apps.TethysApp'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='settings_set', + to='tethys_apps.TethysApp'), ), ] diff --git a/tethys_apps/migrations/0002_tethysapp_tags.py b/tethys_apps/migrations/0002_tethysapp_tags.py deleted file mode 100644 index b7e739133..000000000 --- a/tethys_apps/migrations/0002_tethysapp_tags.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.7 on 2016-06-08 18:29 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_apps', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='tethysapp', - name='tags', - field=models.CharField(blank=True, default=b'', max_length=200), - ), - ] diff --git a/tethys_apps/migrations/0002_tethysextension.py b/tethys_apps/migrations/0002_tethysextension.py new file mode 100644 index 000000000..7f92e3831 --- /dev/null +++ b/tethys_apps/migrations/0002_tethysextension.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-04-14 06:16 +from __future__ import unicode_literals + +from django.db import migrations, models +import tethys_apps.base.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('tethys_apps', '0001_initial_20'), + ] + + operations = [ + migrations.CreateModel( + name='TethysExtension', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('package', models.CharField(default='', max_length=200, unique=True)), + ('name', models.CharField(default='', max_length=200)), + ('description', models.TextField(blank=True, default='', max_length=1000)), + ('root_url', models.CharField(default='', max_length=200)), + ('enabled', models.BooleanField(default=True)), + ], + options={ + 'verbose_name': 'Tethys Extension', + 'verbose_name_plural': 'Installed Extensions', + }, + bases=(models.Model, tethys_apps.base.mixins.TethysBaseMixin), + ), + ] diff --git a/tethys_apps/migrations/0003_auto_20170505_0350.py b/tethys_apps/migrations/0003_auto_20170505_0350.py deleted file mode 100644 index f97aa2aeb..000000000 --- a/tethys_apps/migrations/0003_auto_20170505_0350.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.8 on 2017-05-05 03:50 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_services', '0009_persistentstoreservice'), - ('tethys_apps', '0002_tethysapp_tags'), - ] - - operations = [ - migrations.CreateModel( - name='TethysAppSetting', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(default=b'', max_length=200)), - ('description', models.TextField(blank=True, default=b'', max_length=1000)), - ('required', models.BooleanField(default=True)), - ('initializer', models.CharField(default=b'', max_length=1000)), - ('initialized', models.BooleanField(default=False)), - ], - ), - migrations.CreateModel( - name='CustomSetting', - fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), - ('value', models.CharField(blank=True, max_length=1024)), - ('type', models.CharField(choices=[(b'STRING', b'String'), (b'INTEGER', b'Integer'), (b'FLOAT', b'Float'), (b'BOOLEAN', b'Boolean')], default=b'STRING', max_length=200)), - ], - bases=('tethys_apps.tethysappsetting',), - ), - migrations.CreateModel( - name='DatasetServiceSetting', - fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), - ('engine', models.CharField(choices=[(b'tethys_dataset_services.engines.CkanDatasetEngine', b'CKAN'), (b'tethys_dataset_services.engines.HydroShareDatasetEngine', b'HydroShare')], default=b'tethys_dataset_services.engines.CkanDatasetEngine', max_length=200)), - ('dataset_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tethys_services.DatasetService')), - ], - bases=('tethys_apps.tethysappsetting',), - ), - migrations.CreateModel( - name='PersistentStoreConnectionSetting', - fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), - ('persistent_store_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tethys_services.PersistentStoreService')), - ], - bases=('tethys_apps.tethysappsetting',), - ), - migrations.CreateModel( - name='PersistentStoreDatabaseSetting', - fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), - ('spatial', models.BooleanField(default=False)), - ('dynamic', models.BooleanField(default=False)), - ('persistent_store_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tethys_services.PersistentStoreService')), - ], - bases=('tethys_apps.tethysappsetting',), - ), - migrations.CreateModel( - name='SpatialDatasetServiceSetting', - fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), - ('engine', models.CharField(choices=[(b'tethys_dataset_services.engines.GeoServerSpatialDatasetEngine', b'GeoServer')], default=b'tethys_dataset_services.engines.GeoServerSpatialDatasetEngine', max_length=200)), - ('spatial_dataset_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tethys_services.SpatialDatasetService')), - ], - bases=('tethys_apps.tethysappsetting',), - ), - migrations.CreateModel( - name='WebProcessingServiceSetting', - fields=[ - ('tethysappsetting_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_apps.TethysAppSetting')), - ('web_processing_service', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tethys_services.WebProcessingService')), - ], - bases=('tethys_apps.tethysappsetting',), - ), - migrations.AddField( - model_name='tethysappsetting', - name='tethys_app', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='settings_set', to='tethys_apps.TethysApp'), - ), - ] diff --git a/tethys_apps/migrations/0003_python3_compatibility.py b/tethys_apps/migrations/0003_python3_compatibility.py new file mode 100644 index 000000000..63e04a295 --- /dev/null +++ b/tethys_apps/migrations/0003_python3_compatibility.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.15 on 2018-11-13 17:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tethys_apps', '0002_tethysextension'), + ] + + operations = [ + migrations.AlterField( + model_name='customsetting', + name='value', + field=models.CharField(blank=True, default='', max_length=1024), + ), + ] diff --git a/tethys_apps/models.py b/tethys_apps/models.py index 0834765ff..da7c664a0 100644 --- a/tethys_apps/models.py +++ b/tethys_apps/models.py @@ -7,7 +7,6 @@ * License: BSD 2-Clause ******************************************************************************** """ -from builtins import str as text import sqlalchemy import logging from django.db import models @@ -17,18 +16,20 @@ PersistentStoreInitializerError from tethys_compute.utilities import ListField from sqlalchemy.orm import sessionmaker +from tethys_apps.base.mixins import TethysBaseMixin +from tethys_sdk.testing import is_testing_environment, get_test_db_name + +from tethys_apps.base.function_extractor import TethysFunctionExtractor +log = logging.getLogger('tethys') try: from tethys_services.models import (DatasetService, SpatialDatasetService, WebProcessingService, PersistentStoreService) -except RuntimeError as e: - print(e) - - -from tethys_apps.base.function_extractor import TethysFunctionExtractor +except RuntimeError: + log.exception('An error occurred while trying to import tethys service models.') -class TethysApp(models.Model): +class TethysApp(models.Model, TethysBaseMixin): """ DB Model for Tethys Apps """ @@ -40,7 +41,7 @@ class TethysApp(models.Model): description = models.TextField(max_length=1000, blank=True, default='') enable_feedback = models.BooleanField(default=False) feedback_emails = ListField(default='', blank=True) - tags = models.CharField(max_length=200, blank=True, default='') + tags = models.CharField(max_length=200, blank=True, default='') # Developer first attributes index = models.CharField(max_length=200, default='') @@ -60,8 +61,8 @@ class Meta: verbose_name = 'Tethys App' verbose_name_plural = 'Installed Apps' - def __unicode__(self): - return text(self.name) + def __str__(self): + return self.name def add_settings(self, setting_list): """ @@ -84,22 +85,22 @@ def settings(self): @property def custom_settings(self): return self.settings_set.exclude(customsetting__isnull=True) \ - .select_subclasses('customsetting') + .select_subclasses('customsetting') @property def dataset_service_settings(self): return self.settings_set.exclude(datasetservicesetting__isnull=True) \ - .select_subclasses('datasetservicesetting') + .select_subclasses('datasetservicesetting') @property def spatial_dataset_service_settings(self): return self.settings_set.exclude(spatialdatasetservicesetting__isnull=True) \ - .select_subclasses('spatialdatasetservicesetting') + .select_subclasses('spatialdatasetservicesetting') @property def wps_services_settings(self): return self.settings_set.exclude(webprocessingservicesetting__isnull=True) \ - .select_subclasses('webprocessingservicesetting') + .select_subclasses('webprocessingservicesetting') @property def persistent_store_connection_settings(self): @@ -111,6 +112,42 @@ def persistent_store_database_settings(self): return self.settings_set.exclude(persistentstoredatabasesetting__isnull=True) \ .select_subclasses('persistentstoredatabasesetting') + @property + def configured(self): + required_settings = [s for s in self.settings if s.required] + for setting in required_settings: + try: + if setting.get_value() is None: + return False + except TethysAppSettingNotAssigned: + return False + return True + + +class TethysExtension(models.Model, TethysBaseMixin): + """ + DB Model for Tethys Extension + """ + # The package is enforced to be unique by the file system + package = models.CharField(max_length=200, unique=True, default='') + + # Portal admin first attributes + name = models.CharField(max_length=200, default='') + description = models.TextField(max_length=1000, blank=True, default='') + + # Developer first attributes + root_url = models.CharField(max_length=200, default='') + + # Portal admin only attributes + enabled = models.BooleanField(default=True) + + class Meta: + verbose_name = 'Tethys Extension' + verbose_name_plural = 'Installed Extensions' + + def __str__(self): + return self.name + class TethysAppSetting(models.Model): """ @@ -126,7 +163,7 @@ class TethysAppSetting(models.Model): initializer = models.CharField(max_length=1000, default='') initialized = models.BooleanField(default=False) - def __unicode__(self): + def __str__(self): return self.name @property @@ -147,6 +184,9 @@ def initialize(self): self.initializer_function(self.initialized) self.initialized = True + def get_value(self, *args, **kwargs): + raise NotImplementedError() + class CustomSetting(TethysAppSetting): """ @@ -192,7 +232,7 @@ class CustomSetting(TethysAppSetting): required=True ) - """ + """ # noqa: E501 TYPE_STRING = 'STRING' TYPE_INTEGER = 'INTEGER' TYPE_FLOAT = 'FLOAT' @@ -206,7 +246,7 @@ class CustomSetting(TethysAppSetting): (TYPE_FLOAT, 'Float'), (TYPE_BOOLEAN, 'Boolean'), ) - value = models.CharField(max_length=1024, blank=True) + value = models.CharField(max_length=1024, blank=True, default='') type = models.CharField(max_length=200, choices=TYPE_CHOICES, default=TYPE_STRING) def clean(self): @@ -219,13 +259,13 @@ def clean(self): if self.value != '' and self.type == self.TYPE_FLOAT: try: float(self.value) - except: + except Exception: raise ValidationError('Value must be a float.') elif self.value != '' and self.type == self.TYPE_INTEGER: try: int(self.value) - except: + except Exception: raise ValidationError('Value must be an integer.') elif self.value != '' and self.type == self.TYPE_BOOLEAN: @@ -236,15 +276,19 @@ def get_value(self): """ Get the value, automatically casting it to the correct type. """ - if self.value == '': - return None - elif self.type == self.TYPE_STRING: + if self.value == '' or self.value is None: + return None # TODO Why don't we raise a NotAssigned error here? + + if self.type == self.TYPE_STRING: return self.value - elif self.type == self.TYPE_FLOAT: + + if self.type == self.TYPE_FLOAT: return float(self.value) - elif self.type == self.TYPE_INTEGER: + + if self.type == self.TYPE_INTEGER: return int(self.value) - elif self.type == self.TYPE_BOOLEAN: + + if self.type == self.TYPE_BOOLEAN: return self.value.lower() in self.TRUTHY_BOOL_STRINGS @@ -294,6 +338,23 @@ def clean(self): if not self.dataset_service and self.required: raise ValidationError('Required.') + def get_value(self, as_public_endpoint=False, as_endpoint=False, as_engine=False): + + if not self.dataset_service: + return None # TODO Why don't we raise a NotAssigned error here? + + # TODO order here manters. Is this the order we want? + if as_engine: + return self.dataset_service.get_engine() + + if as_endpoint: + return self.dataset_service.endpoint + + if as_public_endpoint: + return self.dataset_service.public_endpoint + + return self.dataset_service + class SpatialDatasetServiceSetting(TethysAppSetting): """ @@ -333,6 +394,30 @@ def clean(self): if not self.spatial_dataset_service and self.required: raise ValidationError('Required.') + def get_value(self, as_public_endpoint=False, as_endpoint=False, as_wms=False, + as_wfs=False, as_engine=False): + + if not self.spatial_dataset_service: + return None # TODO Why don't we raise a NotAssigned error here? + + # TODO order here manters. Is this the order we want? + if as_engine: + return self.spatial_dataset_service.get_engine() + + if as_wms: + return self.spatial_dataset_service.endpoint.split('/rest')[0] + '/wms' + + if as_wfs: + return self.spatial_dataset_service.endpoint.split('/rest')[0] + '/ows' + + if as_endpoint: + return self.spatial_dataset_service.endpoint + + if as_public_endpoint: + return self.spatial_dataset_service.public_endpoint + + return self.spatial_dataset_service + class WebProcessingServiceSetting(TethysAppSetting): """ @@ -365,6 +450,24 @@ def clean(self): if not self.web_processing_service and self.required: raise ValidationError('Required.') + def get_value(self, as_public_endpoint=False, as_endpoint=False, as_engine=False): + wps_service = self.web_processing_service + + if not wps_service: + return None # TODO Why don't we raise a NotAssigned error here? + + # TODO order here manters. Is this the order we want? + if as_engine: + return wps_service.get_engine() + + if as_endpoint: + return wps_service.endpoint + + if as_public_endpoint: + return wps_service.public_endpoint + + return wps_service + class PersistentStoreConnectionSetting(TethysAppSetting): """ @@ -397,7 +500,7 @@ def clean(self): if not self.persistent_store_service and self.required: raise ValidationError('Required.') - def get_engine(self, as_url=False, as_sessionmaker=False): + def get_value(self, as_url=False, as_sessionmaker=False, as_engine=False): """ Get the SQLAlchemy engine from the connected persistent store service """ @@ -408,6 +511,9 @@ def get_engine(self, as_url=False, as_sessionmaker=False): raise TethysAppSettingNotAssigned('Cannot create engine or url for PersistentStoreConnection "{0}" for app ' '"{1}": no PersistentStoreService found.'.format(self.name, self.tethys_app.package)) + # Order matters here. Think carefully before changing... + if as_engine: + return ps_service.get_engine() if as_sessionmaker: return sessionmaker(bind=ps_service.get_engine()) @@ -415,7 +521,7 @@ def get_engine(self, as_url=False, as_sessionmaker=False): if as_url: return ps_service.get_url() - return ps_service.get_engine() + return ps_service class PersistentStoreDatabaseSetting(TethysAppSetting): @@ -472,17 +578,16 @@ def get_namespaced_persistent_store_name(self): """ Return the namespaced persistent store database name (e.g. my_first_app_db). """ - from django.conf import settings # Convert name given by user to database safe name safe_name = self.name.lower().replace(' ', '_') # If testing environment, the engine for the "test" version of the persistent store should be fetched - if hasattr(settings, 'TESTING') and settings.TESTING: - safe_name = 'test_{0}'.format(safe_name) + if is_testing_environment(): + safe_name = get_test_db_name(safe_name) return '_'.join((self.tethys_app.package, safe_name)) - def get_engine(self, with_db=False, as_url=False, as_sessionmaker=False): + def get_value(self, with_db=False, as_url=False, as_sessionmaker=False, as_engine=False): """ Get the SQLAlchemy engine from the connected persistent store service """ @@ -493,24 +598,27 @@ def get_engine(self, with_db=False, as_url=False, as_sessionmaker=False): raise TethysAppSettingNotAssigned('Cannot create engine or url for PersistentStoreDatabase "{0}" for app ' '"{1}": no PersistentStoreService found.'.format(self.name, self.tethys_app.package)) - if with_db: ps_service.database = self.get_namespaced_persistent_store_name() + # Order matters here. Think carefully before changing... + if as_engine: + return ps_service.get_engine() + if as_sessionmaker: return sessionmaker(bind=ps_service.get_engine()) if as_url: return ps_service.get_url() - return ps_service.get_engine() + return ps_service def persistent_store_database_exists(self): """ Returns True if the persistent store database exists. """ # Get the database engine - engine = self.get_engine() + engine = self.get_value(as_engine=True) namespaced_name = self.get_namespaced_persistent_store_name() # Cannot create databases in a transaction: connect and commit to close transaction @@ -548,15 +656,15 @@ def drop_persistent_store_database(self): )) # Get the database engine - engine = self.get_engine() + engine = self.get_value(as_engine=True) # Connection - drop_connection = engine.connect() + drop_connection = None namespaced_ps_name = self.get_namespaced_persistent_store_name() # Drop db - drop_db_statement = 'DROP DATABASE IF EXISTS {0}'.format(namespaced_ps_name) + drop_db_statement = 'DROP DATABASE IF EXISTS "{0}"'.format(namespaced_ps_name) try: drop_connection = engine.connect() @@ -571,16 +679,16 @@ def drop_persistent_store_database(self): WHERE pg_stat_activity.datname = '{0}' AND pg_stat_activity.pid <> pg_backend_pid(); '''.format(namespaced_ps_name) - drop_connection.execute(disconnect_sessions_statement) + if drop_connection: + drop_connection.execute(disconnect_sessions_statement) - # Try again to drop the database - drop_connection.execute('commit') - drop_connection.execute(drop_db_statement) - drop_connection.close() + # Try again to drop the database + drop_connection.execute('commit') + drop_connection.execute(drop_db_statement) else: raise e finally: - drop_connection.close() + drop_connection and drop_connection.close() def create_persistent_store_database(self, refresh=False, force_first_time=False): """ @@ -590,8 +698,8 @@ def create_persistent_store_database(self, refresh=False, force_first_time=False log = logging.getLogger('tethys') # Connection engine - url = self.get_engine(as_url=True) - engine = self.get_engine() + url = self.get_value(as_url=True) + engine = self.get_value(as_engine=True) namespaced_ps_name = self.get_namespaced_persistent_store_name() db_exists = self.persistent_store_database_exists() @@ -619,7 +727,7 @@ def create_persistent_store_database(self, refresh=False, force_first_time=False # Create db create_db_statement = ''' - CREATE DATABASE {0} + CREATE DATABASE "{0}" WITH OWNER {1} TEMPLATE template0 ENCODING 'UTF8' @@ -629,6 +737,7 @@ def create_persistent_store_database(self, refresh=False, force_first_time=False create_connection.execute('commit') try: create_connection.execute(create_db_statement) + except sqlalchemy.exc.ProgrammingError: raise PersistentStorePermissionError('Database user "{0}" has insufficient permissions to create ' 'the persistent store database "{1}": must have CREATE DATABASES ' @@ -641,7 +750,7 @@ def create_persistent_store_database(self, refresh=False, force_first_time=False # -------------------------------------------------------------------------------------------------------------# if self.spatial: # Connect to new database - new_db_engine = self.get_engine(with_db=True) + new_db_engine = self.get_value(with_db=True, as_engine=True) new_db_connection = new_db_engine.connect() # Notify user @@ -673,11 +782,10 @@ def create_persistent_store_database(self, refresh=False, force_first_time=False )) try: if force_first_time: - self.initializer_function(self.get_engine(with_db=True), True) + self.initializer_function(self.get_value(with_db=True, as_engine=True), True) else: - self.initializer_function(self.get_engine(with_db=True), not self.initialized) + self.initializer_function(self.get_value(with_db=True, as_engine=True), not self.initialized) except Exception as e: - print(type(e)) raise PersistentStoreInitializerError(e) # Update initialization diff --git a/tethys_apps/static/tethys_apps/css/app_library.css b/tethys_apps/static/tethys_apps/css/app_library.css index 849ce94a0..3a615f83e 100644 --- a/tethys_apps/static/tethys_apps/css/app_library.css +++ b/tethys_apps/static/tethys_apps/css/app_library.css @@ -131,6 +131,9 @@ body { border-radius: 5px; box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); } +#app-list .app-container.unconfigured { + background: #666666; +} #app-list .app-container .app-icon img { width: 170px; margin: 15px; @@ -147,6 +150,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +#app-list .app-container.unconfigured .app-title { + color: #ffffff; +} #app-list .app-container .app-help-icon { position: absolute; bottom: 2px; diff --git a/tethys_apps/static_finders.py b/tethys_apps/static_finders.py new file mode 100644 index 000000000..7ca38fae6 --- /dev/null +++ b/tethys_apps/static_finders.py @@ -0,0 +1,73 @@ +""" +******************************************************************************** +* Name: static_finders.py +* Author: nswain +* Created On: February 21, 2018 +* Copyright: +* License: +******************************************************************************** +""" +import os +from collections import OrderedDict as SortedDict +from django.contrib.staticfiles import utils +from django.contrib.staticfiles.finders import BaseFinder +from django.core.files.storage import FileSystemStorage +from django.utils._os import safe_join +from tethys_apps.utilities import get_directories_in_tethys + + +class TethysStaticFinder(BaseFinder): + """ + A static files finder that looks in each app in the tethysapp directory for static files. + This finder search for static files in a directory called 'public' or 'static'. + """ + + def __init__(self, apps=None, *args, **kwargs): + # List of locations with static files + self.locations = get_directories_in_tethys(('static', 'public'), with_app_name=True) + + # Maps dir paths to an appropriate storage instance + self.storages = SortedDict() + + for prefix, root in self.locations: + filesystem_storage = FileSystemStorage(location=root) + filesystem_storage.prefix = prefix + self.storages[root] = filesystem_storage + + super(TethysStaticFinder, self).__init__(*args, **kwargs) + + def find(self, path, all=False): + """ + Looks for files in the Tethys apps static or public directories + """ + matches = [] + for prefix, root in self.locations: + matched_path = self.find_location(root, path, prefix) + if matched_path: + if not all: + return matched_path + matches.append(matched_path) + return matches + + def find_location(self, root, path, prefix=None): + """ + Finds a requested static file in a location, returning the found + absolute path (or ``None`` if no match). + """ + if prefix: + prefix = '%s%s' % (prefix, os.sep) + if not path.startswith(prefix): + return None + path = path[len(prefix):] + path = safe_join(root, path) + if os.path.exists(path): + return path + + def list(self, ignore_patterns): + """ + List all files in all locations. + """ + for prefix, root in self.locations: + storage = self.storages[root] + for path in utils.get_files(storage, ignore_patterns): + yield path, storage diff --git a/tethys_apps/template_loaders.py b/tethys_apps/template_loaders.py index 2b00caa0a..f2f61f0db 100644 --- a/tethys_apps/template_loaders.py +++ b/tethys_apps/template_loaders.py @@ -4,7 +4,7 @@ * Author: swainn * Created On: December 14, 2015 * Copyright: (c) Aquaveo 2015 -* License: +* License: ******************************************************************************** """ import io @@ -15,10 +15,10 @@ from django.template.loaders.base import Loader as BaseLoader from django.utils._os import safe_join -from tethys_apps.utilities import get_directories_in_tethys_apps +from tethys_apps.utilities import get_directories_in_tethys -class TethysAppsTemplateLoader(BaseLoader): +class TethysTemplateLoader(BaseLoader): """ Custom Django template loader for tethys apps """ @@ -42,7 +42,7 @@ def get_template_sources(self, template_name, template_dirs=None): one of the template_dirs it is excluded from the result set. """ if not template_dirs: - template_dirs = get_directories_in_tethys_apps(('templates',)) + template_dirs = get_directories_in_tethys(('templates',)) for template_dir in template_dirs: try: name = safe_join(template_dir, template_name) @@ -56,28 +56,3 @@ def get_template_sources(self, template_name, template_dirs=None): template_name=template_name, loader=self, ) - -# -# def tethys_apps_template_loader(template_name, template_dirs=None): -# """ -# Custom Django template loader for tethys apps -# """ -# # Search for the template in the list of template directories -# tethysapp_template_dirs = get_directories_in_tethys_apps(('templates',)) -# -# template = None -# -# for template_dir in tethysapp_template_dirs: -# template_path = safe_join(template_dir, template_name) -# -# try: -# template = open(template_path).read(), template_name -# break -# except IOError: -# pass -# -# # If the template is still None, raise the exception -# if not template: -# raise TemplateDoesNotExist(template_name) -# -# return template \ No newline at end of file diff --git a/tethys_apps/templates/tethys_apps/app_base.html b/tethys_apps/templates/tethys_apps/app_base.html index 101979c4b..3283e30ff 100644 --- a/tethys_apps/templates/tethys_apps/app_base.html +++ b/tethys_apps/templates/tethys_apps/app_base.html @@ -57,7 +57,7 @@ {% endcomment %} {% block links %} - {% if site_globals.favicon %}{% endif %} + {% if site_globals.favicon %}{% endif %} {% endblock %} {% comment "import_gizmos explanation" %} diff --git a/tethys_apps/templates/tethys_apps/app_library.html b/tethys_apps/templates/tethys_apps/app_library.html index 03728c638..c14a76daf 100644 --- a/tethys_apps/templates/tethys_apps/app_library.html +++ b/tethys_apps/templates/tethys_apps/app_library.html @@ -24,7 +24,7 @@
    - {% if apps %} + {% if apps.configured or apps.unconfigured %}
    @@ -45,7 +45,7 @@
    - {% for app in apps %} + {% for app in apps.configured %} {% if app.show_in_apps_library and app.enabled %} {% else %}

    There are no apps loaded.

    diff --git a/tethys_apps/terminal_colors.py b/tethys_apps/terminal_colors.py index 4d17ce8df..e69de29bb 100644 --- a/tethys_apps/terminal_colors.py +++ b/tethys_apps/terminal_colors.py @@ -1,16 +0,0 @@ -""" -******************************************************************************** -* Name: terminal_colors.py -* Author: Nathan Swain -* Created On: 2014 -* Copyright: (c) Brigham Young University 2014 -* License: BSD 2-Clause -******************************************************************************** -""" - -class TerminalColors: - BLUE = '\033[94m' - GREEN = '\033[92m' - WARNING = '\033[93m' - FAIL = '\033[91m' - ENDC = '\033[0m' \ No newline at end of file diff --git a/tethys_apps/urls.py b/tethys_apps/urls.py index e71044ca6..0b61676b6 100644 --- a/tethys_apps/urls.py +++ b/tethys_apps/urls.py @@ -7,15 +7,12 @@ * License: BSD 2-Clause ******************************************************************************** """ -from django.db.utils import ProgrammingError -from django.core.exceptions import ObjectDoesNotExist from django.conf.urls import url, include -from tethys_apps.utilities import generate_app_url_patterns, sync_tethys_app_db, register_app_permissions +from tethys_apps.harvester import SingletonHarvester from tethys_apps.views import library, send_beta_feedback_email -from tethys_apps import tethys_log +import logging -# Sync the tethys apps database -sync_tethys_app_db() +tethys_log = logging.getLogger('tethys.' + __name__) urlpatterns = [ url(r'^$', library, name='app_library'), @@ -23,15 +20,15 @@ ] # Append the app urls urlpatterns -app_url_patterns = generate_app_url_patterns() +harvester = SingletonHarvester() +app_url_patterns, extension_url_patterns = harvester.get_url_patterns() for namespace, urls in app_url_patterns.items(): root_pattern = r'^{0}/'.format(namespace.replace('_', '-')) urlpatterns.append(url(root_pattern, include(urls, namespace=namespace))) -# Register permissions here? -try: - register_app_permissions() -except (ProgrammingError, ObjectDoesNotExist) as e: - tethys_log.error(e) +extension_urls = [] +for namespace, urls in extension_url_patterns.items(): + root_pattern = r'^{0}/'.format(namespace.replace('_', '-')) + extension_urls.append(url(root_pattern, include(urls, namespace=namespace))) diff --git a/tethys_apps/utilities.py b/tethys_apps/utilities.py index 73370aa03..0d9138bfe 100644 --- a/tethys_apps/utilities.py +++ b/tethys_apps/utilities.py @@ -7,389 +7,87 @@ * License: BSD 2-Clause ******************************************************************************** """ +from __future__ import print_function +from builtins import input import logging import os -import sys -import traceback -from collections import OrderedDict as SortedDict -from django.conf.urls import url -from django.contrib.staticfiles import utils -from django.contrib.staticfiles.finders import BaseFinder from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned -from django.core.files.storage import FileSystemStorage from django.utils._os import safe_join -from past.builtins import basestring -from tethys_apps import tethys_log -from tethys_apps.app_harvester import SingletonAppHarvester -from tethys_apps.base import permissions -from tethys_apps.models import TethysApp +from tethys_apps.harvester import SingletonHarvester -log = logging.getLogger('tethys.tethys_apps.utilities') +tethys_log = logging.getLogger('tethys.' + __name__) -def register_app_permissions(): +def get_tethys_src_dir(): """ - Register and sync the app permissions. - """ - from guardian.shortcuts import assign_perm, remove_perm, get_perms - from django.contrib.contenttypes.models import ContentType - from django.contrib.auth.models import Permission, Group - - # Get the apps - harvester = SingletonAppHarvester() - apps = harvester.apps - all_app_permissions = {} - all_groups = {} - - for app in apps: - perms = app.permissions() - - # Name spaced prefix for app permissions - # e.g. my_first_app:view_things - # e.g. my_first_app | View things - perm_codename_prefix = app.package + ':' - perm_name_prefix = app.package + ' | ' - - if perms is not None: - # Thing is either a Permission or a PermissionGroup object - - for thing in perms: - # Permission Case - if isinstance(thing, permissions.Permission): - # Name space the permissions and add it to the list - permission_codename = perm_codename_prefix + thing.name - permission_name = perm_name_prefix + thing.description - all_app_permissions[permission_codename] = permission_name - - # PermissionGroup Case - elif isinstance(thing, permissions.PermissionGroup): - # Record in dict of groups - group_permissions = [] - group_name = perm_codename_prefix + thing.name - - for perm in thing.permissions: - # Name space the permissions and add it to the list - permission_codename = perm_codename_prefix + perm.name - permission_name = perm_name_prefix + perm.description - all_app_permissions[permission_codename] = permission_name - group_permissions.append(permission_codename) - - # Store all groups for all apps - all_groups[group_name] = {'permissions': group_permissions, 'app_package': app.package} - - # Get the TethysApp content type - tethys_content_type = ContentType.objects.get( - app_label='tethys_apps', - model='tethysapp' - ) - - # Remove any permissions that no longer exist - db_app_permissions = Permission.objects.filter(content_type=tethys_content_type).all() - - for db_app_permission in db_app_permissions: - # Delete the permission if the permission is no longer required by an app - if db_app_permission.codename not in all_app_permissions: - db_app_permission.delete() - - # Create permissions that need to be created - for perm in all_app_permissions: - # Create permission if it doesn't exist - try: - # If permission exists, update it - p = Permission.objects.get(codename=perm) - - p.name = all_app_permissions[perm] - p.content_type = tethys_content_type - p.save() - - except Permission.DoesNotExist: - p = Permission( - name=all_app_permissions[perm], - codename=perm, - content_type=tethys_content_type - ) - p.save() - - # Remove any groups that no longer exist - db_groups = Group.objects.all() - db_apps = TethysApp.objects.all() - db_app_names = [db_app.package for db_app in db_apps] - - for db_group in db_groups: - db_group_name_parts = db_group.name.split(':') - - # Only perform maintenance on groups that belong to Tethys Apps - if (len(db_group_name_parts) > 1) and (db_group_name_parts[0] in db_app_names): - - # Delete groups that is no longer required by an app - if db_group.name not in all_groups: - db_group.delete() - - # Create groups that need to be created - for group in all_groups: - # Look up the app - db_app = TethysApp.objects.get(package=all_groups[group]['app_package']) - - # Create group if it doesn't exist - try: - # If it exists, update the permissions assigned to it - g = Group.objects.get(name=group) - - # Get the permissions for the group and remove all of them - perms = get_perms(g, db_app) + Get/derive the TETHYS_SRC variable. - for p in perms: - remove_perm(p, g, db_app) + Returns: + str: path to TETHYS_SRC. + """ + default = os.path.dirname(os.path.dirname(__file__)) + return os.environ.get('TETHYS_SRC', default) - # Assign the permission to the group and the app instance - for p in all_groups[group]['permissions']: - assign_perm(p, g, db_app) - except Group.DoesNotExist: - # Create a new group - g = Group(name=group) - g.save() +def get_tethys_home_dir(): + """ + Get/derive the TETHYS_HOME variable. - # Assign the permission to the group and the app instance - for p in all_groups[group]['permissions']: - assign_perm(p, g, db_app) + Returns: + str: path to TETHYS_HOME. + """ + default = os.path.dirname(get_tethys_src_dir()) + return os.environ.get('TETHYS_HOME', default) -def generate_app_url_patterns(): +def get_directories_in_tethys(directory_names, with_app_name=False): """ - Generate the url pattern lists for each app and namespace them accordingly. - """ - - # Get controllers list from app harvester - harvester = SingletonAppHarvester() - apps = harvester.apps - app_url_patterns = dict() + # Locate given directories in tethys apps and extensions. + Args: + directory_names: directory to get path to. + with_app_name: inlcud the app name if True. - for app in apps: - if hasattr(app, 'url_maps'): - url_maps = app.url_maps() - elif hasattr(app, 'controllers'): - url_maps = app.controllers() - else: - url_maps = None - - if url_maps: - for url_map in url_maps: - app_root = app.root_url - app_namespace = app_root.replace('-', '_') - - if app_namespace not in app_url_patterns: - app_url_patterns[app_namespace] = [] - - # Create django url object - if isinstance(url_map.controller, basestring): - controller_parts = url_map.controller.split('.') - module_name = '.'.join(controller_parts[:-1]) - function_name = controller_parts[-1] - try: - module = __import__(module_name, fromlist=[function_name]) - except ImportError: - error_msg = 'The following error occurred while trying to import the controller function ' \ - '"{0}":\n {1}'.format(url_map.controller, traceback.format_exc(2)) - log.error(error_msg) - sys.exit(1) - try: - controller_function = getattr(module, function_name) - except AttributeError as e: - error_msg = 'The following error occurred while tyring to access the controller function ' \ - '"{0}":\n {1}'.format(url_map.controller, traceback.format_exc(2)) - log.error(error_msg) - sys.exit(1) - else: - controller_function = url_map.controller - django_url = url(url_map.url, controller_function, name=url_map.name) - - # Append to namespace list - app_url_patterns[app_namespace].append(django_url) - - return app_url_patterns - - -def get_directories_in_tethys_apps(directory_names, with_app_name=False): - # Determine the tethysapp directory + Returns: + list: list of paths to directories in apps and extensions. + """ + # Determine the directories of tethys apps directory tethysapp_dir = safe_join(os.path.abspath(os.path.dirname(__file__)), 'tethysapp') + tethysapp_contents = next(os.walk(tethysapp_dir))[1] + potential_dirs = [safe_join(tethysapp_dir, item) for item in tethysapp_contents] - # Assemble a list of tethysapp directories - tethysapp_contents = os.listdir(tethysapp_dir) - tethysapp_match_dirs = [] + # Determine the directories of tethys extensions + harvester = SingletonHarvester() - for item in tethysapp_contents: - item_path = safe_join(tethysapp_dir, item) - - # Check each directory combination + for _, extension_module in harvester.extension_modules.items(): + try: + extension_module = __import__(extension_module, fromlist=['']) + potential_dirs.append(extension_module.__path__[0]) + except (ImportError, AttributeError, IndexError): + pass + + # Check each directory combination + match_dirs = [] + for potential_dir in potential_dirs: for directory_name in directory_names: # Only check directories - if os.path.isdir(item_path): - match_dir = safe_join(item_path, directory_name) + if os.path.isdir(potential_dir): + match_dir = safe_join(potential_dir, directory_name) - if match_dir not in tethysapp_match_dirs and os.path.isdir(match_dir): + if match_dir not in match_dirs and os.path.isdir(match_dir): if not with_app_name: - tethysapp_match_dirs.append(match_dir) + match_dirs.append(match_dir) else: - tethysapp_match_dirs.append((item, match_dir)) - - return tethysapp_match_dirs + match_dirs.append((os.path.basename(potential_dir), match_dir)) - -class TethysAppsStaticFinder(BaseFinder): - """ - A static files finder that looks in each app in the tethysapp directory for static files. - This finder search for static files in a directory called 'public' or 'static'. - """ - - def __init__(self, apps=None, *args, **kwargs): - # List of locations with static files - self.locations = get_directories_in_tethys_apps(('static', 'public'), with_app_name=True) - - # Maps dir paths to an appropriate storage instance - self.storages = SortedDict() - - for prefix, root in self.locations: - filesystem_storage = FileSystemStorage(location=root) - filesystem_storage.prefix = prefix - self.storages[root] = filesystem_storage - - super(TethysAppsStaticFinder, self).__init__(*args, **kwargs) - - def find(self, path, all=False): - """ - Looks for files in the Tethys apps static or public directories - """ - matches = [] - for prefix, root in self.locations: - matched_path = self.find_location(root, path, prefix) - if matched_path: - if not all: - return matched_path - matches.append(matched_path) - return matches - - def find_location(self, root, path, prefix=None): - """ - Finds a requested static file in a location, returning the found - absolute path (or ``None`` if no match). - """ - if prefix: - prefix = '%s%s' % (prefix, os.sep) - if not path.startswith(prefix): - return None - path = path[len(prefix):] - path = safe_join(root, path) - if os.path.exists(path): - return path - - def list(self, ignore_patterns): - """ - List all files in all locations. - """ - for prefix, root in self.locations: - storage = self.storages[root] - for path in utils.get_files(storage, ignore_patterns): - yield path, storage - - -def sync_tethys_app_db(): - """ - Sync installed apps with database. - """ - from django.conf import settings - - # Get the harvester - harvester = SingletonAppHarvester() - - try: - # Make pass to remove apps that were uninstalled - db_apps = TethysApp.objects.all() - installed_app_packages = [app.package for app in harvester.apps] - - for db_apps in db_apps: - if db_apps.package not in installed_app_packages: - db_apps.delete() - - # Make pass to add apps to db that are newly installed - installed_apps = harvester.apps - - for installed_app in installed_apps: - # Query to see if installed app is in the database - db_apps = TethysApp.objects.\ - filter(package__exact=installed_app.package).\ - all() - - # If the app is not in the database, then add it - if len(db_apps) == 0: - app = TethysApp( - name=installed_app.name, - package=installed_app.package, - description=installed_app.description, - enable_feedback=installed_app.enable_feedback, - feedback_emails=installed_app.feedback_emails, - index=installed_app.index, - icon=installed_app.icon, - root_url=installed_app.root_url, - color=installed_app.color, - tags=installed_app.tags - ) - app.save() - - # custom settings - app.add_settings(installed_app.custom_settings()) - # dataset services settings - app.add_settings(installed_app.dataset_service_settings()) - # spatial dataset services settings - app.add_settings(installed_app.spatial_dataset_service_settings()) - # wps settings - app.add_settings(installed_app.web_processing_service_settings()) - # persistent store settings - app.add_settings(installed_app.persistent_store_settings()) - - app.save() - - # If the app is in the database, update developer-first attributes - elif len(db_apps) == 1: - db_app = db_apps[0] - db_app.index = installed_app.index - db_app.icon = installed_app.icon - db_app.root_url = installed_app.root_url - db_app.color = installed_app.color - db_app.save() - - if hasattr(settings, 'DEBUG') and settings.DEBUG: - db_app.name = installed_app.name - db_app.description = installed_app.description - db_app.tags = installed_app.tags - db_app.enable_feedback = installed_app.enable_feedback - db_app.feedback_emails = installed_app.feedback_emails - db_app.save() - - # custom settings - db_app.add_settings(installed_app.custom_settings()) - # dataset services settings - db_app.add_settings(installed_app.dataset_service_settings()) - # spatial dataset services settings - db_app.add_settings(installed_app.spatial_dataset_service_settings()) - # wps settings - db_app.add_settings(installed_app.web_processing_service_settings()) - # persistent store settings - db_app.add_settings(installed_app.persistent_store_settings()) - db_app.save() - - # More than one instance of the app in db... (what to do here?) - elif len(db_apps) >= 2: - continue - except Exception as e: - log.error(e) + return match_dirs def get_active_app(request=None, url=None): """ Get the active TethysApp object based on the request or URL. """ + from tethys_apps.models import TethysApp apps_root = 'apps' if request is not None: @@ -417,3 +115,176 @@ def get_active_app(request=None, url=None): except MultipleObjectsReturned: tethys_log.warning('Multiple apps found with root url "{0}".'.format(app_root_url)) return app + + +def create_ps_database_setting(app_package, name, description='', required=False, initializer='', initialized=False, + spatial=False, dynamic=False): + from tethys_apps.cli.cli_colors import pretty_output, FG_RED, FG_GREEN + from tethys_apps.models import PersistentStoreDatabaseSetting + from tethys_apps.models import TethysApp + + try: + app = TethysApp.objects.get(package=app_package) + except ObjectDoesNotExist: + with pretty_output(FG_RED) as p: + p.write('A Tethys App with the name "{}" does not exist. Aborted.'.format(app_package)) + return False + + try: + setting = PersistentStoreDatabaseSetting.objects.get(name=name) + if setting: + with pretty_output(FG_RED) as p: + p.write('A PersistentStoreDatabaseSetting with name "{}" already exists. Aborted.'.format(name)) + return False + except ObjectDoesNotExist: + pass + + try: + ps_database_setting = PersistentStoreDatabaseSetting( + tethys_app=app, + name=name, + description=description, + required=required, + initializer=initializer, + initialized=initialized, + spatial=spatial, + dynamic=dynamic + ) + ps_database_setting.save() + with pretty_output(FG_GREEN) as p: + p.write('PersistentStoreDatabaseSetting named "{}" for app "{}" created successfully!'.format(name, + app_package)) + return True + except Exception as e: + print(e) + with pretty_output(FG_RED) as p: + p.write('The above error was encountered. Aborted.'.format(app_package)) + return False + + +def remove_ps_database_setting(app_package, name, force=False): + from tethys_apps.models import TethysApp + from tethys_apps.cli.cli_colors import pretty_output, FG_RED, FG_GREEN + from tethys_apps.models import PersistentStoreDatabaseSetting + + try: + app = TethysApp.objects.get(package=app_package) + except ObjectDoesNotExist: + with pretty_output(FG_RED) as p: + p.write('A Tethys App with the name "{}" does not exist. Aborted.'.format(app_package)) + return False + + try: + setting = PersistentStoreDatabaseSetting.objects.get(tethys_app=app, name=name) + except ObjectDoesNotExist: + with pretty_output(FG_RED) as p: + p.write('An PersistentStoreDatabaseSetting with the name "{}" for app "{}" does not exist. Aborted.' + .format(name, app_package)) + return False + + if not force: + proceed = input('Are you sure you want to delete the ' + 'PersistentStoreDatabaseSetting named "{}"? [y/n]: '.format(name)) + while proceed not in ['y', 'n', 'Y', 'N']: + proceed = input('Please enter either "y" or "n": ') + + if proceed in ['y', 'Y']: + setting.delete() + with pretty_output(FG_GREEN) as p: + p.write('Successfully removed PersistentStoreDatabaseSetting with name "{0}"!'.format(name)) + return True + else: + with pretty_output(FG_RED) as p: + p.write('Aborted. PersistentStoreDatabaseSetting not removed.') + else: + setting.delete() + with pretty_output(FG_GREEN) as p: + p.write('Successfully removed PersistentStoreDatabaseSetting with name "{0}"!'.format(name)) + return True + + +def link_service_to_app_setting(service_type, service_uid, app_package, setting_type, setting_uid): + """ + Links a Tethys Service to a TethysAppSetting. + :param service_type: The type of service being linked to an app. Must be either 'spatial' or 'persistent'. + :param service_uid: The name or id of the service being linked to an app. + :param app_package: The package name of the app whose setting is being linked to a service. + :param setting_type: The type of setting being linked to a service. Must be one of the following: 'ps_database', + 'ps_connection', or 'ds_spatial'. + :param setting_uid: The name or id of the setting being linked to a service. + :return: True if successful, False otherwise. + """ + from tethys_apps.cli.cli_colors import pretty_output, FG_GREEN, FG_RED + from tethys_sdk.app_settings import (SpatialDatasetServiceSetting, PersistentStoreConnectionSetting, + PersistentStoreDatabaseSetting) + from tethys_services.models import (SpatialDatasetService, PersistentStoreService) + from tethys_apps.models import TethysApp + + service_type_to_model_dict = { + 'spatial': SpatialDatasetService, + 'persistent': PersistentStoreService + } + + setting_type_to_link_model_dict = { + 'ps_database': { + 'setting_model': PersistentStoreDatabaseSetting, + 'service_field': 'persistent_store_service' + }, + 'ps_connection': { + 'setting_model': PersistentStoreConnectionSetting, + 'service_field': 'persistent_store_service' + }, + 'ds_spatial': { + 'setting_model': SpatialDatasetServiceSetting, + 'service_field': 'spatial_dataset_service' + } + } + + service_model = service_type_to_model_dict[service_type] + + try: + try: + service_uid = int(service_uid) + service = service_model.objects.get(pk=service_uid) + except ValueError: + service = service_model.objects.get(name=service_uid) + except ObjectDoesNotExist: + with pretty_output(FG_RED) as p: + p.write('A {0} with ID/Name "{1}" does not exist.'.format(str(service_model), service_uid)) + return False + + try: + app = TethysApp.objects.get(package=app_package) + except ObjectDoesNotExist: + with pretty_output(FG_RED) as p: + p.write('A Tethys App with the name "{}" does not exist. Aborted.'.format(app_package)) + return False + + try: + linked_setting_model_dict = setting_type_to_link_model_dict[setting_type] + except KeyError: + with pretty_output(FG_RED) as p: + p.write('The setting_type you specified ("{0}") does not exist.' + '\nChoose from: "ps_database|ps_connection|ds_spatial"'.format(setting_type)) + return False + + linked_setting_model = linked_setting_model_dict['setting_model'] + linked_service_field = linked_setting_model_dict['service_field'] + + try: + try: + setting_uid = int(setting_uid) + setting = linked_setting_model.objects.get(tethys_app=app, pk=setting_uid) + except ValueError: + setting = linked_setting_model.objects.get(tethys_app=app, name=setting_uid) + + setattr(setting, linked_service_field, service) + setting.save() + with pretty_output(FG_GREEN) as p: + p.write('{} with name "{}" was successfully linked to "{}" with name "{}" of the "{}" Tethys App' + .format(str(service_model), service_uid, linked_setting_model, setting_uid, app_package)) + return True + except ObjectDoesNotExist: + with pretty_output(FG_RED) as p: + p.write('A {0} with ID/Name "{1}" does not exist.'.format(str(linked_setting_model), setting_uid)) + return False diff --git a/tethys_apps/views.py b/tethys_apps/views.py index 2ec5decd9..c103089e8 100644 --- a/tethys_apps/views.py +++ b/tethys_apps/views.py @@ -27,8 +27,18 @@ def library(request): # Retrieve the app harvester apps = TethysApp.objects.all() + configured_apps = list() + unconfigured_apps = list() + + for app in apps: + if app.configured: + configured_apps.append(app) + else: + if request.user.is_staff: + unconfigured_apps.append(app) + # Define the context object - context = {'apps': apps} + context = {'apps': {'configured': configured_apps, 'unconfigured': unconfigured_apps}} return render(request, 'tethys_apps/app_library.html', context) @@ -102,7 +112,7 @@ def send_beta_feedback_email(request): send_mail(subject, message, from_email=None, recipient_list=app.feedback_emails) except Exception as e: json = {'success': False, - 'error': 'Failed to send email: ' + e.message} + 'error': 'Failed to send email: ' + str(e)} return JsonResponse(json) json = {'success': True, @@ -118,7 +128,7 @@ def update_job_status(request, job_id): job = TethysJob.objects.filter(id=job_id)[0] job.status json = {'success': True} - except Exception as e: + except Exception: json = {'success': False} return JsonResponse(json) diff --git a/tethys_compute/admin.py b/tethys_compute/admin.py index 5e44f1fde..a154eeefd 100644 --- a/tethys_compute/admin.py +++ b/tethys_compute/admin.py @@ -8,10 +8,7 @@ ******************************************************************************** """ from django.contrib import admin -from django.forms import Textarea -from django.db import models from tethys_compute.models import Scheduler, TethysJob -# Register your models here. @admin.register(Scheduler) @@ -21,8 +18,9 @@ class SchedulerAdmin(admin.ModelAdmin): @admin.register(TethysJob) class JobAdmin(admin.ModelAdmin): - list_display = ['name', 'description', 'label', 'user', 'creation_time', 'execute_time', 'completion_time', 'status'] + list_display = ['name', 'description', 'label', 'user', 'creation_time', 'execute_time', 'completion_time', + 'status'] list_display_links = ('name',) def has_add_permission(self, request): - return False \ No newline at end of file + return False diff --git a/tethys_compute/job_manager.py b/tethys_compute/job_manager.py index 1f098c143..398abd89e 100644 --- a/tethys_compute/job_manager.py +++ b/tethys_compute/job_manager.py @@ -7,12 +7,14 @@ * License: BSD 2-Clause ******************************************************************************** """ +from __future__ import print_function import re from abc import abstractmethod import logging import warnings -from django.core.urlresolvers import reverse + +from django.urls import reverse from tethys_compute.models import (TethysJob, BasicJob, @@ -36,7 +38,7 @@ class JobManager(object): A manager for interacting with the Jobs database providing a simple interface creating and retrieving jobs. Note: - Each app creates its own instance of the JobManager. the ``get_job_manager`` method returns the app. + Each app creates its own instance of the JobManager. The ``get_job_manager`` method returns the app. :: @@ -50,18 +52,11 @@ def __init__(self, app): self.label = app.package self.app_workspace = app.get_app_workspace() self.job_templates = dict() - for template in app.job_templates(): - # TODO remove when JobTemplate is made completely abstract - if template.__class__ == JobTemplate: - msg = 'The job template "{0}" in the app "{1}" uses JobTemplate directly. ' \ - 'This is now depreciated. Please use the job type specific template {2} instead.'\ - .format(template.name, self.app.package, JOB_CAST[template.type].__name__) - warnings.warn(msg, DeprecationWarning) - template.__class__ = JOB_CAST[template.type] - template.__init__(name=template.name, parameters=template.parameters) + templates = app.job_templates() or list() + for template in templates: self.job_templates[template.name] = template - def create_empty_job(self, name, user, job_type, **kwargs): + def create_job(self, name, user, template_name=None, job_type=None, **kwargs): """ Creates a new job from a JobTemplate. @@ -74,14 +69,22 @@ def create_empty_job(self, name, user, job_type, **kwargs): Returns: A new job object of the type specified by job_type. """ - assert issubclass(job_type, TethysJob) + if template_name is not None: + msg = 'The job template "{0}" was used in the "{1}" app. Using job templates is now deprecated.'.format( + template_name, self.app.package + ) + warnings.warn(msg, DeprecationWarning) + print(msg) + return self.old_create_job(name, user, template_name, **kwargs) + + job_type = JOB_TYPES[job_type] user_workspace = self.app.get_user_workspace(user) kwrgs = dict(name=name, user=user, label=self.label, workspace=user_workspace.path) kwrgs.update(kwargs) job = job_type(**kwrgs) return job - def create_job(self, name, user, template_name, **kwargs): + def old_create_job(self, name, user, template_name, **kwargs): """ Creates a new job from a JobTemplate. @@ -96,7 +99,7 @@ def create_job(self, name, user, template_name, **kwargs): """ try: template = self.job_templates[template_name] - except KeyError as e: + except KeyError: raise KeyError('A job template with name %s was not defined' % (template_name,)) user_workspace = self.app.get_user_workspace(user) kwrgs = dict(name=name, user=user, label=self.label, workspace=user_workspace.path) @@ -136,7 +139,7 @@ def get_job(self, job_id, user=None, filters=None): Returns: A instance of a subclass of TethysJob if a job with job_id exists (and was created by user if the user argument is passed in). - """ + """ # noqa: E501 filters = filters or dict() filters['label'] = self.label filters['id'] = job_id @@ -179,7 +182,7 @@ def replace_in_string(string_value): def replace_in_dict(dict_value): new_dict_value = dict() - for key, value in dict_value.iteritems(): + for key, value in dict_value.items(): new_dict_value[key] = replace_in_value(value) return new_dict_value @@ -191,14 +194,15 @@ def replace_in_tuple(tuple_value): new_tuple_value = tuple(replace_in_list(tuple_value)) return new_tuple_value - TYPE_DICT = {str: replace_in_string, - dict: replace_in_dict, - list: replace_in_list, - tuple: replace_in_tuple, - } + TYPE_DICT = { + str: replace_in_string, + dict: replace_in_dict, + list: replace_in_list, + tuple: replace_in_tuple, + } new_parameters = dict() - for parameter, value in parameters.iteritems(): + for parameter, value in parameters.items(): new_value = replace_in_value(value) new_parameters[parameter] = new_value return new_parameters @@ -206,6 +210,7 @@ def replace_in_tuple(tuple_value): class JobTemplate(object): """ + **DEPRECATED** A template from which to create a job. Args: @@ -235,6 +240,7 @@ def create_job(self, app_workspace, user_workspace, **kwargs): class BasicJobTemplate(JobTemplate): """ + **DEPRECATED** A subclass of JobTemplate with the ``type`` argument set to BasicJob. Args: @@ -242,7 +248,7 @@ class BasicJobTemplate(JobTemplate): parameters (dict): A dictionary of parameters to pass to the BasicJob constructor. """ def __init__(self, name, parameters=None): - super(self.__class__, self).__init__(name, JOB_TYPES['BASIC'], parameters) + super(BasicJobTemplate, self).__init__(name, JOB_TYPES['BASIC'], parameters) def process_parameters(self): pass @@ -250,16 +256,13 @@ def process_parameters(self): class CondorJobDescription(object): """ + **DEPRECATED** Helper class for CondorJobTemplate and CondorWorkflowJobTemplates. Stores job attributes. """ def __init__(self, condorpy_template_name=None, remote_input_files=None, **kwargs): self.remote_input_files = remote_input_files - self.attributes = dict() - - if condorpy_template_name: - template = CondorJob.get_condorpy_template(condorpy_template_name) - self.attributes.update(template) - self.attributes.update(kwargs) + self.condorpy_template_name = condorpy_template_name + self.attributes = kwargs def process_attributes(self, app_workspace, user_workspace): self.__dict__ = JobManager._replace_workspaces(self.__dict__, app_workspace, user_workspace) @@ -267,50 +270,30 @@ def process_attributes(self, app_workspace, user_workspace): class CondorJobTemplate(JobTemplate): """ + **DEPRECATED** A subclass of the JobTemplate with the ``type`` argument set to CondorJob. Args: name (str): Name to refer to the template. - parameters (dict, DEPRECATED): A dictionary of key-value pairs. Each Job type defines the possible parameters. job_description (CondorJobDescription): An object containing the attributes for the condorpy job. scheduler (Scheduler): An object containing the connection information to submit the condorpy job remotely. """ - def __init__(self, name, parameters=None, job_description=None, scheduler=None, **kwargs): - parameters = parameters or dict() + def __init__(self, name, job_description, scheduler=None, **kwargs): + parameters = dict() parameters['scheduler'] = scheduler - # TODO job_description will be required when parameters is fully deprecated - if job_description: - parameters['remote_input_files'] = job_description.remote_input_files - parameters['attributes'] = job_description.attributes - else: - msg = 'The job_description argument was not defined in the job_template {0}. ' \ - 'This argument will be required in version 1.5 of Tethys.'.format(name) - warnings.warn(msg, DeprecationWarning) + parameters['remote_input_files'] = job_description.remote_input_files + parameters['condorpy_template_name'] = job_description.condorpy_template_name + parameters['attributes'] = job_description.attributes parameters.update(kwargs) - super(self.__class__, self).__init__(name, JOB_TYPES['CONDORJOB'], parameters) + super(CondorJobTemplate, self).__init__(name, JOB_TYPES['CONDORJOB'], parameters) def process_parameters(self): - attributes = dict() - - def update_attribute(attribute_name): - if attribute_name in self.parameters: - attribute = self.parameters.pop(attribute_name) - attributes[attribute_name] = attribute - - if 'condorpy_template_name' in self.parameters: - template_name = self.parameters.pop('condorpy_template_name') - template = CondorJob.get_condorpy_template(template_name) - attributes.update(template) - if 'attributes' in self.parameters: - attributes.update(self.parameters.pop('attributes')) - for attribute_name in ['executable']: - update_attribute(attribute_name) - - self.parameters['_attributes'] = attributes + pass class CondorWorkflowTemplate(JobTemplate): """ + **DEPRECATED** A subclass of the JobTemplate with the ``type`` argument set to CondorWorkflow. Args: @@ -319,14 +302,15 @@ class CondorWorkflowTemplate(JobTemplate): jobs (list): A list of CondorWorkflowJobTemplates. max_jobs (dict, optional): A dictionary of category-max_job pairs defining the maximum number of jobs that will run simultaneously from each category. config (str, optional): A path to a configuration file for the condorpy DAG. - """ - def __init__(self, name, parameters=None, jobs=None, max_jobs=None, config=None, **kwargs): + """ # noqa: E501 + def __init__(self, name, parameters=None, jobs=None, max_jobs=None, config='', **kwargs): parameters = parameters or dict() + jobs = jobs or list() self.node_templates = set(jobs) - parameters['_max_jobs'] = max_jobs - parameters['_config'] = config + parameters['max_jobs'] = max_jobs + parameters['config'] = config parameters.update(kwargs) - super(self.__class__, self).__init__(name, JOB_TYPES['CONDORWORKFLOW'], parameters) + super(CondorWorkflowTemplate, self).__init__(name, JOB_TYPES['CONDORWORKFLOW'], parameters) def process_parameters(self): pass @@ -334,7 +318,7 @@ def process_parameters(self): # add methods to workflow to get nodes by name. def create_job(self, app_workspace, user_workspace, **kwargs): - job = super(self.__class__, self).create_job(app_workspace, user_workspace, **kwargs) + job = super(CondorWorkflowTemplate, self).create_job(app_workspace, user_workspace, **kwargs) job.save() node_dict = dict() @@ -354,10 +338,6 @@ def add_to_node_dict(node): # add code to link nodes return job -# TODO remove when JobTemplate is made completely abstract -JOB_CAST = {CondorJob: CondorJobTemplate, - BasicJob: BasicJobTemplate, - } NODE_TYPES = {'JOB': CondorWorkflowJobNode, # 'SUBWWORKFLOW': CondorWorkflowSubworkflowNode, @@ -368,6 +348,7 @@ def add_to_node_dict(node): class CondorWorkflowNodeBaseTemplate(object): """ + **DEPRECATED** A template from which to create a job. Args: @@ -398,39 +379,26 @@ def create_node(self, workflow, app_workspace, user_workspace): kwargs = JobManager._replace_workspaces(self.parameters, app_workspace, user_workspace) if 'parents' in kwargs: kwargs.pop('parents') - node = self.type(name=self.name, - workflow=workflow, - **kwargs) + node = self.type(name=self.name, workflow=workflow, **kwargs) node.save() return node class CondorWorkflowJobTemplate(CondorWorkflowNodeBaseTemplate): """ + **DEPRECATED** A subclass of the CondorWorkflowNodeBaseTemplate with the ``type`` argument set to CondorWorkflowJobNode. Args: name (str): Name to refer to the template. job_description (CondorJobDescription): An instance of `CondorJobDescription` containing of key-value pairs of job attributes. - """ + """ # noqa: E501 def __init__(self, name, job_description, **kwargs): parameters = kwargs parameters['remote_input_files'] = job_description.remote_input_files - parameters['_attributes'] = job_description.attributes - super(self.__class__, self).__init__(name, NODE_TYPES['JOB'], parameters) + parameters['condorpy_template_name'] = job_description.condorpy_template_name + parameters['attributes'] = job_description.attributes + super(CondorWorkflowJobTemplate, self).__init__(name, NODE_TYPES['JOB'], parameters) def process_parameters(self): pass - - -class CondorWorkflowSubworkflowTemplate(CondorWorkflowNodeBaseTemplate): - pass - - -class CondorWorkflowDataJobTemplate(CondorWorkflowNodeBaseTemplate): - pass - - -class CondorWorkflowFinalTemplate(CondorWorkflowNodeBaseTemplate): - pass - diff --git a/tethys_compute/migrations/0001_initial.py b/tethys_compute/migrations/0001_initial.py deleted file mode 100644 index d33a6ab87..000000000 --- a/tethys_compute/migrations/0001_initial.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import datetime -from django.conf import settings -from django.utils.timezone import utc - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Cluster', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('_name', models.CharField(default=b'tethys_default', unique=True, max_length=30)), - ('_size', models.IntegerField(default=1)), - ('_status', models.CharField(default=b'STR', max_length=3, choices=[(b'STR', b'Starting'), (b'RUN', b'Running'), (b'STP', b'Stopped'), (b'UPD', b'Updating'), (b'DEL', b'Deleting'), (b'ERR', b'Error')])), - ('_cloud_provider', models.CharField(default=b'AWS', max_length=3, choices=[(b'AWS', b'Amazon Web Services'), (b'AZR', b'Microsoft Azure')])), - ('_master_image_id', models.CharField(max_length=9, null=True, blank=True)), - ('_node_image_id', models.CharField(max_length=9, null=True, blank=True)), - ('_master_instance_type', models.CharField(max_length=20, null=True, blank=True)), - ('_node_instance_type', models.CharField(max_length=20, null=True, blank=True)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Setting', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.TextField(max_length=30)), - ('content', models.TextField(max_length=500, blank=True)), - ('date_modified', models.DateTimeField(auto_now=True, verbose_name=b'date modified')), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='SettingsCategory', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=30)), - ], - options={ - 'verbose_name': 'Settings Category', - 'verbose_name_plural': 'Settings', - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='TethysJob', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=30)), - ('group', models.CharField(max_length=30)), - ('creation_time', models.DateTimeField(default=datetime.datetime(2015, 4, 6, 22, 37, 42, 933728, tzinfo=utc))), - ('submission_time', models.DateTimeField()), - ('completion_time', models.DateTimeField()), - ('status', models.CharField(default=b'PEN', max_length=3, choices=[(b'PEN', b'Pending'), (b'SUB', b'Submitted'), (b'RUN', b'Running'), (b'COM', b'Complete'), (b'ERR', b'Error')])), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='CondorJob', - fields=[ - ('tethysjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tethys_compute.TethysJob')), - ('scheduler', models.CharField(max_length=12)), - ('ami', models.CharField(max_length=9)), - ], - options={ - }, - bases=('tethys_compute.tethysjob',), - ), - migrations.AddField( - model_name='tethysjob', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), - preserve_default=True, - ), - migrations.AddField( - model_name='setting', - name='category', - field=models.ForeignKey(to='tethys_compute.SettingsCategory'), - preserve_default=True, - ), - ] diff --git a/tethys_compute/migrations/0001_initial_20.py b/tethys_compute/migrations/0001_initial_20.py index 02a875565..f64661ae4 100644 --- a/tethys_compute/migrations/0001_initial_20.py +++ b/tethys_compute/migrations/0001_initial_20.py @@ -12,15 +12,6 @@ class Migration(migrations.Migration): initial = True - replaces = [(b'tethys_compute', '0001_initial'), (b'tethys_compute', '0002_initialize_settings'), - (b'tethys_compute', '0003_auto_20150529_1651'), (b'tethys_compute', '0004_auto_20150812_1915'), - (b'tethys_compute', '0005_auto_20150914_1712'), (b'tethys_compute', '0006_auto_20151221_2207'), - (b'tethys_compute', '0006_auto_20151026_2142'), (b'tethys_compute', '0007_merge'), - (b'tethys_compute', '0008_start_condorjob_refactor'), - (b'tethys_compute', '0009_condorjob_data_migration'), - (b'tethys_compute', '0010_finish_condorjob_refactor'), (b'tethys_compute', '0011_delete_cluster'), - (b'tethys_compute', '0012_delete_settings')] - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -30,16 +21,16 @@ class Migration(migrations.Migration): name='CondorPyJob', fields=[ ('condorpyjob_id', models.AutoField(primary_key=True, serialize=False)), - ('_attributes', tethys_compute.utilities.DictionaryField(default=b'')), + ('_attributes', tethys_compute.utilities.DictionaryField(default='')), ('_num_jobs', models.IntegerField(default=1)), - ('_remote_input_files', tethys_compute.utilities.ListField(default=b'')), + ('_remote_input_files', tethys_compute.utilities.ListField(default='')), ], ), migrations.CreateModel( name='CondorPyWorkflow', fields=[ ('condorpyworkflow_id', models.AutoField(primary_key=True, serialize=False)), - ('_max_jobs', tethys_compute.utilities.DictionaryField(blank=True, default=b'')), + ('_max_jobs', tethys_compute.utilities.DictionaryField(blank=True, default='')), ('_config', models.CharField(blank=True, max_length=1024, null=True)), ], ), @@ -52,7 +43,7 @@ class Migration(migrations.Migration): ('pre_script_args', models.CharField(blank=True, max_length=1024, null=True)), ('post_script', models.CharField(blank=True, max_length=1024, null=True)), ('post_script_args', models.CharField(blank=True, max_length=1024, null=True)), - ('variables', tethys_compute.utilities.DictionaryField(blank=True, default=b'')), + ('variables', tethys_compute.utilities.DictionaryField(blank=True, default='')), ('priority', models.IntegerField(blank=True, null=True)), ('category', models.CharField(blank=True, max_length=128, null=True)), ('retry', models.PositiveSmallIntegerField(blank=True, null=True)), @@ -82,16 +73,19 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=1024)), - ('description', models.CharField(blank=True, default=b'', max_length=2048)), + ('description', models.CharField(blank=True, default='', max_length=2048)), ('label', models.CharField(max_length=1024)), ('creation_time', models.DateTimeField(auto_now_add=True)), ('execute_time', models.DateTimeField(blank=True, null=True)), ('start_time', models.DateTimeField(blank=True, null=True)), ('completion_time', models.DateTimeField(blank=True, null=True)), - ('workspace', models.CharField(default=b'', max_length=1024)), - ('extended_properties', tethys_compute.utilities.DictionaryField(blank=True, default=b'')), + ('workspace', models.CharField(default='', max_length=1024)), + ('extended_properties', tethys_compute.utilities.DictionaryField(blank=True, default='')), ('_process_results_function', models.CharField(blank=True, max_length=1024, null=True)), - ('_status', models.CharField(choices=[(b'PEN', b'Pending'), (b'SUB', b'Submitted'), (b'RUN', b'Running'), (b'COM', b'Complete'), (b'ERR', b'Error'), (b'ABT', b'Aborted'), (b'VAR', b'Various'), (b'VCP', b'Various-Complete')], default=b'PEN', max_length=3)), + ('_status', models.CharField(choices=[('PEN', 'Pending'), ('SUB', 'Submitted'), ('RUN', 'Running'), + ('COM', 'Complete'), ('ERR', 'Error'), ('ABT', 'Aborted'), + ('VAR', 'Various'), ('VCP', 'Various-Complete')], default='PEN', + max_length=3)), ], options={ 'verbose_name': 'Job', @@ -100,14 +94,18 @@ class Migration(migrations.Migration): migrations.CreateModel( name='BasicJob', fields=[ - ('tethysjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_compute.TethysJob')), + ('tethysjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='tethys_compute.TethysJob')), ], bases=('tethys_compute.tethysjob',), ), migrations.CreateModel( name='CondorBase', fields=[ - ('tethysjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_compute.TethysJob')), + ('tethysjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='tethys_compute.TethysJob')), ('cluster_id', models.IntegerField(blank=True, default=0)), ('remote_id', models.CharField(blank=True, max_length=32, null=True)), ], @@ -116,8 +114,12 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CondorWorkflowJobNode', fields=[ - ('condorpyjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tethys_compute.CondorPyJob')), - ('condorworkflownode_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_compute.CondorWorkflowNode')), + ('condorpyjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, + parent_link=True, to='tethys_compute.CondorPyJob')), + ('condorworkflownode_ptr', models.OneToOneField(auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='tethys_compute.CondorWorkflowNode')), ], bases=('tethys_compute.condorworkflownode', 'tethys_compute.condorpyjob'), ), @@ -134,27 +136,36 @@ class Migration(migrations.Migration): migrations.AddField( model_name='condorworkflownode', name='workflow', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='node_set', to='tethys_compute.CondorPyWorkflow'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='node_set', + to='tethys_compute.CondorPyWorkflow'), ), migrations.CreateModel( name='CondorJob', fields=[ - ('condorpyjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tethys_compute.CondorPyJob')), - ('condorbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_compute.CondorBase')), + ('condorpyjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, + parent_link=True, to='tethys_compute.CondorPyJob')), + ('condorbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='tethys_compute.CondorBase')), ], bases=('tethys_compute.condorbase', 'tethys_compute.condorpyjob'), ), migrations.CreateModel( name='CondorWorkflow', fields=[ - ('condorpyworkflow_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tethys_compute.CondorPyWorkflow')), - ('condorbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_compute.CondorBase')), + ('condorpyworkflow_ptr', models.OneToOneField(auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, to='tethys_compute.CondorPyWorkflow')), + ('condorbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='tethys_compute.CondorBase')), ], bases=('tethys_compute.condorbase', 'tethys_compute.condorpyworkflow'), ), migrations.AddField( model_name='condorbase', name='scheduler', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tethys_compute.Scheduler'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, + to='tethys_compute.Scheduler'), ), ] diff --git a/tethys_compute/migrations/0002_initialize_settings.py b/tethys_compute/migrations/0002_initialize_settings.py deleted file mode 100644 index 0dc2f4162..000000000 --- a/tethys_compute/migrations/0002_initialize_settings.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -from tethys_compute.migrations import initialize_settings, clear_settings - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0001_initial'), - ] - - operations = [ - migrations.RunPython(clear_settings), - migrations.RunPython(initialize_settings, reverse_code=clear_settings), - ] \ No newline at end of file diff --git a/tethys_compute/migrations/0003_auto_20150529_1651.py b/tethys_compute/migrations/0003_auto_20150529_1651.py deleted file mode 100644 index ac539380a..000000000 --- a/tethys_compute/migrations/0003_auto_20150529_1651.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import tethys_compute.utilities - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0002_initialize_settings'), - ] - - operations = [ - migrations.AlterModelOptions( - name='tethysjob', - options={'verbose_name': 'Job'}, - ), - migrations.RenameField( - model_name='tethysjob', - old_name='group', - new_name='label', - ), - migrations.RemoveField( - model_name='condorjob', - name='ami', - ), - migrations.RemoveField( - model_name='condorjob', - name='scheduler', - ), - migrations.RemoveField( - model_name='condorjob', - name='tethysjob_ptr', - ), - migrations.RemoveField( - model_name='tethysjob', - name='status', - ), - migrations.RemoveField( - model_name='tethysjob', - name='submission_time', - ), - migrations.AddField( - model_name='condorjob', - name='attributes', - field=tethys_compute.utilities.DictionaryField(default=b''), - preserve_default=True, - ), - migrations.AddField( - model_name='condorjob', - name='cluster_id', - field=models.IntegerField(default=0, blank=True), - preserve_default=True, - ), - migrations.AddField( - model_name='condorjob', - name='condorpy_template_name', - field=models.CharField(max_length=256, null=True, blank=True), - preserve_default=True, - ), - migrations.AddField( - model_name='condorjob', - name='executable', - field=models.CharField(default='', max_length=256), - preserve_default=False, - ), - migrations.AddField( - model_name='condorjob', - name='num_jobs', - field=models.IntegerField(default=1), - preserve_default=True, - ), - migrations.AddField( - model_name='condorjob', - name='remote_id', - field=models.CharField(max_length=32, null=True, blank=True), - preserve_default=True, - ), - migrations.AddField( - model_name='condorjob', - name='remote_input_files', - field=tethys_compute.utilities.ListField(default=b''), - preserve_default=True, - ), - migrations.AddField( - model_name='condorjob', - name='tethys_job', - field=models.OneToOneField(related_name='child', primary_key=True, default='', serialize=False, to='tethys_compute.TethysJob'), - preserve_default=False, - ), - migrations.AddField( - model_name='condorjob', - name='working_directory', - field=models.CharField(max_length=512, null=True, blank=True), - preserve_default=True, - ), - migrations.AddField( - model_name='tethysjob', - name='_status', - field=models.CharField(default=b'PEN', max_length=3, choices=[(b'PEN', b'Pending'), (b'SUB', b'Submitted'), (b'RUN', b'Running'), (b'COM', b'Complete'), (b'ERR', b'Error'), (b'ABT', b'Aborted')]), - preserve_default=True, - ), - migrations.AddField( - model_name='tethysjob', - name='description', - field=models.CharField(default=b'', max_length=1024, blank=True), - preserve_default=True, - ), - migrations.AddField( - model_name='tethysjob', - name='execute_time', - field=models.DateTimeField(null=True, blank=True), - preserve_default=True, - ), - migrations.AlterField( - model_name='tethysjob', - name='completion_time', - field=models.DateTimeField(null=True, blank=True), - preserve_default=True, - ), - migrations.AlterField( - model_name='tethysjob', - name='creation_time', - field=models.DateTimeField(auto_now_add=True), - preserve_default=True, - ), - ] diff --git a/tethys_compute/migrations/0004_auto_20150812_1915.py b/tethys_compute/migrations/0004_auto_20150812_1915.py deleted file mode 100644 index 73cee0ffe..000000000 --- a/tethys_compute/migrations/0004_auto_20150812_1915.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import tethys_compute.utilities - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0003_auto_20150529_1651'), - ] - - operations = [ - migrations.CreateModel( - name='BasicJob', - fields=[ - ('tethysjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tethys_compute.TethysJob')), - ], - options={ - }, - bases=('tethys_compute.tethysjob',), - ), - migrations.CreateModel( - name='Scheduler', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=1024)), - ('host', models.CharField(max_length=1024)), - ('username', models.CharField(max_length=1024, null=True, blank=True)), - ('password', models.CharField(max_length=1024, null=True, blank=True)), - ('private_key_path', models.CharField(max_length=1024, null=True, blank=True)), - ('private_key_pass', models.CharField(max_length=1024, null=True, blank=True)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.RemoveField( - model_name='condorjob', - name='working_directory', - ), - migrations.AddField( - model_name='condorjob', - name='scheduler', - field=models.ForeignKey(default=1, to='tethys_compute.Scheduler'), - preserve_default=False, - ), - migrations.AddField( - model_name='tethysjob', - name='_subclass', - field=models.CharField(default=b'basicjob', max_length=30), - preserve_default=True, - ), - migrations.AddField( - model_name='tethysjob', - name='extended_properties', - field=tethys_compute.utilities.DictionaryField(default=b''), - preserve_default=True, - ), - migrations.AddField( - model_name='tethysjob', - name='workspace', - field=models.CharField(default=b'', max_length=1024), - preserve_default=True, - ), - migrations.AlterField( - model_name='condorjob', - name='condorpy_template_name', - field=models.CharField(max_length=1024, null=True, blank=True), - preserve_default=True, - ), - migrations.AlterField( - model_name='condorjob', - name='executable', - field=models.CharField(max_length=1024), - preserve_default=True, - ), - migrations.AlterField( - model_name='condorjob', - name='tethys_job', - field=models.OneToOneField(primary_key=True, serialize=False, to='tethys_compute.TethysJob'), - preserve_default=True, - ), - migrations.AlterField( - model_name='tethysjob', - name='_status', - field=models.CharField(default=b'PEN', max_length=3, choices=[(b'PEN', b'Pending'), (b'SUB', b'Submitted'), (b'RUN', b'Running'), (b'COM', b'Complete'), (b'ERR', b'Error'), (b'ABT', b'Aborted'), (b'VAR', b'Various'), (b'VCP', b'Various-Complete')]), - preserve_default=True, - ), - migrations.AlterField( - model_name='tethysjob', - name='description', - field=models.CharField(default=b'', max_length=2048, blank=True), - preserve_default=True, - ), - migrations.AlterField( - model_name='tethysjob', - name='label', - field=models.CharField(max_length=1024), - preserve_default=True, - ), - migrations.AlterField( - model_name='tethysjob', - name='name', - field=models.CharField(max_length=1024), - preserve_default=True, - ), - ] diff --git a/tethys_compute/migrations/0005_auto_20150914_1712.py b/tethys_compute/migrations/0005_auto_20150914_1712.py deleted file mode 100644 index 2f56da717..000000000 --- a/tethys_compute/migrations/0005_auto_20150914_1712.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0004_auto_20150812_1915'), - ] - - operations = [ - migrations.AlterField( - model_name='condorjob', - name='scheduler', - field=models.ForeignKey(blank=True, to='tethys_compute.Scheduler', null=True), - preserve_default=True, - ), - ] diff --git a/tethys_compute/migrations/0006_auto_20151026_2142.py b/tethys_compute/migrations/0006_auto_20151026_2142.py deleted file mode 100644 index 2df6837b2..000000000 --- a/tethys_compute/migrations/0006_auto_20151026_2142.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import tethys_compute.utilities - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0005_auto_20150914_1712'), - ] - - operations = [ - migrations.AlterField( - model_name='tethysjob', - name='extended_properties', - field=tethys_compute.utilities.DictionaryField(default=b'', blank=True), - preserve_default=True, - ), - migrations.AlterField( - model_name='tethysjob', - name='workspace', - field=models.CharField(default=b'', max_length=1024), - preserve_default=True, - ), - ] diff --git a/tethys_compute/migrations/0006_auto_20151221_2207.py b/tethys_compute/migrations/0006_auto_20151221_2207.py deleted file mode 100644 index 2df6837b2..000000000 --- a/tethys_compute/migrations/0006_auto_20151221_2207.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import tethys_compute.utilities - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0005_auto_20150914_1712'), - ] - - operations = [ - migrations.AlterField( - model_name='tethysjob', - name='extended_properties', - field=tethys_compute.utilities.DictionaryField(default=b'', blank=True), - preserve_default=True, - ), - migrations.AlterField( - model_name='tethysjob', - name='workspace', - field=models.CharField(default=b'', max_length=1024), - preserve_default=True, - ), - ] diff --git a/tethys_compute/migrations/0007_merge.py b/tethys_compute/migrations/0007_merge.py deleted file mode 100644 index 98c987c78..000000000 --- a/tethys_compute/migrations/0007_merge.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9 on 2015-12-21 22:19 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0006_auto_20151221_2207'), - ('tethys_compute', '0006_auto_20151026_2142'), - ] - - operations = [ - ] diff --git a/tethys_compute/migrations/0008_start_condorjob_refactor.py b/tethys_compute/migrations/0008_start_condorjob_refactor.py deleted file mode 100644 index e2874e78a..000000000 --- a/tethys_compute/migrations/0008_start_condorjob_refactor.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.2 on 2016-02-25 23:36 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion -import tethys_compute.utilities - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0007_merge'), - ] - - operations = [ - migrations.CreateModel( - name='CondorBase', - fields=[ - ('tethysjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_compute.TethysJob')), - ('cluster_id', models.IntegerField(blank=True, default=0)), - ('remote_id', models.CharField(blank=True, max_length=32, null=True)), - ('scheduler', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tethys_compute.Scheduler')), - ], - bases=('tethys_compute.tethysjob',), - ), - migrations.CreateModel( - name='CondorPyJob', - fields=[ - ('condorpyjob_id', models.AutoField(primary_key=True, serialize=False)), - ('_attributes', tethys_compute.utilities.DictionaryField(default=b'')), - ('_num_jobs', models.IntegerField(default=1)), - ('_remote_input_files', tethys_compute.utilities.ListField(default=b'')), - ], - ), - migrations.AddField( - model_name='condorjob', - name='condorpyjob_ptr', - field=models.OneToOneField(auto_created=True, null=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tethys_compute.CondorPyJob'), - preserve_default=False, - ), - migrations.AlterField( - model_name='condorjob', - name='executable', - field=models.CharField(max_length=1024, default=''), - preserve_default=True, - ), - migrations.RemoveField( - model_name='tethysjob', - name='_subclass', - ), - ] diff --git a/tethys_compute/migrations/0009_condorjob_data_migration.py b/tethys_compute/migrations/0009_condorjob_data_migration.py deleted file mode 100644 index 3197494e9..000000000 --- a/tethys_compute/migrations/0009_condorjob_data_migration.py +++ /dev/null @@ -1,87 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.2 on 2016-02-26 14:53 -from __future__ import unicode_literals - -from django.db import migrations - - -def migrate_condorjobs(apps, schema_editor): - """ - Copy data from old CondorJob model to new CondorBase and CondorPyJob models. - - Args: - apps: Historical version of the models from django.apps.registry.Apps - schema_editor: Instance of SchemaEditor for manual DB editing. - """ - CondorJob = apps.get_model('tethys_compute', 'CondorJob') - CondorBase = apps.get_model('tethys_compute', 'CondorBase') - CondorPyJob = apps.get_model('tethys_compute', 'CondorPyJob') - - condorjobs = CondorJob.objects.all() - - for condorjob in condorjobs: - condorbase = CondorBase(tethysjob_ptr=condorjob.tethys_job, - cluster_id=condorjob.cluster_id, - remote_id=condorjob.remote_id, - scheduler=condorjob.scheduler, - ) - tethysjob = condorbase.tethysjob_ptr - condorbase.creation_time = tethysjob.creation_time - condorbase.user_id = tethysjob.user_id - condorbase.save() - - condorpyjob = CondorPyJob(_attributes=condorjob.attributes, - _num_jobs=condorjob.num_jobs, - _remote_input_files=condorjob.remote_input_files) - condorpyjob.save() - - condorjob.condorbase_ptr = condorbase - condorjob.condorpyjob_ptr = condorpyjob - condorjob.save() - - tethysjob = condorjob.tethys_job - tethysjob._subclass = 'condorbase' - tethysjob.save() - - -def unmigrate_condorjobs(apps, schema_editor): - """ - Copy data from new CondorBase and CondorPyJob models back to old CondorJob model. - - Args: - apps: Historical version of the models from django.apps.registry.Apps - schema_editor: Instance of SchemaEditor for manual DB editing. - """ - CondorJob = apps.get_model('tethys_compute', 'CondorJob') - CondorBase = apps.get_model('tethys_compute', 'CondorBase') - - condorjobs = CondorJob.objects.all() - for condorjob in condorjobs: - condorbase = CondorBase.objects.get(pk=condorjob.tethys_job_id) - condorjob.cluster_id =condorbase.cluster_id - condorjob.remote_id = condorbase.remote_id - condorjob.scheduler = condorbase.scheduler - tethysjob = condorbase.tethysjob_ptr - - condorpyjob = condorjob.condorpyjob_ptr - condorjob.attributes = condorpyjob.attributes - condorjob.num_jobs = condorpyjob.num_jobs - condorjob.remote_input_files = condorpyjob.remote_input_files - if 'executable' in condorjob.attributes: - condorjob.executable = condorjob.attributes['executable'] - - condorjob.save() - - tethysjob._subclass = 'condorjob' - tethysjob.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0008_start_condorjob_refactor'), - ] - - operations = [ - migrations.RunPython(code=migrate_condorjobs, reverse_code=unmigrate_condorjobs) - ] diff --git a/tethys_compute/migrations/0010_finish_condorjob_refactor.py b/tethys_compute/migrations/0010_finish_condorjob_refactor.py deleted file mode 100644 index b80b7ca5e..000000000 --- a/tethys_compute/migrations/0010_finish_condorjob_refactor.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.2 on 2016-02-26 14:54 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - -import tethys_compute - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0009_condorjob_data_migration'), - ] - - operations = [ - migrations.AddField( - model_name='tethysjob', - name='_process_results_function', - field=models.CharField(blank=True, max_length=1024, null=True), - ), - migrations.AddField( - model_name='tethysjob', - name='start_time', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.RemoveField( - model_name='condorjob', - name='cluster_id', - ), - migrations.RemoveField( - model_name='condorjob', - name='remote_id', - ), - migrations.RemoveField( - model_name='condorjob', - name='scheduler', - ), - migrations.RemoveField( - model_name='condorjob', - name='attributes', - ), - migrations.RemoveField( - model_name='condorjob', - name='num_jobs', - ), - migrations.RemoveField( - model_name='condorjob', - name='remote_input_files', - ), - migrations.RemoveField( - model_name='condorjob', - name='condorpy_template_name', - ), - migrations.RemoveField( - model_name='condorjob', - name='executable', - ), - migrations.RenameField( - model_name='condorjob', - old_name='tethys_job', - new_name='condorbase_ptr', - ), - migrations.AlterField( - model_name='condorjob', - name='condorbase_ptr', - field=models.OneToOneField(auto_created=True, primary_key=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, serialize=False, to='tethys_compute.CondorBase'), - preserve_default=False, - ), - migrations.AlterField( - model_name='condorjob', - name='condorpyjob_ptr', - field=models.OneToOneField(auto_created=True, null=False, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tethys_compute.CondorPyJob'), - preserve_default=False, - ), - migrations.CreateModel( - name='CondorPyWorkflow', - fields=[ - ('condorpyworkflow_id', models.AutoField(primary_key=True, serialize=False)), - ('_max_jobs', tethys_compute.utilities.DictionaryField(blank=True, default=b'')), - ('_config', models.CharField(blank=True, max_length=1024, null=True)), - ], - ), - migrations.CreateModel( - name='CondorWorkflow', - fields=[ - ('condorpyworkflow_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tethys_compute.CondorPyWorkflow')), - ('condorbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_compute.CondorBase')), - ], - bases=('tethys_compute.condorbase', 'tethys_compute.condorpyworkflow'), - ), - migrations.CreateModel( - name='CondorWorkflowNode', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=1024)), - ('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='node_set', to='tethys_compute.CondorPyWorkflow')), - ('pre_script', models.CharField(blank=True, max_length=1024, null=True)), - ('pre_script_args', models.CharField(blank=True, max_length=1024, null=True)), - ('post_script', models.CharField(blank=True, max_length=1024, null=True)), - ('post_script_args', models.CharField(blank=True, max_length=1024, null=True)), - ('variables', tethys_compute.utilities.DictionaryField(blank=True, default=b'')), - ('priority', models.IntegerField(blank=True, null=True)), - ('category', models.CharField(blank=True, max_length=128, null=True)), - ('retry', models.PositiveSmallIntegerField(blank=True, null=True)), - ('retry_unless_exit_value', models.IntegerField(blank=True, null=True)), - ('pre_skip', models.IntegerField(blank=True, null=True)), - ('abort_dag_on', models.IntegerField(blank=True, null=True)), - ('abort_dag_on_return_value', models.IntegerField(blank=True, null=True)), - ('dir', models.CharField(blank=True, max_length=1024, null=True)), - ('noop', models.BooleanField(default=False)), - ('done', models.BooleanField(default=False)), - ('parent_nodes', models.ManyToManyField(related_name='children_nodes', to='tethys_compute.CondorWorkflowNode')), - ], - ), - migrations.CreateModel( - name='CondorWorkflowJobNode', - fields=[ - ('condorpyjob_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='tethys_compute.CondorPyJob')), - ('condorworkflownode_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tethys_compute.CondorWorkflowNode')), - ], - bases=('tethys_compute.condorworkflownode', 'tethys_compute.condorpyjob'), - ), - ] diff --git a/tethys_compute/migrations/0011_delete_cluster.py b/tethys_compute/migrations/0011_delete_cluster.py deleted file mode 100644 index 5a2d234a6..000000000 --- a/tethys_compute/migrations/0011_delete_cluster.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2017-06-02 02:32 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0010_finish_condorjob_refactor'), - ] - - operations = [ - migrations.DeleteModel( - name='Cluster', - ), - ] diff --git a/tethys_compute/migrations/0012_delete_settings.py b/tethys_compute/migrations/0012_delete_settings.py deleted file mode 100644 index e4c00d6bb..000000000 --- a/tethys_compute/migrations/0012_delete_settings.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.2 on 2017-06-17 14:10 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_compute', '0011_delete_cluster'), - ] - - operations = [ - migrations.RemoveField( - model_name='setting', - name='category', - ), - migrations.DeleteModel( - name='Setting', - ), - migrations.DeleteModel( - name='SettingsCategory', - ), - ] diff --git a/tethys_compute/migrations/__init__.py b/tethys_compute/migrations/__init__.py index a5e4bf07b..e37944e29 100644 --- a/tethys_compute/migrations/__init__.py +++ b/tethys_compute/migrations/__init__.py @@ -1,6 +1,6 @@ # add the following to operations in migrations # from . import initialize_settings - # migrations.RunPython(initialize_settings), +# migrations.RunPython(initialize_settings), def initialize_settings(apps, schema_editor): @@ -25,9 +25,10 @@ def initialize_settings(apps, schema_editor): s = Setting(name=setting, category=category) s.save() + def clear_settings(apps, schema_edititor): SettingsCategory = apps.get_model('tethys_compute', 'SettingsCategory') Setting = apps.get_model('tethys_compute', 'Setting') SettingsCategory.objects.all().delete() - Setting.objects.all().delete() \ No newline at end of file + Setting.objects.all().delete() diff --git a/tethys_compute/models.py b/tethys_compute/models.py index 99b48d76c..95c8fb50c 100644 --- a/tethys_compute/models.py +++ b/tethys_compute/models.py @@ -11,13 +11,12 @@ import shutil import datetime import inspect -from abc import abstractmethod, abstractproperty +from abc import abstractmethod import logging -log = logging.getLogger('tethys.tethys_compute.models') from django.db import models from django.contrib.auth.models import User -from django.db.models.signals import pre_save, pre_delete +from django.db.models.signals import pre_save, pre_delete, post_save from django.dispatch import receiver from django.utils import timezone from model_utils.managers import InheritanceManager @@ -27,6 +26,8 @@ from condorpy import Job, Workflow, Node, Templates +log = logging.getLogger('tethys' + __name__) + class Scheduler(models.Model): name = models.CharField(max_length=1024) @@ -97,6 +98,7 @@ def run_time(self): end_time = self.completion_time or datetime.datetime.now(start_time.tzinfo) run_time = end_time - start_time else: + # TODO: Is this code reachable? if self.completion_time and self.execute_time: run_time = self.completion_time - self.execute_time else: @@ -117,7 +119,7 @@ def update_status(self, *args, **kwargs): old_status = self._status if self._status in ['PEN', 'SUB', 'RUN', 'VAR']: if not hasattr(self, '_last_status_update') \ - or datetime.datetime.now()-self.last_status_update > self.update_status_interval: + or datetime.datetime.now() - self.last_status_update > self.update_status_interval: self._update_status(*args, **kwargs) self._last_status_update = datetime.datetime.now() if self._status == 'RUN' and (old_status == 'PEN' or old_status == 'SUB'): @@ -214,7 +216,6 @@ def resume(self): # condorpy_logger.activate_console_logging() - class CondorBase(TethysJob): """ Base class for CondorJob and CondorWorkflow @@ -238,7 +239,7 @@ class CondorBase(TethysJob): def condor_object(self): """ Returns: an instance of a condorpy job or condorpy workflow with scheduler, cluster_id, and remote_id attributes set - """ + """ # noqa: E501 condor_object = self._condor_object condor_object._cluster_id = self.cluster_id condor_object._cwd = self.workspace @@ -254,7 +255,7 @@ def condor_object(self): condor_object._remote_id = self.remote_id return condor_object - @abstractproperty + @abstractmethod def _condor_object(self): """ Returns: an instance of a condorpy job or condorpy workflow @@ -287,7 +288,7 @@ def _update_status(self): running_statuses = statuses['Unexpanded'] + statuses['Idle'] + statuses['Running'] if not running_statuses: condor_status = 'Various-Complete' - except Exception as e: + except Exception: # raise e condor_status = 'Submission_err' self._status = self.STATUS_MAP[condor_status] @@ -328,10 +329,23 @@ class CondorPyJob(models.Model): _num_jobs = models.IntegerField(default=1) _remote_input_files = ListField(default='') + def __init__(self, *args, **kwargs): + # if condorpy_template_name or attributes is passed in then get the template and add it to the _attributes + attributes = kwargs.pop('attributes', dict()) + _attributes = kwargs.get('_attributes', dict()) + attributes.update(_attributes) + condorpy_template_name = kwargs.pop('condorpy_template_name', None) + if condorpy_template_name is not None: + template = self.get_condorpy_template(condorpy_template_name) + template.update(attributes) + attributes = template + kwargs['_attributes'] = attributes + super(CondorPyJob, self).__init__(*args, **kwargs) + @classmethod def get_condorpy_template(cls, template_name): template_name = template_name or 'base' - template = getattr(Templates, template_name) + template = getattr(Templates, template_name, None) if not template: template = Templates.base return template @@ -353,11 +367,11 @@ def condorpy_job(self): def attributes(self): return self._attributes - # @attributes.setter - # def attributes(self, attributes): - # assert isinstance(attributes, dict) - # self.condorpy_job._attributes = attributes - # self._attributes = attributes + @attributes.setter + def attributes(self, attributes): + assert isinstance(attributes, dict) + self._attributes = attributes + self.condorpy_job._attributes = attributes @property def num_jobs(self): @@ -383,7 +397,7 @@ def initial_dir(self): return os.path.join(self.workspace, self.condorpy_job.initial_dir) def get_attribute(self, attribute): - self.condorpy_job.get(attribute) + return self.condorpy_job.get(attribute) def set_attribute(self, attribute, value): setattr(self.condorpy_job, attribute, value) @@ -424,7 +438,7 @@ def condor_job_pre_save(sender, instance, raw, using, update_fields, **kwargs): def condor_job_pre_delete(sender, instance, using, **kwargs): try: instance.condor_object.close_remote() - shutil.rmtree(instance.initial_dir) + shutil.rmtree(instance.initial_dir, ignore_errors=True) except Exception as e: log.exception(str(e)) @@ -457,6 +471,11 @@ def condorpy_workflow(self): def max_jobs(self): return self._max_jobs + @max_jobs.setter + def max_jobs(self, max_jobs): + self.condorpy_workflow._max_jobs = max_jobs + self._max_jobs = max_jobs + @property def config(self): return self._config @@ -542,7 +561,7 @@ def condor_workflow_pre_save(sender, instance, raw, using, update_fields, **kwar def condor_workflow_pre_delete(sender, instance, using, **kwargs): try: instance.condor_object.close_remote() - shutil.rmtree(instance.workspace) + shutil.rmtree(instance.workspace, ignore_errors=True) except Exception as e: log.exception(str(e)) @@ -550,6 +569,27 @@ def condor_workflow_pre_delete(sender, instance, using, **kwargs): class CondorWorkflowNode(models.Model): """ Base class for CondorWorkflow Nodes + + Args: + name (str): + workflow (`CondorWorkflow`): instance of a `CondorWorkflow` that node belongs to + parent_nodes (list): list of `CondorWorkflowNode` objects that are prerequisites to this node + pre_script (str): + pre_script_args (str): + post_script (str): + post_script_args (str): + variables (dict): + priority (int): + category (str): + retry (int): + retry_unless_exit_value (int): + pre_skip (int): + abort_dag_on (int): + dir (str): + noop (bool): + done (bool): + + For a description of the arguments see http://research.cs.wisc.edu/htcondor/manual/v8.6/2_10DAGMan_Applications.html """ TYPES = (('JOB', 'JOB'), ('DAT', 'DATA'), @@ -581,11 +621,11 @@ class CondorWorkflowNode(models.Model): noop = models.BooleanField(default=False) done = models.BooleanField(default=False) - @abstractproperty + @abstractmethod def type(self): pass - @abstractproperty + @abstractmethod def job(self): pass @@ -626,19 +666,6 @@ class CondorWorkflowJobNode(CondorWorkflowNode, CondorPyJob): """ CondorWorkflow JOB type node """ - def __init__(self, *args, **kwargs): - """ - Initialize both CondorWorkflowNode and CondorPyJob - - Args: - name: - workflow: - attributes: - num_jobs: - remote_input_files: - """ - CondorWorkflowNode.__init__(self, *args, **kwargs) - CondorPyJob.__init__(self, *args, **kwargs) @property def type(self): @@ -660,3 +687,12 @@ def update_database_fields(self): @receiver(pre_save, sender=CondorWorkflowJobNode) def condor_workflow_job_node_pre_save(sender, instance, raw, using, update_fields, **kwargs): instance.update_database_fields() + + +@receiver(post_save, sender=CondorJob) +@receiver(post_save, sender=BasicJob) +@receiver(post_save, sender=TethysJob) +def tethys_job_post_save(sender, instance, raw, using, update_fields, **kwargs): + if instance.name.find('{id}') >= 0: + instance.name = instance.name.format(id=instance.id) + instance.save() diff --git a/tethys_compute/tests.py b/tethys_compute/tests.py deleted file mode 100644 index 54bb157c6..000000000 --- a/tethys_compute/tests.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -******************************************************************************** -* Name: tests.py -* Author: Scott Christensen -* Created On: 2015 -* Copyright: (c) Brigham Young University 2015 -* License: BSD 2-Clause -******************************************************************************** -""" -from django.test import TestCase - -# Create your tests here. diff --git a/tethys_compute/urls.py b/tethys_compute/urls.py index 915d6a8bd..bdf285dc0 100644 --- a/tethys_compute/urls.py +++ b/tethys_compute/urls.py @@ -7,13 +7,3 @@ * License: BSD 2-Clause ******************************************************************************** """ -from django.conf.urls import url - -# from tethys_compute import views -# -# urlpatterns = [ -# url(r'^cluster/$', views.index, name='index'), -# url(r'^cluster/add/$', views.create_cluster, name='create_cluster'), -# url(r'^cluster/(?P\d+)/update/$', views.update_cluster, name='update_cluster'), -# url(r'^cluster/(?P\d+)/delete/$', views.delete_cluster, name='delete_cluster'), -# ] \ No newline at end of file diff --git a/tethys_compute/utilities.py b/tethys_compute/utilities.py index 74b291f3b..421ebc43e 100644 --- a/tethys_compute/utilities.py +++ b/tethys_compute/utilities.py @@ -10,7 +10,6 @@ import json from django import forms -from django.core import exceptions from django.db import models from django.utils.translation import ugettext_lazy as _ from future.utils import with_metaclass @@ -22,6 +21,7 @@ # deprecated code copied from: https://github.com/django/django/blob/stable/1.9.x/django/db/models/fields/subclassing.py +# TODO: Talk to Scott about this class SubfieldBase(type): """ A metaclass for custom Field subclasses. This ensures the model's attribute @@ -49,9 +49,12 @@ def __get__(self, obj, type=None): return obj.__dict__[self.field.name] def __set__(self, obj, value): + # Google says this is bad + # TODO: this is NOT tested obj.__dict__[self.field.name] = self.field.to_python(value) +# TODO: Talk to Scott about this - Not used, can we take this out? def make_contrib(superclass, func=None): """ Returns a suitable contribute_to_class() method for the Field subclass. @@ -86,15 +89,16 @@ def to_python(self, value): elif isinstance(value, basestring): try: return dict(json.loads(value)) - except (ValueError, TypeError): - raise exceptions.ValidationError(self.error_messages['invalid value for json: %s' % value]) + except (ValueError, TypeError) as e: + raise e + # raise exceptions.ValidationError(self.error_messages['invalid value for json: %s' % value]) if isinstance(value, dict): return value else: return {} - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection, context): return self.to_python(value) def get_prep_value(self, value): @@ -164,4 +168,4 @@ def clean(self, value, model_instance): def formfield(self, **kwargs): defaults = {'widget': forms.Textarea} defaults.update(kwargs) - return super(ListField, self).formfield(**defaults) \ No newline at end of file + return super(ListField, self).formfield(**defaults) diff --git a/tethys_compute/views.py b/tethys_compute/views.py deleted file mode 100644 index bb004b39e..000000000 --- a/tethys_compute/views.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -******************************************************************************** -* Name: views.py -* Author: Scott Christensen -* Created On: 2015 -* Copyright: (c) Brigham Young University 2015 -* License: BSD 2-Clause -******************************************************************************** -""" -# from django.shortcuts import render, redirect, get_object_or_404, get_list_or_404 -# from django.http import HttpResponse, HttpResponseServerError -# from django.core.urlresolvers import reverse -# from django.core.exceptions import PermissionDenied -# -# from tethyscluster.cli_api import TethysCluster -# from tethyscluster import config, cluster -# from subprocess import Popen -# from multiprocessing import Process -# from threading import Timer -# -# from tethys_compute.models import TethysJob, Cluster - -# Create your views here. - -# def index(request): -# clusters = Cluster.objects.all() -# return render(request, 'tethys_compute/cluster_index.html', {'title':'Computing Resources', 'clusters':clusters}) -# -# -# -# -# def create_cluster(request): -# if request.POST: -# name = request.POST['name'] -# size = int(request.POST['size']) -# try: -# #sc = TethysCluster() -# #sc.start(name, cluster_size=size) -# -# # cm = config.get_cluster_manager() -# # cl = cm.get_default_template_cluster() -# # cl.update({'cluster_size':size}) -# # cl.start() -# -# process = Popen(['tethyscluster', 'start', '-s', str(size), name]) -# -# # t = Timer(120, _status_update) -# # t.start() -# -# except Exception as e: -# return HttpResponseServerError('There was an error with TethysCluster: %s' % str(e.message)) -# -# cluster = Cluster(name=name, size=size) -# cluster.save() -# -# return redirect(reverse('index')) -# else: -# raise Exception -# -# -# def update_cluster(request, pk): -# if request.POST: -# cluster = get_object_or_404(Cluster, id=pk) -# name = cluster.name -# delta_size = abs(request.POST['size'] - cluster.size) -# if delta_size != 0: -# cmd = 'addnode' if request.POST['size'] > cluster.size else 'deletenode' -# -# Popen(['tethyscluster', cmd, '-n', delta_size, name]) -# -# cluster.size = request.POST['size'] -# cluster.status = 'UPD' -# cluster.save() -# -# return redirect(reverse('index')) -# else: -# raise Exception -# -# def delete_cluster(request, pk): -# cluster = get_object_or_404(Cluster, id=pk) -# name = cluster.name -# -# try: -# # sc = TethysCluster() -# # sc.terminate(name) -# -# # cm = config.get_cluster_manager() -# # cl = cm.get_cluster(name) -# # cl.terminate_cluster(force=True) -# -# # Popen(['tethyscluster', 'terminate', '-f', '-c', name]) -# -# process = Process(target=_delete_cluster, args=(name,)) -# process.start() -# -# except: -# HttpResponse('There was an error with TethysCluster') -# -# cluster.status = 'DEL' -# cluster.save() -# -# return redirect(reverse('index')) -# -# -# def _start_cluster(name, size): -# cm = config.get_cluster_manager() -# cl = cm.get_default_template_cluster(name) -# cl.update({'cluster_size':size}) -# cl.start() -# -# def _delete_cluster(name): -# cm = config.get_cluster_manager() -# cl = cm.get_cluster(name) -# cl.terminate_cluster(force=True) -# cluster = Cluster.objects.get(name=name) -# cluster.delete() \ No newline at end of file diff --git a/tethys_config/__init__.py b/tethys_config/__init__.py index 4752315fe..e4e835c96 100644 --- a/tethys_config/__init__.py +++ b/tethys_config/__init__.py @@ -8,4 +8,4 @@ ******************************************************************************** """ # Load the custom app config -default_app_config = 'tethys_config.apps.TethysPortalConfig' \ No newline at end of file +default_app_config = 'tethys_config.apps.TethysPortalConfig' diff --git a/tethys_config/admin.py b/tethys_config/admin.py index ef56f06ee..72e0bd86f 100644 --- a/tethys_config/admin.py +++ b/tethys_config/admin.py @@ -37,4 +37,4 @@ def has_add_permission(self, request): return False -admin.site.register(SettingsCategory, SettingCategoryAdmin) \ No newline at end of file +admin.site.register(SettingsCategory, SettingCategoryAdmin) diff --git a/tethys_config/context_processors.py b/tethys_config/context_processors.py index 8a8ba552b..b9caf68b9 100644 --- a/tethys_config/context_processors.py +++ b/tethys_config/context_processors.py @@ -20,7 +20,15 @@ def tethys_global_settings_context(request): site_globals = Setting.as_dict() # Get terms and conditions - site_globals.update({'documents': TermsAndConditions.get_active_list(as_dict=False)}) + + # Grrr!!! TermsAndConditions has a different interface for Python 2 and 3 + try: + # for Python 3 + site_globals.update({'documents': TermsAndConditions.get_active_terms_list()}) + except AttributeError: + # for Python 3 + site_globals.update({'documents': TermsAndConditions.get_active_list(as_dict=False)}) + context = {'site_globals': site_globals} return context diff --git a/tethys_config/init.py b/tethys_config/init.py index 04562e131..2e505aa4b 100644 --- a/tethys_config/init.py +++ b/tethys_config/init.py @@ -28,7 +28,7 @@ def initial_settings(apps, schema_editor): date_modified=now) general_category.setting_set.create(name="Favicon", - content="/static/tethys_portal/images/default_favicon.png", + content="/tethys_portal/images/default_favicon.png", date_modified=now) general_category.setting_set.create(name="Brand Text", @@ -36,7 +36,7 @@ def initial_settings(apps, schema_editor): date_modified=now) general_category.setting_set.create(name="Brand Image", - content="/static/tethys_portal/images/tethys-logo-75.png", + content="/tethys_portal/images/tethys-logo-75.png", date_modified=now) general_category.setting_set.create(name="Brand Image Height", @@ -113,7 +113,7 @@ def initial_settings(apps, schema_editor): date_modified=now) home_category.setting_set.create(name="Feature 1 Image", - content="/static/tethys_portal/images/placeholder.gif", + content="/tethys_portal/images/placeholder.gif", date_modified=now) home_category.setting_set.create(name="Feature 2 Heading", @@ -121,12 +121,12 @@ def initial_settings(apps, schema_editor): date_modified=now) home_category.setting_set.create(name="Feature 2 Body", - content="Describe the apps and tools that your Tethys Portal provides and add" + content="Describe the apps and tools that your Tethys Portal provides and add " "custom pictures to each feature as a finishing touch.", date_modified=now) home_category.setting_set.create(name="Feature 2 Image", - content="/static/tethys_portal/images/placeholder.gif", + content="/tethys_portal/images/placeholder.gif", date_modified=now) home_category.setting_set.create(name="Feature 3 Heading", @@ -140,7 +140,7 @@ def initial_settings(apps, schema_editor): date_modified=now) home_category.setting_set.create(name="Feature 3 Image", - content="/static/tethys_portal/images/placeholder.gif", + content="/tethys_portal/images/placeholder.gif", date_modified=now) home_category.setting_set.create(name="Call to Action", @@ -167,6 +167,3 @@ def reverse_init(apps, schema_editor): for category in categories: category.delete() - - - diff --git a/tethys_config/migrations/0001_initial.py b/tethys_config/migrations/0001_initial.py deleted file mode 100644 index 0aa7a300b..000000000 --- a/tethys_config/migrations/0001_initial.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Setting', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=30)), - ('content', models.CharField(max_length=500)), - ('date_modified', models.DateTimeField(verbose_name=b'date modified')), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='SettingsCategory', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=30)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.AddField( - model_name='setting', - name='category', - field=models.ForeignKey(to='tethys_config.SettingsCategory'), - preserve_default=True, - ), - ] diff --git a/tethys_config/migrations/0001_initial_20.py b/tethys_config/migrations/0001_initial_20.py index 6673eab4a..fb297e88a 100644 --- a/tethys_config/migrations/0001_initial_20.py +++ b/tethys_config/migrations/0001_initial_20.py @@ -4,12 +4,10 @@ from django.db import migrations, models import django.db.models.deletion -import tethys_config.init from ..init import initial_settings, reverse_init -class Migration(migrations.Migration): - replaces = [(b'tethys_config', '0001_initial'), (b'tethys_config', '0002_auto_20141029_1848'), (b'tethys_config', '0003_auto_20141223_2244'), (b'tethys_config', '0004_auto_20150424_2050'), (b'tethys_config', '0005_auto_20151023_1720')] +class Migration(migrations.Migration): initial = True @@ -18,48 +16,25 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Setting', + name='SettingsCategory', + options={'verbose_name': 'Settings Category', 'verbose_name_plural': 'Site Settings'}, fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=30)), - ('content', models.CharField(max_length=500)), - ('date_modified', models.DateTimeField(verbose_name=b'date modified')), + ('name', models.TextField(max_length=30)), ], ), migrations.CreateModel( - name='SettingsCategory', + name='Setting', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=30)), + ('name', models.TextField(max_length=30)), + ('content', models.TextField(blank=True, max_length=500)), + ('date_modified', models.DateTimeField(auto_now=True, verbose_name='date modified')), + ('category', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='tethys_config.SettingsCategory' + )), ], ), - migrations.AddField( - model_name='setting', - name='category', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tethys_config.SettingsCategory'), - ), - migrations.RunPython( - code=tethys_config.init.initial_settings, - reverse_code=tethys_config.init.reverse_init, - ), - migrations.AlterModelOptions( - name='settingscategory', - options={'verbose_name': 'Settings Category', 'verbose_name_plural': 'Site Settings'}, - ), - migrations.AlterField( - model_name='setting', - name='content', - field=models.TextField(blank=True, max_length=500), - ), - migrations.AlterField( - model_name='setting', - name='date_modified', - field=models.DateTimeField(auto_now=True, verbose_name=b'date modified'), - ), - migrations.AlterField( - model_name='setting', - name='name', - field=models.TextField(max_length=30), - ), migrations.RunPython(initial_settings, reverse_init), ] diff --git a/tethys_config/migrations/0002_auto_20141029_1848.py b/tethys_config/migrations/0002_auto_20141029_1848.py deleted file mode 100644 index ddc9f9ef5..000000000 --- a/tethys_config/migrations/0002_auto_20141029_1848.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -from ..init import initial_settings, reverse_init - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_config', '0001_initial'), - ] - - operations = [ - migrations.RunPython(initial_settings, reverse_init), - ] diff --git a/tethys_config/migrations/0003_auto_20141223_2244.py b/tethys_config/migrations/0003_auto_20141223_2244.py deleted file mode 100644 index c9adfaa57..000000000 --- a/tethys_config/migrations/0003_auto_20141223_2244.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_config', '0002_auto_20141029_1848'), - ] - - operations = [ - migrations.AlterModelOptions( - name='settingscategory', - options={'verbose_name': 'Settings Category', 'verbose_name_plural': 'Site Settings'}, - ), - migrations.AlterField( - model_name='setting', - name='content', - field=models.TextField(max_length=500, blank=True), - preserve_default=True, - ), - migrations.AlterField( - model_name='setting', - name='date_modified', - field=models.DateTimeField(auto_now=True, verbose_name=b'date modified'), - preserve_default=True, - ), - migrations.AlterField( - model_name='setting', - name='name', - field=models.TextField(max_length=30), - preserve_default=True, - ), - ] diff --git a/tethys_config/migrations/0004_auto_20150424_2050.py b/tethys_config/migrations/0004_auto_20150424_2050.py deleted file mode 100644 index 2d2f7195f..000000000 --- a/tethys_config/migrations/0004_auto_20150424_2050.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.utils import timezone -from django.db import models, migrations - - -def settings10to11(apps, schema_editor): - """ - Update with new settings introduced in 1.1 which include: - - * Apps Library Title - """ - # Figure out what time it is right now - now = timezone.now() - - # Get current settings - Setting = apps.get_model('tethys_config', 'Setting') - all_settings = Setting.objects.all() - - # Remove any settings that already exist - SettingsCategory = apps.get_model('tethys_config', 'SettingsCategory') - general_category = SettingsCategory.objects.get(name="General Settings") - - app_library_title = False - - for setting in all_settings: - if setting.name == 'Apps Library Title': - app_library_title = True - - if not app_library_title: - general_category.setting_set.create(name='Apps Library Title', - content='Apps Library', - date_modified=now) - - general_category.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_config', '0003_auto_20141223_2244'), - ] - - operations = [ - migrations.RunPython(settings10to11), - ] diff --git a/tethys_config/migrations/0005_auto_20151023_1720.py b/tethys_config/migrations/0005_auto_20151023_1720.py deleted file mode 100644 index 27e0e0b8f..000000000 --- a/tethys_config/migrations/0005_auto_20151023_1720.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.utils import timezone -from django.db import models, migrations - - -def settings12to13(apps, schema_editor): - """ - Update with new settings introduced in 1.3 which include: - - * Text Color - * Hover Text Color - * Apps Library Background Color - * Logo Height and Padding Settings - """ - # Figure out what time it is right now - now = timezone.now() - - # Get current settings - Setting = apps.get_model('tethys_config', 'Setting') - all_settings = Setting.objects.all() - - # Remove any settings that already exist - SettingsCategory = apps.get_model('tethys_config', 'SettingsCategory') - general_category = SettingsCategory.objects.get(name="General Settings") - - primary_text_color = False - primary_hover_color = False - secondary_text_color = False - secondary_hover_color = False - background_color = False - brand_image_height = False - brand_image_width = False - brand_image_padding = False - - - for setting in all_settings: - if setting.name == 'Primary Text Color': - primary_text_color = True - if setting.name == 'Primary Text Hover Color': - primary_hover_color = True - if setting.name == 'Secondary Text Color': - secondary_text_color = True - if setting.name == 'Secondary Text Hover Color': - secondary_hover_color = True - if setting.name == 'Background Color': - background_color = True - if setting.name == 'Brand Image Height': - brand_image_height = True - if setting.name == 'Brand Image Width': - brand_image_width = True - if setting.name == 'Brand Image Padding': - brand_image_padding = True - - if not primary_text_color: - general_category.setting_set.create(name="Primary Text Color", - content="", - date_modified=now) - - if not primary_hover_color: - general_category.setting_set.create(name="Primary Text Hover Color", - content="", - date_modified=now) - - if not secondary_text_color: - general_category.setting_set.create(name="Secondary Text Color", - content="", - date_modified=now) - - if not secondary_hover_color: - general_category.setting_set.create(name="Secondary Text Hover Color", - content="", - date_modified=now) - - if not background_color: - general_category.setting_set.create(name="Background Color", - content="", - date_modified=now) - - if not brand_image_height: - general_category.setting_set.create(name="Brand Image Height", - content="", - date_modified=now) - - if not brand_image_width: - general_category.setting_set.create(name="Brand Image Width", - content="", - date_modified=now) - - if not brand_image_padding: - general_category.setting_set.create(name="Brand Image Padding", - content="", - date_modified=now) - - general_category.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_config', '0004_auto_20150424_2050'), - ] - - operations = [ - migrations.RunPython(settings12to13), - ] diff --git a/tethys_config/migrations/0006_auto_20170603_0419.py b/tethys_config/migrations/0006_auto_20170603_0419.py deleted file mode 100644 index fff0dcd4d..000000000 --- a/tethys_config/migrations/0006_auto_20170603_0419.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2017-06-03 04:19 -from __future__ import unicode_literals -from django.db import migrations -from django.utils import timezone - - -def settings14to20(apps, schema_editor): - """ - Update settings to be compatible with 2.0: - * Remove /static/ from all static files paths. - """ - # Figure out what time it is right now - now = timezone.now() - - # Get current settings - Setting = apps.get_model('tethys_config', 'Setting') - all_settings = Setting.objects.all() - - for setting in all_settings: - setting.content = setting.content.replace('/static/', '') - setting.content = setting.content.replace('static/', '') - setting.save() - - -def settings20to14(apps, schema_editor): - """ - Reverse updates applied while migrating from 2.0 to 1.4. - """ - # nothing to reverse really... - pass - - -class Migration(migrations.Migration): - - dependencies = [ - ('tethys_config', '0005_auto_20151023_1720'), - ] - - operations = [ - migrations.RunPython(settings14to20, settings20to14), - ] diff --git a/tethys_config/models.py b/tethys_config/models.py index 7bc004d15..3b47c4a76 100644 --- a/tethys_config/models.py +++ b/tethys_config/models.py @@ -11,13 +11,13 @@ class SettingsCategory(models.Model): - name = models.CharField(max_length=30) + name = models.TextField(max_length=30) class Meta: verbose_name = 'Settings Category' verbose_name_plural = 'Site Settings' - def __unicode__(self): + def __str__(self): return self.name @@ -27,7 +27,7 @@ class Setting(models.Model): date_modified = models.DateTimeField('date modified', auto_now=True) category = models.ForeignKey(SettingsCategory) - def __unicode__(self): + def __str__(self): return self.name @classmethod diff --git a/tethys_config/tests.py b/tethys_config/tests.py deleted file mode 100644 index f785d1b21..000000000 --- a/tethys_config/tests.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -******************************************************************************** -* Name: tests.py -* Author: Nathan Swain -* Created On: 2014 -* Copyright: (c) Brigham Young University 2014 -* License: BSD 2-Clause -******************************************************************************** -""" -from django.test import TestCase - -# Create your tests here. diff --git a/tethys_config/views.py b/tethys_config/views.py index 00fa4bbb1..e69de29bb 100644 --- a/tethys_config/views.py +++ b/tethys_config/views.py @@ -1,12 +0,0 @@ -""" -******************************************************************************** -* Name: views.py -* Author: Nathan Swain -* Created On: 2014 -* Copyright: (c) Brigham Young University 2014 -* License: BSD 2-Clause -******************************************************************************** -""" -from django.shortcuts import render - -# Create your views here. diff --git a/tethys_gizmos/__init__.py b/tethys_gizmos/__init__.py index e57838cbb..e3d1e52cc 100644 --- a/tethys_gizmos/__init__.py +++ b/tethys_gizmos/__init__.py @@ -6,4 +6,4 @@ * Copyright: (c) Brigham Young University 2014 * License: BSD 2-Clause ******************************************************************************** -""" \ No newline at end of file +""" diff --git a/tethys_gizmos/admin.py b/tethys_gizmos/admin.py index 94f5292fc..bd1fee0b0 100644 --- a/tethys_gizmos/admin.py +++ b/tethys_gizmos/admin.py @@ -7,6 +7,6 @@ * License: BSD 2-Clause ******************************************************************************** """ -from django.contrib import admin +# from django.contrib import admin # Register your models here. diff --git a/tethys_gizmos/context_processors.py b/tethys_gizmos/context_processors.py index 217a23365..f4aa089a1 100644 --- a/tethys_gizmos/context_processors.py +++ b/tethys_gizmos/context_processors.py @@ -16,4 +16,4 @@ def tethys_gizmos_context(request): # Setup variables context = {'gizmos_rendered': []} - return context \ No newline at end of file + return context diff --git a/tethys_gizmos/gizmo_options/__init__.py b/tethys_gizmos/gizmo_options/__init__.py index 9916965d4..39a901f87 100644 --- a/tethys_gizmos/gizmo_options/__init__.py +++ b/tethys_gizmos/gizmo_options/__init__.py @@ -7,6 +7,7 @@ * License: BSD 2-Clause ******************************************************************************** """ +# flake8: noqa from .date_picker import * from .button import * from .range_slider import * diff --git a/tethys_gizmos/gizmo_options/base.py b/tethys_gizmos/gizmo_options/base.py index 1fcaa39f2..3febd7cb0 100644 --- a/tethys_gizmos/gizmo_options/base.py +++ b/tethys_gizmos/gizmo_options/base.py @@ -11,13 +11,14 @@ from past.builtins import basestring + class TethysGizmoOptions(dict): """ Base class for Tethys Gizmo Options objects. """ - + gizmo_name = "tethys_gizmo_options" - + def __init__(self, attributes={}, classes=''): """ Constructor for Tethys Gizmo Options base. @@ -36,7 +37,7 @@ def __init__(self, attributes={}, classes=''): pairs = [x.strip().strip('\'').strip('\"') for x in pairs] attributes = dict() for i in range(1, len(pairs), 2): - attributes[pairs[i]] = pairs[i+1] + attributes[pairs[i]] = pairs[i + 1] self.attributes = attributes self.classes = classes @@ -58,7 +59,7 @@ def get_tethys_gizmos_css(): @staticmethod def get_vendor_js(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ return () @@ -66,7 +67,7 @@ def get_vendor_js(): @staticmethod def get_gizmo_js(): """ - JavaScript specific to gizmo to be placed in the + JavaScript specific to gizmo to be placed in the {% block scripts %} block """ return () @@ -74,7 +75,7 @@ def get_gizmo_js(): @staticmethod def get_vendor_css(): """ - CSS vendor libraries to be placed in the + CSS vendor libraries to be placed in the {% block styles %} block """ return () @@ -82,11 +83,12 @@ def get_vendor_css(): @staticmethod def get_gizmo_css(): """ - CSS specific to gizmo to be placed in the - {% block content_dependent_styles %} block + CSS specific to gizmo to be placed in the + {% block content_dependent_styles %} block """ return () - + + class SecondaryGizmoOptions(dict): """ Base class for Secondary Tethys Gizmo Options objects. @@ -100,4 +102,4 @@ def __init__(self): super(SecondaryGizmoOptions, self).__init__() # Dictionary magic - self.__dict__ = self \ No newline at end of file + self.__dict__ = self diff --git a/tethys_gizmos/gizmo_options/bokeh_view.py b/tethys_gizmos/gizmo_options/bokeh_view.py index 3de643db3..bee9494d0 100644 --- a/tethys_gizmos/gizmo_options/bokeh_view.py +++ b/tethys_gizmos/gizmo_options/bokeh_view.py @@ -1,7 +1,7 @@ # coding=utf-8 from bokeh.embed import components from bokeh.resources import CDN - + from .base import TethysGizmoOptions __all__ = ['BokehView'] @@ -10,7 +10,7 @@ class BokehView(TethysGizmoOptions): """ Simple options object for Bokeh plotting. - + .. note:: For more information about Bokeh and for Python examples, see http://bokeh.pydata.org. Attributes: @@ -20,27 +20,27 @@ class BokehView(TethysGizmoOptions): attributes(Optional[dict]): Dictionary of attributed to add to the outer div. classes(Optional[str]): Space separated string of classes to add to the outer div. hidden(Optional[bool]): If True, the plot will be hidden. Default is False. - + Controller Code Example:: - + from tethys_sdk.gizmos import BokehView from bokeh.plotting import figure - + plot = figure(plot_height=300) plot.circle([1,2], [3,4]) my_bokeh_view = BokehView(plot, height="300px") context = {'bokeh_view_input': my_bokeh_view} - + Template Code Example:: - + {% load tethys_gizmos %} - + {% gizmo bokeh_view_input %} """ gizmo_name = "bokeh_view" - def __init__(self, plot_input, height='520px', width='100%', + def __init__(self, plot_input, height='520px', width='100%', attributes='', classes='', divid='', hidden=False): """ Constructor @@ -56,15 +56,15 @@ def __init__(self, plot_input, height='520px', width='100%', @staticmethod def get_vendor_css(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ return CDN.css_files - + @staticmethod def get_vendor_js(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ - return CDN.js_files \ No newline at end of file + return CDN.js_files diff --git a/tethys_gizmos/gizmo_options/button.py b/tethys_gizmos/gizmo_options/button.py index d40180471..6df1e936e 100644 --- a/tethys_gizmos/gizmo_options/button.py +++ b/tethys_gizmos/gizmo_options/button.py @@ -14,7 +14,7 @@ class ButtonGroup(TethysGizmoOptions): """ - The button group gizmo can be used to generate a single button or a group of buttons. Groups of buttons can be stacked horizontally or vertically. For a single button, specify a button group with one button. This gizmo is a wrapper for Twitter Bootstrap buttons. + The button group gizmo can be used to generate a single button or a group of buttons. Groups of buttons can be stacked horizontally or vertically. For a single button, specify a button group with one button. This gizmo is a wrapper for Twitter Bootstrap buttons. Attributes: buttons(list, required): A list of dictionaries where each dictionary contains the options for a button. @@ -68,7 +68,7 @@ class ButtonGroup(TethysGizmoOptions): {% gizmo horizontal_buttons %} {% gizmo vertical_buttons %} - """ + """ # noqa: E501 gizmo_name = "button_group" def __init__(self, buttons, vertical=False, attributes='', classes=''): @@ -92,7 +92,7 @@ class Button(TethysGizmoOptions): href(str): Link for anchor type buttons. submit(bool): Set this to true to make the button a submit type button for forms. disabled(bool): Set the disabled state. - attributes(dict): A dictionary representing additional HTML attributes to add to the primary element (e.g. {"onclick": "run_me();"}). + attributes(dict): A dictionary representing additional HTML attributes to add to the primary element (e.g. {"onclick": "run_me();"}). classes(str): Additional classes to add to the primary HTML element (e.g. "example-class another-class"). Controller Example @@ -191,7 +191,7 @@ class Button(TethysGizmoOptions): {% gizmo remove_button %} {% gizmo previous_button %} {% gizmo next_button %} - """ + """ # noqa: E501 gizmo_name = "button" def __init__(self, display_text='', name='', style='', icon='', href='', @@ -208,4 +208,4 @@ def __init__(self, display_text='', name='', style='', icon='', href='', self.icon = icon self.href = href self.submit = submit - self.disabled = disabled \ No newline at end of file + self.disabled = disabled diff --git a/tethys_gizmos/gizmo_options/datatable_view.py b/tethys_gizmos/gizmo_options/datatable_view.py index b6b2426f5..c257c4b29 100644 --- a/tethys_gizmos/gizmo_options/datatable_view.py +++ b/tethys_gizmos/gizmo_options/datatable_view.py @@ -12,10 +12,12 @@ __all__ = ['DataTableView'] + class DataTableView(TethysGizmoOptions): """ - Table views can be used to display tabular data. The table view gizmo can be configured to have columns that are editable. When used in this capacity, embed the table view in a form with a submit button. - + Table views can be used to display tabular data. The table view gizmo can be configured to have columns that are + editable. When used in this capacity, embed the table view in a form with a submit button. + .. note:: The current version of DataTables in Tethys Platform is 1.10.12. Attributes: @@ -43,14 +45,14 @@ class DataTableView(TethysGizmoOptions): context = { 'datatable_view': datatable_default} - Regular Template Example + Regular Template Example :: - + {% load tethys_gizmos %} - + {% gizmo datatable_view %} - + .. note:: You can also add extensions to the data table view as shown in the next example. To learn more about DataTable extensions, go to https://datatables.net/extensions/index. @@ -66,35 +68,35 @@ class DataTableView(TethysGizmoOptions): ('Bob', 26, 'boss')], colReorder=True, ) - + context = { 'datatable_with_extension': datatable_with_extension} - ColReorder Template Example + ColReorder Template Example :: {% load tethys_gizmos %} - + #LOAD IN EXTENSION JAVASCRIPT/CSS {% block global_scripts %} {{ block.super }} {% endblock %} - + {% block styles %} {{ block.super }} {% endblock %} #END LOAD IN EXTENSION JAVASCRIPT/CSS - + {% gizmo datatable_with_extension %} - """ - ##UNSUPPORTED_EXTENSIONS = ('autoFill', 'select', 'keyTable', 'rowReorder') - ##SUPPORTED_EXTENSIONS = ('buttons', 'colReorder', 'fizedColumns', - ## 'fixedHeader', 'responsive', 'scroller') + """ # noqa: E501 + # UNSUPPORTED_EXTENSIONS = ('autoFill', 'select', 'keyTable', 'rowReorder') + # SUPPORTED_EXTENSIONS = ('buttons', 'colReorder', 'fizedColumns', + # 'fixedHeader', 'responsive', 'scroller') gizmo_name = "datatable_view" - + def __init__(self, rows, column_names, footer=False, attributes={}, classes='', **kwargs): """ Constructor @@ -105,23 +107,23 @@ def __init__(self, rows, column_names, footer=False, attributes={}, classes='', self.rows = rows self.column_names = column_names self.footer = footer - self.datatable_options = {} + self.datatable_options = {} for key, value in kwargs.items(): - data_name = re.sub("([a-z])([A-Z])","\g<1>-\g<2>",key).lower() + data_name = re.sub("([a-z])([A-Z])", "\g<1>-\g<2>", key).lower() self.datatable_options[data_name] = dumps(value) - + @staticmethod def get_vendor_css(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ return ('https://cdn.datatables.net/1.10.12/css/jquery.dataTables.min.css',) - + @staticmethod def get_vendor_js(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ return ('https://cdn.datatables.net/1.10.12/js/jquery.dataTables.min.js',) @@ -129,7 +131,7 @@ def get_vendor_js(): @staticmethod def get_gizmo_js(): """ - JavaScript specific to gizmo to be placed in the + JavaScript specific to gizmo to be placed in the {% block scripts %} block """ - return ('tethys_gizmos/js/datatable_view.js',) \ No newline at end of file + return ('tethys_gizmos/js/datatable_view.js',) diff --git a/tethys_gizmos/gizmo_options/date_picker.py b/tethys_gizmos/gizmo_options/date_picker.py index 04784802d..781e7d134 100644 --- a/tethys_gizmos/gizmo_options/date_picker.py +++ b/tethys_gizmos/gizmo_options/date_picker.py @@ -14,7 +14,7 @@ class DatePicker(TethysGizmoOptions): """ - Date pickers are used to make the input of dates streamlined and easy. Rather than typing the date, the user is presented with a calendar to select the date. This date picker was implemented using `Bootstrap Datepicker `_. + Date pickers are used to make the input of dates streamlined and easy. Rather than typing the date, the user is presented with a calendar to select the date. This date picker was implemented using `Bootstrap Datepicker `_. Attributes: name (str, required): Name of the input element that will be used for form submission. @@ -74,7 +74,7 @@ class DatePicker(TethysGizmoOptions): {% gizmo date_picker %} {% gizmo date_picker_error %} - """ + """ # noqa: E501 gizmo_name = "date_picker" def __init__(self, name, display_text='', autoclose=False, calendar_weeks=False, clear_button=False, @@ -106,19 +106,18 @@ def __init__(self, name, display_text='', autoclose=False, calendar_weeks=False, self.disabled = disabled self.error = error - @staticmethod def get_vendor_css(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ return ('tethys_gizmos/vendor/bootstrap_datepicker/css/datepicker3.css',) - + @staticmethod def get_vendor_js(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ - return ('tethys_gizmos/vendor/bootstrap_datepicker/js/bootstrap_datepicker.js',) \ No newline at end of file + return ('tethys_gizmos/vendor/bootstrap_datepicker/js/bootstrap_datepicker.js',) diff --git a/tethys_gizmos/gizmo_options/esri_map.py b/tethys_gizmos/gizmo_options/esri_map.py index 5d5a735bb..32834ef16 100644 --- a/tethys_gizmos/gizmo_options/esri_map.py +++ b/tethys_gizmos/gizmo_options/esri_map.py @@ -12,7 +12,7 @@ class ESRIMap(TethysGizmoOptions): Attributes height(string, required): Height of map container in normal css units width(string, required): Width of map container in normal css units - basemap(string, required): Basemap layer. Values=[streets,satellite,hybrid,topo,gray,dark-gray,oceans,national-geographic,terrain,osm,dark-gray-vector,gray-vector,street-vector,topo-vector,streets-night-vector,streets-relief-vector,streets-navigation-vector] + basemap(string, required): Basemap layer. Values=[streets,satellite,hybrid,topo,gray,dark-gray,oceans, national-geographic,terrain,osm,dark-gray-vector,gray-vector,street-vector, topo-vector,streets-night-vector,streets-relief-vector,streets-navigation-vector] zoom(string,required): Zoom Level of the Basemap. view(EMView): An EVView object specifying the initial view or extent for the map @@ -31,10 +31,10 @@ class ESRIMap(TethysGizmoOptions): {% gizmo esri_map_view_options %} - """ + """ # noqa: E501 gizmo_name = "esri_map" - def __init__(self, height='100%', width='100%', basemap='topo',view={'center':[-100,40],'zoom':2},layers=[]): + def __init__(self, height='100%', width='100%', basemap='topo', view={'center': [-100, 40], 'zoom': 2}, layers=[]): """ Constructor """ @@ -90,7 +90,7 @@ class EMView(SecondaryGizmoOptions): zoom=3.5, ) - """ + """ # noqa: E501 def __init__(self, center, zoom): """ Constructor @@ -122,12 +122,12 @@ class EMLayer(SecondaryGizmoOptions): esri_image_layer = EMLayer(type='ImageryLayer', url='https://sampleserver6.arcgisonline.com/arcgis/rest/services/NLCDLandCover2001/ImageServer') - """ - def __init__(self,type,url): + """ # noqa: E501 + def __init__(self, type, url): """ Constructor """ - #Initialize super class - super(EMLayer,self).__init__() + # Initialize super class + super(EMLayer, self).__init__() self.type = type self.url = url diff --git a/tethys_gizmos/gizmo_options/google_map_view.py b/tethys_gizmos/gizmo_options/google_map_view.py index df0b794f2..3b129306b 100644 --- a/tethys_gizmos/gizmo_options/google_map_view.py +++ b/tethys_gizmos/gizmo_options/google_map_view.py @@ -119,9 +119,9 @@ class GoogleMapView(TethysGizmoOptions): {% gizmo google_map_view_options %} - """ + """ # noqa:E501 gizmo_name = "google_map_view" - + def __init__(self, height, width, maps_api_key="", reference_kml_action="", drawing_types_enabled=[], initial_drawing_mode="", output_format='GEOJSON', input_overlays=[None], attributes={}, classes=''): """ @@ -138,19 +138,19 @@ def __init__(self, height, width, maps_api_key="", reference_kml_action="", draw self.initial_drawing_mode = initial_drawing_mode self.output_format = output_format self.input_overlays = input_overlays - + @staticmethod def get_vendor_js(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ return ('tethys_gizmos/vendor/farbtastic/farbtastic.js',) - @staticmethod + @staticmethod def get_vendor_css(): """ - CSS vendor libraries to be placed in the + CSS vendor libraries to be placed in the {% block styles %} block """ return ('tethys_gizmos/vendor/farbtastic/farbtastic.css',) @@ -158,7 +158,7 @@ def get_vendor_css(): @staticmethod def get_gizmo_js(): """ - JavaScript specific to gizmo to be placed in the + JavaScript specific to gizmo to be placed in the {% block scripts %} block """ return ('tethys_gizmos/js/tethys_google_map_view.js',) diff --git a/tethys_gizmos/gizmo_options/jobs_table.py b/tethys_gizmos/gizmo_options/jobs_table.py index b11b90333..9785c12db 100644 --- a/tethys_gizmos/gizmo_options/jobs_table.py +++ b/tethys_gizmos/gizmo_options/jobs_table.py @@ -61,9 +61,9 @@ class JobsTable(TethysGizmoOptions): {% gizmo jobs_table_options %} - """ + """ # noqa: E501 gizmo_name = "jobs_table" - + def __init__(self, jobs, column_fields, status_actions=True, run_btn=True, delete_btn=True, results_url='', hover=False, striped=False, bordered=False, condensed=False, attributes={}, classes='', refresh_interval=5000, delay_loading_status=True): @@ -74,9 +74,11 @@ def __init__(self, jobs, column_fields, status_actions=True, run_btn=True, delet super(JobsTable, self).__init__(attributes=attributes, classes=classes) self.jobs = jobs - self.rows = self.get_rows(jobs, column_fields) - self.column_fields = column_fields - self.column_names = [col_name.title().replace('_', ' ') for col_name in column_fields] + self.rows = None + self.column_fields = None + self.column_names = None + self.set_rows_and_columns(jobs, column_fields) + self.status_actions = status_actions self.run = run_btn self.delete = delete_btn @@ -90,45 +92,66 @@ def __init__(self, jobs, column_fields, status_actions=True, run_btn=True, delet self.refresh_interval = refresh_interval self.delay_loading_status = delay_loading_status - @classmethod - def get_rows(cls, jobs, column_fields): - rows = [] - column_names = [] + def set_rows_and_columns(self, jobs, column_fields): + self.rows = list() + self.column_fields = list() + self.column_names = list() + + if len(jobs) == 0: + return + + first_job = jobs[0] + for field in column_fields: + column_name = field.title().replace('_', ' ') + try: + getattr(first_job, field) # verify that the field name is a valid attribute on the job + self.column_names.append(column_name) + self.column_fields.append(field) + except AttributeError: + log.warning('Column %s was not added because the %s has no attribute %s.', + column_name, str(first_job), field) + for job in jobs: - row_values = [] - for attribute in column_fields: - column_name = attribute.title().replace('_', ' ') - if hasattr(job, attribute): - value = getattr(job, attribute) - # Truncate fractional seconds - if attribute == 'run_time': - # times = [] - # total_seconds = value.seconds - # times.append(('days', run_time.days)) - # times.append(('hr', total_seconds/3600)) - # times.append(('min', (total_seconds%3600)/60)) - # times.append(('sec', total_seconds%60)) - # run_time_str = '' - # for time_str, time in times: - # if time: - # run_time_str += "%s %s " % (time, time_str) - # if not run_time_str or (run_time.days == 0 and total_seconds < 2): - # run_time_str = '%.2f sec' % (total_seconds + float(run_time.microseconds)/1000000,) - value = str(value).split('.')[0] - row_values.append(value) - else: - log.waring('Column %s was not added because %s Job %s has no attribute %s.', - column_name, str(job), attribute) - - rows.append(row_values) - column_names.append(column_name) - return rows + row_values = self.get_row(job, self.column_fields) + self.rows.append(row_values) + + @staticmethod + def get_row(job, job_attributes): + """Get the field values for one row (corresponding to one job). + + Args: + job (TethysJob): An instance of a subclass of TethysJob + job_attributes (list): a list of attribute names corresponding to the fields in the jobs table + + Returns: + A list of field values for one row. + + """ + row_values = list() + for attribute in job_attributes: + value = getattr(job, attribute) + # Truncate fractional seconds + if attribute == 'run_time': + # times = [] + # total_seconds = value.seconds + # times.append(('days', run_time.days)) + # times.append(('hr', total_seconds/3600)) + # times.append(('min', (total_seconds%3600)/60)) + # times.append(('sec', total_seconds%60)) + # run_time_str = '' + # for time_str, time in times: + # if time: + # run_time_str += "%s %s " % (time, time_str) + # if not run_time_str or (run_time.days == 0 and total_seconds < 2): + # run_time_str = '%.2f sec' % (total_seconds + float(run_time.microseconds)/1000000,) + value = str(value).split('.')[0] + row_values.append(value) + + return row_values @staticmethod def get_gizmo_js(): """ - JavaScript specific to gizmo to be placed in the - {% block scripts %} block + JavaScript specific to gizmo to be placed in the {% block scripts %} block """ return ('tethys_gizmos/js/jobs_table.js',) - diff --git a/tethys_gizmos/gizmo_options/map_view.py b/tethys_gizmos/gizmo_options/map_view.py index eaf8eac32..7a9c06c7f 100644 --- a/tethys_gizmos/gizmo_options/map_view.py +++ b/tethys_gizmos/gizmo_options/map_view.py @@ -9,13 +9,16 @@ """ from .base import TethysGizmoOptions, SecondaryGizmoOptions from django.conf import settings +import logging +log = logging.getLogger('tethys.tethys_gizmos.gizmo_options.map_view') -__all__ = ['MapView', 'MVDraw', 'MVView', 'MVLayer', 'MVLegendClass', 'MVLegendImageClass', 'MVLegendGeoServerImageClass'] +__all__ = ['MapView', 'MVDraw', 'MVView', 'MVLayer', + 'MVLegendClass', 'MVLegendImageClass', 'MVLegendGeoServerImageClass'] class MapView(TethysGizmoOptions): """ - The Map View gizmo can be used to produce interactive maps of spatial data. It is powered by OpenLayers 4, a free and open source pure javascript mapping library. It supports layers in a variety of different formats including WMS, Tiled WMS, GeoJSON, KML, and ArcGIS REST. It includes drawing capabilities and the ability to create a legend for the layers included in the map. + The Map View gizmo can be used to produce interactive maps of spatial data. It is powered by OpenLayers, a free and open source pure javascript mapping library. It supports layers in a variety of different formats including WMS, Tiled WMS, GeoJSON, KML, and ArcGIS REST. It includes drawing capabilities and the ability to create a legend for the layers included in the map. Shapes that are drawn on the map by users can be retrieved from the map via a hidden text field named 'geometry' and it is updated every time the map is changed. The text in the text field is a string representation of JSON. The geometry definition contained in this JSON can be formatted as either GeoJSON or Well Known Text. This can be configured via the output_format option of the MVDraw object. If the Map View is embedded in a form, the geometry that is drawn on the map will automatically be submitted with the rest of the form via the hidden text field. @@ -44,15 +47,36 @@ class MapView(TethysGizmoOptions): **Base Maps** - There are three base maps supported by the Map View gizmo: OpenStreetMap, Bing, and MapQuest. Use the following links to learn about the additional options you can configure the base maps with: + There are several base maps supported by the Map View gizmo: `OpenStreetMap`, `Bing`, `Stamen`, `CartoDB`, and `ESRI`. All base maps can be specified as a string or as an options dictionary. When using an options dictionary all base maps map services accept the option `control_label`, which is used to specify the label to be used in the Base Map control. For example:: - * Bing: `ol.source.BingMaps `_ - * MapQuest: `ol.source.MapQuest `_ - * OpenStreetMap: `ol.source.OSM `_ + {'Bing': {'key': 'Ap|k3yheRE', 'imagerySet': 'Aerial', 'control_label': 'Bing Aerial'}} - :: + For additional options that can be provided to each base map service see the following links: + + * OpenStreetMap: `ol/source/OSM `_ + * Bing: `ol/source/BingMaps `_ + * Stamen: `ol/source/Stamen `_ + * XYZ `ol/source/XYZ `_ + + .. note:: + + The CartoDB and ESRI services are just pre-defined instances of the XYZ service. In addition to the standard XYZ options they have the following additional options: - {'Bing': {'key': 'Ap|k3yheRE', 'imagerySet': 'Aerial'}} + CartoDB: + * `style`: The style of map. Possibilities are 'light' or 'dark'. + * `labels`: Boolean specifying whether or not to include labels. + + ESRI: + * `layer`: A string specifying which ESRI map to use. Possibilities are: + * NatGeo_World_Map + * Ocean_Basemap + * USA_Topo_Maps + * World_Imagery + * World_Physical_Map + * World_Shaded_Relief + * World_Street_Map + * World_Terrain_Base + * World_Topo_Map **Controls** @@ -72,6 +96,15 @@ class MapView(TethysGizmoOptions): * multiselect: Set to True to allow multiple features to be selected while holding the shift key on the keyboard. Defaults to False. * sensitivity: Integer value that adjust the feature selection sensitivity. Defaults to 2. + .. tip:: + + **OpenLayers Version** + + Currently, OpenLayers version 5.3.0 is used by default with the Map View gizmo. If you need a specific version of OpenLayers you can specify the version number using the `ol_version` class attribute on the `MapView` class:: + + MapView.ol_version = '4.6.5' + + Any versions that are provided by https://www.jsdelivr.com/package/npm/openlayers can be specified. Controller Example @@ -216,6 +249,35 @@ class MapView(TethysGizmoOptions): legend_extent=[-173, 17, -65, 72] ) + # Define base map options + esri_layer_names = [ + 'NatGeo_World_Map', + 'Ocean_Basemap', + 'USA_Topo_Maps', + 'World_Imagery', + 'World_Physical_Map', + 'World_Shaded_Relief', + 'World_Street_Map', + 'World_Terrain_Base', + 'World_Topo_Map', + ] + esri_layers = [{'ESRI': {'layer': l}} for l in esri_layer_names] + basemaps = [ + 'Stamen', + {'Stamen': {'layer': 'toner', 'control_label': 'Black and White'}}, + {'Stamen': {'layer': 'watercolor'}}, + 'OpenStreetMap', + 'CartoDB', + {'CartoDB': {'style': 'dark'}}, + {'CartoDB': {'style': 'light', 'labels': False, 'control_label': 'CartoDB-light-no-labels'}}, + {'XYZ': {'url': 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png', 'control_label': 'Wikimedia'}} + 'ESRI', + ] + basemaps.extend(esri_layers) + + # Specify OpenLayers version + MapView.ol_version = '5.3.0' + # Define map view options map_view_options = MapView( height='600px', @@ -225,7 +287,7 @@ class MapView(TethysGizmoOptions): {'ZoomToExtent': {'projection': 'EPSG:4326', 'extent': [-130, 22, -65, 54]}}], layers=[geojson_layer, geojson_point_layer, geoserver_layer, kml_layer, arc_gis_layer], view=view_options, - basemap='OpenStreetMap', + basemap=basemaps, draw=drawing_options, legend=True ) @@ -240,10 +302,14 @@ class MapView(TethysGizmoOptions): {% gizmo map_view_options %} - """ + """ # noqa: E501 gizmo_name = "map_view" + ol_version = '5.3.0' + cdn = 'https://cdn.jsdelivr.net/npm/openlayers@{version}/dist/ol{debug}.{ext}' + alternate_cdn = 'https://cdnjs.cloudflare.com/ajax/libs/openlayers/{version}/ol{debug}.{ext}' + local_url = 'tethys_gizmos/vendor/openlayers/{version}/ol.{ext}' - def __init__(self, height='100%', width='100%', basemap='OpenStreetMap', view={'center': [-100, 40], 'zoom': 2}, + def __init__(self, height='100%', width='100%', basemap=None, view={'center': [-100, 40], 'zoom': 2}, controls=[], layers=[], draw=None, legend=False, attributes={}, classes='', disable_basemap=False, feature_selection=None): """ @@ -263,16 +329,28 @@ def __init__(self, height='100%', width='100%', basemap='OpenStreetMap', view={' self.disable_basemap = disable_basemap self.feature_selection = feature_selection - @staticmethod - def get_vendor_js(): + @classmethod + def static_url(cls): + return cls.cdn if cls.ol_version != '5.3.0' else cls.local_url + + @classmethod + def debug(cls): + # Note: Since version 5 OpenLayers now uses source maps instead of a '-debug' version of the code + return '-debug' if settings.DEBUG and int(cls.ol_version[0]) < 5 else '' + + @classmethod + def get_vendor_js(cls): """ JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ - openlayers_library = 'tethys_gizmos/vendor/openlayers/ol.js' - if settings.DEBUG: - openlayers_library = 'tethys_gizmos/vendor/openlayers/ol-debug.js' - return (openlayers_library,) + openlayers_library = cls.static_url().format( + version=cls.ol_version, + debug=cls.debug(), + ext='js' + ) + + return openlayers_library, @staticmethod def get_gizmo_js(): @@ -283,13 +361,19 @@ def get_gizmo_js(): return ('tethys_gizmos/js/gizmo_utilities.js', 'tethys_gizmos/js/tethys_map_view.js') - @staticmethod - def get_vendor_css(): + @classmethod + def get_vendor_css(cls): """ CSS vendor libraries to be placed in the {% block styles %} block """ - return ('tethys_gizmos/vendor/openlayers/ol.css',) + openlayers_css = cls.static_url().format( + version=cls.ol_version, + debug=cls.debug(), + ext='css' + ) + + return openlayers_css, @staticmethod def get_gizmo_css(): @@ -297,7 +381,7 @@ def get_gizmo_css(): CSS specific to gizmo to be placed in the {% block content_dependent_styles %} block """ - return ('tethys_gizmos/css/tethys_map_view.min.css',) + return 'tethys_gizmos/css/tethys_map_view.min.css', class MVView(SecondaryGizmoOptions): @@ -323,7 +407,7 @@ class MVView(SecondaryGizmoOptions): minZoom=2 ) - """ + """ # noqa: E501 def __init__(self, projection, center, zoom, maxZoom=28, minZoom=0): """ @@ -364,9 +448,11 @@ class MVDraw(SecondaryGizmoOptions): point_color='#663399' ) - """ + """ # noqa: E501 - def __init__(self, controls, initial, output_format='GeoJSON',line_color="#ffcc33",fill_color='rgba(255, 255, 255, 0.2)',point_color="#ffcc33"): + def __init__(self, controls, initial, output_format='GeoJSON', + line_color="#ffcc33", fill_color='rgba(255, 255, 255, 0.2)', + point_color="#ffcc33"): """ Constructor """ @@ -374,7 +460,6 @@ def __init__(self, controls, initial, output_format='GeoJSON',line_color="#ffcc3 super(MVDraw, self).__init__() self.controls = controls - # Validate initial if initial not in self.controls: raise ValueError('Value of "initial" must be contained in the "controls" list.') @@ -521,10 +606,12 @@ class MVLayer(SecondaryGizmoOptions): options={'url': 'http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/' + 'Specialty/ESRI_StateCityHighway_USA/MapServer'}, legend_title='ESRI USA Highway', legend_extent=[-173, 17, -65, 72]), - """ + """ # noqa: E501 - def __init__(self, source, options, legend_title, layer_options=None, editable=True, legend_classes=None, legend_extent=None, - legend_extent_projection='EPSG:4326', feature_selection=False, geometry_attribute=None, data={}): + def __init__(self, source, options, legend_title, layer_options=None, editable=True, + legend_classes=None, legend_extent=None, + legend_extent_projection='EPSG:4326', + feature_selection=False, geometry_attribute=None, data=None): """ Constructor """ @@ -540,11 +627,11 @@ def __init__(self, source, options, legend_title, layer_options=None, editable=T self.legend_extent_projection = legend_extent_projection self.feature_selection = feature_selection self.geometry_attribute = geometry_attribute - self.data = data + self.data = data or dict() - # TODO: this should be a log if feature_selection and not geometry_attribute: - print("WARNING: geometry_attribute not defined -using default value 'the_geom'") + log.warning("geometry_attribute not defined -using default value 'the_geom'") + class MVLegendClass(SecondaryGizmoOptions): """ @@ -565,7 +652,7 @@ class MVLegendClass(SecondaryGizmoOptions): line_class = MVLegendClass(type='line', value='Roads', stroke='rbga(0,0,0,0.7)') polygon_class = MVLegendClass(type='polygon', value='Lakes', stroke='#0000aa', fill='#0000ff') - """ + """ # noqa: E501 def __init__(self, type, value, fill='', stroke='', ramp=[]): """ @@ -616,8 +703,6 @@ def __init__(self, type, value, fill='', stroke='', ramp=[]): self.ramp = ramp else: raise ValueError('Argument "ramp" must be specified for MVLegendClass of type "raster".') - else: - raise ValueError('Invalid type specified: {0}.'.format(type)) class MVLegendImageClass(SecondaryGizmoOptions): @@ -635,7 +720,7 @@ class MVLegendImageClass(SecondaryGizmoOptions): image_class = MVLegendImageClass(value='Cities', image_url='https://upload.wikimedia.org/wikipedia/commons/d/da/The_City_London.jpg' ) - """ + """ # noqa: E501 def __init__(self, value, image_url): """ @@ -648,6 +733,7 @@ def __init__(self, value, image_url): self.value = value self.image_url = image_url + class MVLegendGeoServerImageClass(MVLegendImageClass): """ MVLegendGeoServerImageClasses are used to define the classes listed in the legend using the GeoServer generated legend. @@ -670,7 +756,7 @@ class MVLegendGeoServerImageClass(MVLegendImageClass): layer='rivers', width=20, height=10) - """ + """ # noqa: E501 def __init__(self, value, geoserver_url, style, layer, width=20, height=10): """ diff --git a/tethys_gizmos/gizmo_options/message_box.py b/tethys_gizmos/gizmo_options/message_box.py index c45c11f2e..573ad89ed 100644 --- a/tethys_gizmos/gizmo_options/message_box.py +++ b/tethys_gizmos/gizmo_options/message_box.py @@ -56,7 +56,7 @@ class MessageBox(TethysGizmoOptions): {% block after_app_content %} {% gizmo message_box %} {% endblock %} - """ + """ # noqa: E501 gizmo_name = "message_box" def __init__(self, name, title, message='', dismiss_button='Cancel', affirmative_button='Ok', diff --git a/tethys_gizmos/gizmo_options/plot_view.py b/tethys_gizmos/gizmo_options/plot_view.py index 07f059024..e6caec933 100644 --- a/tethys_gizmos/gizmo_options/plot_view.py +++ b/tethys_gizmos/gizmo_options/plot_view.py @@ -8,7 +8,7 @@ class PlotViewBase(TethysGizmoOptions): """ Plot view classes inherit from this class. - """ + """ gizmo_name = "plot_view" def __init__(self, width='500px', height='500px', engine='d3'): @@ -30,7 +30,7 @@ def __init__(self, width='500px', height='500px', engine='d3'): @staticmethod def get_vendor_js(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ return ('tethys_gizmos/vendor/highcharts/js/highcharts.js', @@ -42,7 +42,7 @@ def get_vendor_js(): @staticmethod def get_gizmo_js(): """ - JavaScript specific to gizmo to be placed in the + JavaScript specific to gizmo to be placed in the {% block scripts %} block """ return ('tethys_gizmos/js/plot_view.js',) @@ -50,8 +50,8 @@ def get_gizmo_js(): @staticmethod def get_gizmo_css(): """ - CSS specific to gizmo to be placed in the - {% block content_dependent_styles %} block + CSS specific to gizmo to be placed in the + {% block content_dependent_styles %} block """ return ('tethys_gizmos/css/plot_view.css',) @@ -61,7 +61,7 @@ class PlotObject(TethysGizmoOptions): Base Plot Object that is constructed by plot views. """ - def __init__(self, chart={}, title='', subtitle='', legend=None, display_legend=True, + def __init__(self, chart={}, title='', subtitle='', legend=None, display_legend=True, tooltip=True, x_axis={}, y_axis={}, tooltip_format={}, plotOptions={}, **kwargs): """ Constructor @@ -82,11 +82,11 @@ def __init__(self, chart={}, title='', subtitle='', legend=None, display_legend if display_legend: default_legend = { - 'layout': 'vertical', - 'align': 'right', - 'verticalAlign': 'middle', - 'borderWidth': 0 - } + 'layout': 'vertical', + 'align': 'right', + 'verticalAlign': 'middle', + 'borderWidth': 0 + } self.legend = legend or default_legend if tooltip: @@ -668,8 +668,8 @@ def __init__(self, series=[], height='500px', width='500px', engine='d3', title= if group_tools: tooltip_format = { 'headerFormat': '{point.key}', - 'pointFormat': '' + '' % ( - axis_units), + 'pointFormat': '' + + '' % (axis_units), 'footerFormat': '
    {series.name}: {point.y:.1f} %s
    {series.name}: {point.y:.1f} %s
    ', 'shared': True, 'useHTML': True @@ -875,7 +875,7 @@ class AreaRange(PlotViewBase): {% gizmo area_range_plot_object %} - """ + """ # noqa: E501 def __init__(self, series=[], height='500px', width='500px', engine='d3', title='', subtitle='', y_axis_title='', y_axis_units='', **kwargs): @@ -995,7 +995,7 @@ class HeatMap(PlotViewBase): {% gizmo heat_map_plot %} - """ + """ # noqa: E501 def __init__(self, series=[], height='500px', width='500px', engine='d3', title='', subtitle='', x_categories=[], y_categories=[], tooltip_phrase_one='', tooltip_phrase_two='', **kwargs): @@ -1024,7 +1024,9 @@ def __init__(self, series=[], height='500px', width='500px', engine='d3', title= } tooltip_format = { - 'formatter': 'function() {return "" + this.series.xAxis.categories[this.point.x] + " %s
    " + this.point.value + " %s
    " + this.series.yAxis.categories[this.point.y] + "";' % (tooltip_phrase_one, tooltip_phrase_two) + 'formatter': 'function() {return "" + this.series.xAxis.categories[this.point.x] + " %s
    " + ' + 'this.point.value + " %s
    " + this.series.yAxis.categories[this.point.y] + "";' + % (tooltip_phrase_one, tooltip_phrase_two) } # Initialize super class diff --git a/tethys_gizmos/gizmo_options/plotly_view.py b/tethys_gizmos/gizmo_options/plotly_view.py index 53d5677e3..ce4175d2c 100644 --- a/tethys_gizmos/gizmo_options/plotly_view.py +++ b/tethys_gizmos/gizmo_options/plotly_view.py @@ -9,7 +9,7 @@ class PlotlyView(TethysGizmoOptions): """ Simple options object for plotly view. - + .. note:: Information about the Plotly API can be found at https://plot.ly/python. Attributes: @@ -20,9 +20,9 @@ class PlotlyView(TethysGizmoOptions): classes(Optional[str]): Space separated string of classes to add to the outer div. hidden(Optional[bool]): If True, the plot will be hidden. Default is False. show_link(Optional[bool]): If True, the link to export plot to view in plotly is shown. Default is False. - + Controller Code Basic Example:: - + from datetime import datetime import plotly.graph_objs as go from tethys_sdk.gizmos import PlotlyView @@ -30,32 +30,32 @@ class PlotlyView(TethysGizmoOptions): x = [datetime(year=2013, month=10, day=04), datetime(year=2013, month=11, day=05), datetime(year=2013, month=12, day=06)] - + my_plotly_view = PlotlyView([go.Scatter(x=x, y=[1, 3, 6])]) - + context = {'plotly_view_input': my_plotly_view} - + Controller Code Pandas Example:: - + import numpy as np import pandas as pd from tethys_sdk.gizmos import PlotlyView - + df = pd.DataFrame(np.random.randn(1000, 2), columns=['A', 'B']).cumsum() my_plotly_view = PlotlyView(df.iplot(asFigure=True)) context = {'plotly_view_input': my_plotly_view} - + Template Code:: - + {% load tethys_gizmos %} - + {% gizmo plotly_view_input %} """ gizmo_name = "plotly_view" - - def __init__(self, plot_input, height='520px', width='100%', + + def __init__(self, plot_input, height='520px', width='100%', attributes='', classes='', divid='', hidden=False, show_link=False): """ @@ -63,9 +63,9 @@ def __init__(self, plot_input, height='520px', width='100%', """ # Initialize the super class super(PlotlyView, self).__init__() - - self.plotly_div = opy.plot(plot_input, - auto_open=False, + + self.plotly_div = opy.plot(plot_input, + auto_open=False, output_type='div', include_plotlyjs=False, show_link=show_link) @@ -79,7 +79,7 @@ def __init__(self, plot_input, height='520px', width='100%', @staticmethod def get_vendor_js(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ - return ('://plotly-load_from_python.js',) \ No newline at end of file + return ('://plotly-load_from_python.js',) diff --git a/tethys_gizmos/gizmo_options/range_slider.py b/tethys_gizmos/gizmo_options/range_slider.py index f5f432aa7..de450d0c1 100644 --- a/tethys_gizmos/gizmo_options/range_slider.py +++ b/tethys_gizmos/gizmo_options/range_slider.py @@ -62,10 +62,11 @@ class RangeSlider(TethysGizmoOptions): {% gizmo slider1 %} {% gizmo slider2 %} - """ + """ # noqa: E501 gizmo_name = "range_slider" - - def __init__(self, name, min, max, initial, step, disabled=False, display_text='', error='', attributes={}, classes=''): + + def __init__(self, name, min, max, initial, step, disabled=False, display_text='', error='', + attributes={}, classes=''): """ Constructor """ @@ -84,7 +85,7 @@ def __init__(self, name, min, max, initial, step, disabled=False, display_text=' @staticmethod def get_gizmo_js(): """ - JavaScript specific to gizmo to be placed in the + JavaScript specific to gizmo to be placed in the {% block scripts %} block """ - return ('tethys_gizmos/js/range_slider.js',) \ No newline at end of file + return ('tethys_gizmos/js/range_slider.js',) diff --git a/tethys_gizmos/gizmo_options/select_input.py b/tethys_gizmos/gizmo_options/select_input.py index 59ed5725b..84d0a14e7 100644 --- a/tethys_gizmos/gizmo_options/select_input.py +++ b/tethys_gizmos/gizmo_options/select_input.py @@ -43,27 +43,27 @@ class SelectInput(TethysGizmoOptions): initial=['Three'], select2_options={'placeholder': 'Select a number', 'allowClear': True}) - + select_input2_multiple = SelectInput(display_text='Select2 Multiple', name='select21', multiple=True, options=[('One', '1'), ('Two', '2'), ('Three', '3')], initial=['Two', 'One']) - + select_input2_error = SelectInput(display_text='Select2 Disabled', name='select22', multiple=False, options=[('One', '1'), ('Two', '2'), ('Three', '3')], disabled=True, error='Here is my error text') - + select_input = SelectInput(display_text='Select', name='select1', multiple=False, original=True, options=[('One', '1'), ('Two', '2'), ('Three', '3')], initial=['Three']) - + select_input_multiple = SelectInput(display_text='Select Multiple', name='select11', multiple=True, @@ -89,9 +89,9 @@ class SelectInput(TethysGizmoOptions): {% gizmo select_input %} {% gizmo select_input_multiple %} - """ + """ # noqa: E501 gizmo_name = "select_input" - + def __init__(self, name, display_text='', initial=[], multiple=False, original=False, select2_options=None, options='', disabled=False, error='', attributes={}, classes=''): @@ -116,7 +116,7 @@ def __init__(self, name, display_text='', initial=[], multiple=False, original=F @staticmethod def get_vendor_js(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ return ('tethys_gizmos/vendor/select2_4.0.2/js/select2.full.min.js',) @@ -124,7 +124,7 @@ def get_vendor_js(): @staticmethod def get_vendor_css(): """ - CSS vendor libraries to be placed in the + CSS vendor libraries to be placed in the {% block styles %} block """ return ('tethys_gizmos/vendor/select2_4.0.2/css/select2.min.css',) @@ -132,7 +132,7 @@ def get_vendor_css(): @staticmethod def get_gizmo_js(): """ - JavaScript specific to gizmo to be placed in the + JavaScript specific to gizmo to be placed in the {% block scripts %} block """ return ('tethys_gizmos/js/select_input.js',) diff --git a/tethys_gizmos/gizmo_options/table_view.py b/tethys_gizmos/gizmo_options/table_view.py index c2a4201eb..fda9220e7 100644 --- a/tethys_gizmos/gizmo_options/table_view.py +++ b/tethys_gizmos/gizmo_options/table_view.py @@ -64,11 +64,11 @@ class TableView(TethysGizmoOptions): :: {% load tethys_gizmos %} - + {% gizmo table_view %} {% gizmo table_view_edit %} - """ + """ # noqa: E501 gizmo_name = "table_view" def __init__(self, rows, column_names='', hover=False, striped=False, bordered=False, condensed=False, @@ -86,4 +86,4 @@ def __init__(self, rows, column_names='', hover=False, striped=False, bordered=F self.bordered = bordered self.condensed = condensed self.editable_columns = editable_columns - self.row_ids = row_ids \ No newline at end of file + self.row_ids = row_ids diff --git a/tethys_gizmos/gizmo_options/text_input.py b/tethys_gizmos/gizmo_options/text_input.py index fd58c75ea..8f8c73418 100644 --- a/tethys_gizmos/gizmo_options/text_input.py +++ b/tethys_gizmos/gizmo_options/text_input.py @@ -60,9 +60,9 @@ class TextInput(TethysGizmoOptions): {% gizmo text_input %} {% gizmo text_error_input %} - """ + """ # noqa: E501 gizmo_name = "text_input" - + def __init__(self, name, display_text='', initial='', placeholder='', prepend='', append='', icon_prepend='', icon_append='', disabled=False, error='', attributes={}, classes=''): """ @@ -80,4 +80,4 @@ def __init__(self, name, display_text='', initial='', placeholder='', prepend='' self.icon_prepend = icon_prepend self.icon_append = icon_append self.disabled = disabled - self.error = error \ No newline at end of file + self.error = error diff --git a/tethys_gizmos/gizmo_options/toggle_switch.py b/tethys_gizmos/gizmo_options/toggle_switch.py index 1d978147a..ca84084f2 100644 --- a/tethys_gizmos/gizmo_options/toggle_switch.py +++ b/tethys_gizmos/gizmo_options/toggle_switch.py @@ -73,9 +73,9 @@ class ToggleSwitch(TethysGizmoOptions): {% gizmo toggle_switch_styled %} {% gizmo toggle_switch_disabled %} - """ + """ # noqa: E501 gizmo_name = "toggle_switch" - + def __init__(self, name, display_text='', on_label='ON', off_label='OFF', on_style='primary', off_style='default', size='regular', initial=False, disabled=False, error='', attributes={}, classes=''): """ @@ -98,7 +98,7 @@ def __init__(self, name, display_text='', on_label='ON', off_label='OFF', on_sty @staticmethod def get_vendor_js(): """ - JavaScript vendor libraries to be placed in the + JavaScript vendor libraries to be placed in the {% block global_scripts %} block """ return ('tethys_gizmos/vendor/bootstrap_switch/dist/js/bootstrap-switch.min.js',) @@ -106,7 +106,7 @@ def get_vendor_js(): @staticmethod def get_vendor_css(): """ - CSS vendor libraries to be placed in the + CSS vendor libraries to be placed in the {% block styles %} block """ return ('tethys_gizmos/vendor/bootstrap_switch/dist/css/bootstrap3/bootstrap-switch.min.css',) @@ -114,7 +114,7 @@ def get_vendor_css(): @staticmethod def get_gizmo_js(): """ - JavaScript specific to gizmo to be placed in the + JavaScript specific to gizmo to be placed in the {% block scripts %} block """ - return ('tethys_gizmos/js/toggle_switch.js',) \ No newline at end of file + return ('tethys_gizmos/js/toggle_switch.js',) diff --git a/tethys_gizmos/static/tethys_gizmos/js/select_input.js b/tethys_gizmos/static/tethys_gizmos/js/select_input.js index eedb41c52..93c84d900 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/select_input.js +++ b/tethys_gizmos/static/tethys_gizmos/js/select_input.js @@ -46,7 +46,7 @@ var TETHYS_SELECT_INPUT = (function() { // the DOM tree finishes loading $(function() { // Initialize any select2 elements - initSelectInput($('.select2')); + initSelectInput($('.tethys-select2')); }); return public_interface; diff --git a/tethys_gizmos/static/tethys_gizmos/js/tethys_map_view.js b/tethys_gizmos/static/tethys_gizmos/js/tethys_map_view.js index 95d6cde9f..18b5b5fb7 100644 --- a/tethys_gizmos/static/tethys_gizmos/js/tethys_map_view.js +++ b/tethys_gizmos/static/tethys_gizmos/js/tethys_map_view.js @@ -119,14 +119,79 @@ var TETHYS_MAP_VIEW = (function() { * Initialization Methods ***********************************/ + var base_map_labels = []; // Initialize the background map ol_base_map_init = function() { // Constants - var OPEN_STREET_MAP = 'OpenStreetMap', - BING = 'Bing', - MAP_QUEST = 'MapQuest'; + var SUPPORTED_BASE_MAPS = { + 'OpenStreetMap': { + source_class: ol.source.OSM, + default_source_options: {}, + label_property: null, + }, + 'Bing': { + source_class: ol.source.BingMaps, + default_source_options: null, + label_property: 'imagerySet', + }, + 'Stamen': { + source_class: ol.source.Stamen, + default_source_options: { + layer: 'terrain', + }, + label_property: 'layer', + }, + 'ESRI': { + source_class: function(options){ + //ESRI_Imagery_World_2D (MapServer) + //ESRI_StreetMap_World_2D (MapServer) + //NatGeo_World_Map (MapServer) + //NGS_Topo_US_2D (MapServer) + //Ocean_Basemap (MapServer) + //USA_Topo_Maps (MapServer) + //World_Imagery (MapServer) + //World_Physical_Map (MapServer) + //World_Shaded_Relief (MapServer) + //World_Street_Map (MapServer) + //World_Terrain_Base (MapServer) + //World_Topo_Map (MapServer) + + options.url = 'https://server.arcgisonline.com/ArcGIS/rest/services/' + + options.layer + '/MapServer/tile/{z}/{y}/{x}'; + + return new ol.source.XYZ(options); + }, + default_source_options: { + attributions: 'Tiles © ArcGIS', + layer: 'World_Street_Map' + }, + label_property: 'layer', + }, + 'CartoDB': { + source_class: function(options){ + var style, // 'light' or 'dark'. Default is 'light' + labels; // true or false. Default is true. + style = is_defined(options.style) ? options.style : 'light'; + labels = options.labels === false ? '_nolabels': '_all'; + options.url = 'http://{1-4}.basemaps.cartocdn.com/' + style + labels + '/{z}/{x}/{y}.png' + + return new ol.source.XYZ(options) + }, + default_source_options: { + style: 'light', + labels: true, + }, + label_property: 'style', + }, + 'XYZ': { + source_class: ol.source.XYZ, + default_source_options: {}, + label_property: null, + }, + } // Declarations var base_map_layer; @@ -134,11 +199,6 @@ var TETHYS_MAP_VIEW = (function() { return; } - // Default base map - base_map_layer = new ol.layer.Tile({ - source: new ol.source.OSM() - }); - if (is_defined(m_base_map_options)) { var base_map_options = Array.isArray(m_base_map_options) ? m_base_map_options : [m_base_map_options] var first_flag = true; @@ -149,92 +209,64 @@ var TETHYS_MAP_VIEW = (function() { visible = true; first_flag = false; } - if (typeof base_map_option === 'string') { - if (base_map_option === OPEN_STREET_MAP) { - // Initialize default open street map layer - base_map_layer = new ol.layer.Tile({ - source: new ol.source.OSM(), - visible: visible - }); - } else if (base_map_option === BING) { - // Initialize default bing layer - - } else if (base_map_option === MAP_QUEST) { - // Initialize default map quest layer - base_map_layer = new ol.layer.Tile({ - source: new ol.source.MapQuest({layer: 'sat'}), - visible: visible - }); - } - // Add legend attributes - base_map_layer.tethys_legend_title = 'Basemap: ' + base_map_option; - } else if (typeof base_map_option === 'object') { + var base_map_layer_name, + base_map_layer_arguments; - if (OPEN_STREET_MAP in base_map_option) { - // Initialize custom open street map layer - base_map_layer = new ol.layer.Tile({ - source: new ol.source.OSM(base_map_option[OPEN_STREET_MAP]), - visible: visible - }); + if (typeof base_map_option === 'string') { + base_map_layer_name = base_map_option; + base_map_layer_arguments = null; + } + else if (typeof base_map_option === 'object'){ + base_map_layer_name = Object.getOwnPropertyNames(base_map_option)[0]; + base_map_layer_arguments = base_map_option[base_map_layer_name]; + } - if (base_map_option[OPEN_STREET_MAP].hasOwnProperty('label')) { - label = base_map_option[OPEN_STREET_MAP].label; - } else { - label = OPEN_STREET_MAP; - } - // Add legend attributes - base_map_layer.tethys_legend_title = 'Basemap: ' + label; - } else if (BING in base_map_option) { - // Initialize custom bing layer - base_map_layer = new ol.layer.Tile({ - preload: Infinity, - source: new ol.source.BingMaps(base_map_option[BING]), - visible: visible - }); + if (Object.getOwnPropertyNames(SUPPORTED_BASE_MAPS).includes(base_map_layer_name)){ + var base_map_metadata = SUPPORTED_BASE_MAPS[base_map_layer_name]; + var LayerSource = base_map_metadata.source_class; + var source_options = base_map_layer_arguments ? base_map_layer_arguments : base_map_metadata.default_source_options; - if (base_map_option[BING].hasOwnProperty('label')) { - label = base_map_option[BING].label; - } else { - label = BING + '-' + base_map_option[BING]['imagerySet']; + if(source_options){ + base_map_layer = new ol.layer.Tile({ + source: new LayerSource(source_options), + visible: visible + }); } - // Add legend attributes - base_map_layer.tethys_legend_title = 'Basemap: ' + label; - - } else if (MAP_QUEST in base_map_option) { - // Initialize custom map quest layer - base_map_layer = new ol.layer.Tile({ - source: new ol.source.MapQuest(base_map_option[MAP_QUEST]), - visible: visible - }); - if (base_map_option[MAP_QUEST].hasOwnProperty('label')) { - label = base_map_option[MAP_QUEST].label; - } else { - label = MAP_QUEST; + label = base_map_layer_name; + if (source_options && source_options.hasOwnProperty('control_label')) { + label = source_options.control_label; + } + else if(base_map_metadata.label_property) { + label += '-' + source_options[base_map_metadata.label_property]; } + // Add legend attributes base_map_layer.tethys_legend_title = 'Basemap: ' + label; - } + base_map_labels.push(label); } // Add the base map to layers m_map.addLayer(base_map_layer); }); } + else{ + // Default base map + base_map_layer = new ol.layer.Tile({ + source: new ol.source.OSM() + }); + // Add the base map to layers + m_map.addLayer(base_map_layer); + } } // Initialize the base map switcher ol_base_map_switcher_init = function () { - // Constants - var OPEN_STREET_MAP = 'OpenStreetMap', - BING = 'Bing', - MAP_QUEST = 'MapQuest'; - - if (is_defined(m_base_map_options)) { - var base_map_options = Array.isArray(m_base_map_options) ? m_base_map_options : [m_base_map_options] - if (base_map_options.length >= 1) { + if (is_defined(base_map_labels)) { +// var base_map_options = Array.isArray(m_base_map_options) ? m_base_map_options : [m_base_map_options] + if (base_map_labels.length >= 1) { var $map_element = $('#' + m_map_target); var html = '' + '
    '; } else if (legend_class.LEGEND_TYPE === "mvlegend") { - html += ''; + html += ''; if (legend_class.type === legend_class.POINT_TYPE) { - html += ''; + html += ''; } else if (legend_class.type === legend_class.LINE_TYPE) { - html += ''; + html += ''; } else if (legend_class.type === legend_class.POLYGON_TYPE) { - html += ''; + html += ''; } else if (legend_class.type === legend_class.RASTER_TYPE) { - //TODO: ADD IMPLEMENTATION FOR RASTER + for (var j = 0; j < legend_class.ramp.length; j++) { + html += ''; + } } - html += '' + legend_class.value + ''; + html += '' + legend_class.value + ''; } } @@ -1744,7 +1758,7 @@ var TETHYS_MAP_VIEW = (function() { bbox = bbox.replace('{{maxy}}', y + tolerance); cql_filter = '&CQL_FILTER=BBOX(' + geometry_attribute + '%2C' + bbox + '%2C%27EPSG%3A3857%27)'; layer_params = source.getParams(); - layer_name = layer_params.LAYERS; + layer_name = layer_params.LAYERS.replace('_group', ''); layer_view_params = layer_params.VIEWPARAMS ? layer_params.VIEWPARAMS : ''; if (source instanceof ol.source.ImageWMS) { @@ -1833,7 +1847,7 @@ var TETHYS_MAP_VIEW = (function() { + '?SERVICE=wfs' + '&VERSION=2.0.0' + '&REQUEST=GetFeature' - + '&TYPENAMES=' + layer_name + + '&TYPENAMES=' + layer_name.replace('_group', '') + '&VIEWPARAMS=' + layer_view_params + '&OUTPUTFORMAT=text/javascript' + '&FORMAT_OPTIONS=callback:TETHYS_MAP_VIEW.jsonResponseHandler;' diff --git a/tethys_gizmos/templates/tethys_gizmos/gizmos/select_input.html b/tethys_gizmos/templates/tethys_gizmos/gizmos/select_input.html index 111825aaa..ccccbe8fe 100644 --- a/tethys_gizmos/templates/tethys_gizmos/gizmos/select_input.html +++ b/tethys_gizmos/templates/tethys_gizmos/gizmos/select_input.html @@ -3,7 +3,7 @@ {% endif %}
    {% if display_text %}{% endif %} -