Merge branch 'master' into request_form_styles
This commit is contained in:
commit
cbe20298af
156
.bandit_config
Normal file
156
.bandit_config
Normal file
@ -0,0 +1,156 @@
|
||||
### This config may optionally select a subset of tests to run or skip by
|
||||
### filling out the 'tests' and 'skips' lists given below. If no tests are
|
||||
### specified for inclusion then it is assumed all tests are desired. The skips
|
||||
### set will remove specific tests from the include set.
|
||||
### Note that the same test ID should not appear in both 'tests' and 'skips',
|
||||
### this would be nonsensical and is detected by Bandit at runtime.
|
||||
|
||||
# (optional) list included test IDs here, eg '[B101, B406]':
|
||||
tests:
|
||||
|
||||
# (optional) list skipped test IDs here, eg '[B101, B406]':
|
||||
skips:
|
||||
|
||||
### (optional) plugin settings - some test plugins require configuration data
|
||||
### that may be given here, per-plugin. All bandit test plugins have a built in
|
||||
### set of sensible defaults and these will be used if no configuration is
|
||||
### provided. It is not necessary to provide settings for every (or any) plugin
|
||||
### if the defaults are acceptable.
|
||||
|
||||
any_other_function_with_shell_equals_true:
|
||||
no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp,
|
||||
os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve,
|
||||
os.spawnvp, os.spawnvpe, os.startfile]
|
||||
shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3,
|
||||
popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput]
|
||||
subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output,
|
||||
utils.execute, utils.execute_with_timeout]
|
||||
execute_with_run_as_root_equals_true:
|
||||
function_names: [ceilometer.utils.execute, cinder.utils.execute, neutron.agent.linux.utils.execute,
|
||||
nova.utils.execute, nova.utils.trycmd]
|
||||
hardcoded_tmp_directory:
|
||||
tmp_dirs: [/tmp, /var/tmp, /dev/shm]
|
||||
linux_commands_wildcard_injection:
|
||||
no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp,
|
||||
os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve,
|
||||
os.spawnvp, os.spawnvpe, os.startfile]
|
||||
shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3,
|
||||
popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput]
|
||||
subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output,
|
||||
utils.execute, utils.execute_with_timeout]
|
||||
password_config_option_not_marked_secret:
|
||||
function_names: [oslo.config.cfg.StrOpt, oslo_config.cfg.StrOpt]
|
||||
ssl_with_bad_defaults:
|
||||
bad_protocol_versions: [PROTOCOL_SSLv2, SSLv2_METHOD, SSLv23_METHOD, PROTOCOL_SSLv3,
|
||||
PROTOCOL_TLSv1, SSLv3_METHOD, TLSv1_METHOD]
|
||||
ssl_with_bad_version:
|
||||
bad_protocol_versions: [PROTOCOL_SSLv2, SSLv2_METHOD, SSLv23_METHOD, PROTOCOL_SSLv3,
|
||||
PROTOCOL_TLSv1, SSLv3_METHOD, TLSv1_METHOD]
|
||||
start_process_with_a_shell:
|
||||
no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp,
|
||||
os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve,
|
||||
os.spawnvp, os.spawnvpe, os.startfile]
|
||||
shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3,
|
||||
popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput]
|
||||
subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output,
|
||||
utils.execute, utils.execute_with_timeout]
|
||||
start_process_with_no_shell:
|
||||
no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp,
|
||||
os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve,
|
||||
os.spawnvp, os.spawnvpe, os.startfile]
|
||||
shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3,
|
||||
popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput]
|
||||
subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output,
|
||||
utils.execute, utils.execute_with_timeout]
|
||||
start_process_with_partial_path:
|
||||
no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp,
|
||||
os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve,
|
||||
os.spawnvp, os.spawnvpe, os.startfile]
|
||||
shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3,
|
||||
popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput]
|
||||
subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output,
|
||||
utils.execute, utils.execute_with_timeout]
|
||||
subprocess_popen_with_shell_equals_true:
|
||||
no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp,
|
||||
os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve,
|
||||
os.spawnvp, os.spawnvpe, os.startfile]
|
||||
shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3,
|
||||
popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput]
|
||||
subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output,
|
||||
utils.execute, utils.execute_with_timeout]
|
||||
subprocess_without_shell_equals_true:
|
||||
no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp,
|
||||
os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve,
|
||||
os.spawnvp, os.spawnvpe, os.startfile]
|
||||
shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3,
|
||||
popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput]
|
||||
subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output,
|
||||
utils.execute, utils.execute_with_timeout]
|
||||
try_except_continue: {check_typed_exception: false}
|
||||
try_except_pass: {check_typed_exception: false}
|
||||
|
||||
### Reference of Available tests:
|
||||
# B101 : assert_used
|
||||
# B102 : exec_used
|
||||
# B103 : set_bad_file_permissions
|
||||
# B104 : hardcoded_bind_all_interfaces
|
||||
# B105 : hardcoded_password_string
|
||||
# B106 : hardcoded_password_funcarg
|
||||
# B107 : hardcoded_password_default
|
||||
# B108 : hardcoded_tmp_directory
|
||||
# B109 : password_config_option_not_marked_secret
|
||||
# B110 : try_except_pass
|
||||
# B111 : execute_with_run_as_root_equals_true
|
||||
# B112 : try_except_continue
|
||||
# B201 : flask_debug_true
|
||||
# B301 : pickle
|
||||
# B302 : marshal
|
||||
# B303 : md5
|
||||
# B304 : ciphers
|
||||
# B305 : cipher_modes
|
||||
# B306 : mktemp_q
|
||||
# B307 : eval
|
||||
# B308 : mark_safe
|
||||
# B309 : httpsconnection
|
||||
# B310 : urllib_urlopen
|
||||
# B311 : random
|
||||
# B312 : telnetlib
|
||||
# B313 : xml_bad_cElementTree
|
||||
# B314 : xml_bad_ElementTree
|
||||
# B315 : xml_bad_expatreader
|
||||
# B316 : xml_bad_expatbuilder
|
||||
# B317 : xml_bad_sax
|
||||
# B318 : xml_bad_minidom
|
||||
# B319 : xml_bad_pulldom
|
||||
# B320 : xml_bad_etree
|
||||
# B321 : ftplib
|
||||
# B322 : input
|
||||
# B401 : import_telnetlib
|
||||
# B402 : import_ftplib
|
||||
# B403 : import_pickle
|
||||
# B404 : import_subprocess
|
||||
# B405 : import_xml_etree
|
||||
# B406 : import_xml_sax
|
||||
# B407 : import_xml_expat
|
||||
# B408 : import_xml_minidom
|
||||
# B409 : import_xml_pulldom
|
||||
# B410 : import_lxml
|
||||
# B411 : import_xmlrpclib
|
||||
# B412 : import_httpoxy
|
||||
# B501 : request_with_no_cert_validation
|
||||
# B502 : ssl_with_bad_version
|
||||
# B503 : ssl_with_bad_defaults
|
||||
# B504 : ssl_with_no_version
|
||||
# B505 : weak_cryptographic_key
|
||||
# B506 : yaml_load
|
||||
# B601 : paramiko_calls
|
||||
# B602 : subprocess_popen_with_shell_equals_true
|
||||
# B603 : subprocess_without_shell_equals_true
|
||||
# B604 : any_other_function_with_shell_equals_true
|
||||
# B605 : start_process_with_a_shell
|
||||
# B606 : start_process_with_no_shell
|
||||
# B607 : start_process_with_partial_path
|
||||
# B608 : hardcoded_sql_expressions
|
||||
# B609 : linux_commands_wildcard_injection
|
||||
# B701 : jinja2_autoescape_false
|
||||
# B702 : use_of_mako_templates
|
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
[submodule "script/include"]
|
||||
path = script/include
|
||||
url = git@github.com:dod-ccpo/scriptz.git
|
||||
branch = master
|
12
.travis.yml
12
.travis.yml
@ -2,20 +2,28 @@ sudo: required
|
||||
language: python
|
||||
python: "3.6"
|
||||
services: docker
|
||||
git:
|
||||
submodules: false
|
||||
env:
|
||||
global:
|
||||
- TESTER_IMAGE_NAME=atst-tester
|
||||
- PROD_IMAGE_NAME=atst-prod
|
||||
|
||||
before_install:
|
||||
# Use sed to replace the SSH URL with the public URL
|
||||
- sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules
|
||||
# Manually initialize submodules
|
||||
- git submodule update --init --recursive
|
||||
|
||||
before_script:
|
||||
- docker login -u $ATAT_DOCKER_REGISTRY_USERNAME -p $ATAT_DOCKER_REGISTRY_PASSWORD $ATAT_DOCKER_REGISTRY_URL
|
||||
- docker build --tag "${TESTER_IMAGE_NAME}" . -f docker/tester/Dockerfile
|
||||
- docker build --tag "${TESTER_IMAGE_NAME}" . -f deploy/docker/tester/Dockerfile
|
||||
|
||||
script:
|
||||
- docker run "${TESTER_IMAGE_NAME}"
|
||||
|
||||
before_deploy:
|
||||
- docker build --tag "${PROD_IMAGE_NAME}" . -f docker/prod/Dockerfile
|
||||
- docker build --tag "${PROD_IMAGE_NAME}" . -f deploy/docker/prod/Dockerfile
|
||||
- git_sha="$(git rev-parse --short HEAD)"
|
||||
- remote_image_name="${ATAT_DOCKER_REGISTRY_URL}/${PROD_IMAGE_NAME}:${git_sha}"
|
||||
- docker tag "${PROD_IMAGE_NAME}" "${remote_image_name}"
|
||||
|
1
Pipfile
1
Pipfile
@ -19,6 +19,7 @@ ipython = "*"
|
||||
ipdb = "*"
|
||||
pylint = "*"
|
||||
black = "*"
|
||||
pytest-watch = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
47
Pipfile.lock
generated
47
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "53a2048f5dc853a0ac2d565164c23dcdb2df802e055c1530864bfc8390af3c3e"
|
||||
"sha256": "3bea02ccdb0e3877f2595d7fb405408114ec8947e0484d5b4aaf14a4c8ff78b2"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -119,6 +119,13 @@
|
||||
"markers": "sys_platform == 'darwin'",
|
||||
"version": "==0.1.0"
|
||||
},
|
||||
"argh": {
|
||||
"hashes": [
|
||||
"sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3",
|
||||
"sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65"
|
||||
],
|
||||
"version": "==0.26.2"
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:a8d8c7fe34e34e868426b9bafce852c355a3951eef60bc831b2ed541558f8d37",
|
||||
@ -170,6 +177,13 @@
|
||||
],
|
||||
"version": "==6.7"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
|
||||
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
|
||||
],
|
||||
"version": "==0.3.9"
|
||||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
"sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82",
|
||||
@ -177,6 +191,12 @@
|
||||
],
|
||||
"version": "==4.3.0"
|
||||
},
|
||||
"docopt": {
|
||||
"hashes": [
|
||||
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
|
||||
],
|
||||
"version": "==0.6.2"
|
||||
},
|
||||
"gitdb2": {
|
||||
"hashes": [
|
||||
"sha256:b60e29d4533e5e25bb50b7678bbc187c8f6bcff1344b4f293b2ba55c85795f09",
|
||||
@ -280,10 +300,16 @@
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:8105449d86d858e53ce3e0044ede9dd3a395b1c9716c696af8aa3787158ab806",
|
||||
"sha256:d250235e52e8f9fc5a80cc2a5f804c9fefd886b2e67a2b1099cf085f403f8e33"
|
||||
"sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2",
|
||||
"sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24"
|
||||
],
|
||||
"version": "==0.3.0"
|
||||
"version": "==0.3.1"
|
||||
},
|
||||
"pathtools": {
|
||||
"hashes": [
|
||||
"sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"
|
||||
],
|
||||
"version": "==0.1.2"
|
||||
},
|
||||
"pbr": {
|
||||
"hashes": [
|
||||
@ -370,6 +396,13 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"pytest-watch": {
|
||||
"hashes": [
|
||||
"sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.0"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
|
||||
@ -460,6 +493,12 @@
|
||||
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"watchdog": {
|
||||
"hashes": [
|
||||
"sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162"
|
||||
],
|
||||
"version": "==0.8.3"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||
|
55
README.md
55
README.md
@ -3,35 +3,74 @@
|
||||
|
||||
[](https://travis-ci.org/dod-ccpo/atst)
|
||||
|
||||
## Description
|
||||
|
||||
This is the main user-facing web application for the ATAT stack. All end-user
|
||||
requests are handled by ATST, with it making backend calls to various
|
||||
microservices when appropriate.
|
||||
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
See the [scriptz](https://github.com/dod-ccpo/scriptz) repository for the shared
|
||||
requirements and guidelines for all ATAT applications.
|
||||
Additionally, ATST requires a redis instance for session management. Have redis
|
||||
installed and running. By default, ATST will try to connect to a redis instance
|
||||
running on localhost on its default port, 6379.
|
||||
|
||||
### Cloning
|
||||
This project contains git submodules. Here is an example clone command that will
|
||||
automatically initialize and update those modules:
|
||||
|
||||
git clone --recurse-submodules git@github.com:dod-ccpo/atst.git
|
||||
|
||||
If you have an existing clone that does not yet contain the submodules, you can
|
||||
set them up with the following command:
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
### Setup
|
||||
This application uses Pipenv to manage Python dependencies and a virtual
|
||||
environment. Instead of the classic `requirements.txt` file, pipenv uses a
|
||||
Pipfile and Pipfile.lock, making it more similar to other modern package managers
|
||||
like yarn or mix.
|
||||
|
||||
To perform the installation, run the setup script:
|
||||
|
||||
script/setup
|
||||
|
||||
The setup script installs pipenv, which is what this application uses to manage its dependences and virtualenv. Instead of the classic `requirements.txt` file, pipenv uses a Pipfile and Pipfile.lock, making it more similar to other modern package managers like yarn or mix.
|
||||
The setup script creates the virtual environment, and then calls script/bootstrap
|
||||
to install all of the Python and Node dependencies.
|
||||
|
||||
To enter the virtualenv manually (a la `source .venv/bin/activate`):
|
||||
|
||||
pipenv shell
|
||||
|
||||
If you want to automatically load the virtual environment whenever you enter the project directory, take a look at [direnv](https://direnv.net/). An `.envrc` file is included in this repository. direnv will activate and deactivate virtualenvs for you when you enter and leave the directory.
|
||||
If you want to automatically load the virtual environment whenever you enter the
|
||||
project directory, take a look at [direnv](https://direnv.net/). An `.envrc`
|
||||
file is included in this repository. direnv will activate and deactivate
|
||||
virtualenvs for you when you enter and leave the directory.
|
||||
|
||||
Additionally, ATST requires a redis instance for session management. Have redis installed and running. By default, ATST will try to connect to a redis instance running on localhost on its default port, 6379.
|
||||
|
||||
## Running (development)
|
||||
|
||||
To start the app and watch for changes:
|
||||
To start the app locally in the foreground and watch for changes:
|
||||
|
||||
DEBUG=1 script/server
|
||||
script/dev_server
|
||||
|
||||
## Testing
|
||||
|
||||
To run unit tests:
|
||||
To run lint, static analysis, and unit tests:
|
||||
|
||||
script/test
|
||||
|
||||
or
|
||||
To run only the unit tests:
|
||||
|
||||
python -m pytest
|
||||
pipenv run python -m pytest
|
||||
|
||||
To re-run tests each time a file is changed:
|
||||
|
||||
pipenv run ptw
|
||||
|
||||
## Notes
|
||||
|
||||
|
33
atst/app.py
33
atst/app.py
@ -4,12 +4,13 @@ import tornado.web
|
||||
from tornado.web import url
|
||||
from redis import StrictRedis
|
||||
|
||||
from atst.handlers.main import MainHandler
|
||||
from atst.handlers.home import Home
|
||||
from atst.handlers.login import Login
|
||||
from atst.handlers.main import Main
|
||||
from atst.handlers.root import Root
|
||||
from atst.handlers.login_redirect import LoginRedirect
|
||||
from atst.handlers.workspace import Workspace
|
||||
from atst.handlers.request import Request
|
||||
from atst.handlers.request_new import RequestNew
|
||||
from atst.handlers.request_submit import RequestsSubmit
|
||||
from atst.handlers.dev import Dev
|
||||
from atst.home import home
|
||||
from atst.api_client import ApiClient
|
||||
@ -21,23 +22,23 @@ ENV = os.getenv("TORNADO_ENV", "dev")
|
||||
def make_app(config, deps, **kwargs):
|
||||
|
||||
routes = [
|
||||
url(r"/", Home, {"page": "login"}, name="main"),
|
||||
url(r"/", Root, {"page": "root"}, name="root"),
|
||||
url(
|
||||
r"/login",
|
||||
Login,
|
||||
r"/login-redirect",
|
||||
LoginRedirect,
|
||||
{"sessions": deps["sessions"], "authnid_client": deps["authnid_client"]},
|
||||
name="login",
|
||||
name="login_redirect",
|
||||
),
|
||||
url(r"/home", MainHandler, {"page": "home"}, name="home"),
|
||||
url(r"/home", Main, {"page": "home"}, name="home"),
|
||||
url(
|
||||
r"/styleguide",
|
||||
MainHandler,
|
||||
Main,
|
||||
{"page": "styleguide"},
|
||||
name="styleguide",
|
||||
),
|
||||
url(
|
||||
r"/workspaces/blank",
|
||||
MainHandler,
|
||||
Main,
|
||||
{"page": "workspaces_blank"},
|
||||
name="workspaces_blank",
|
||||
),
|
||||
@ -71,9 +72,15 @@ def make_app(config, deps, **kwargs):
|
||||
{"page": "requests_new", "requests_client": deps["requests_client"]},
|
||||
name="request_form_update",
|
||||
),
|
||||
url(r"/users", MainHandler, {"page": "users"}, name="users"),
|
||||
url(r"/reports", MainHandler, {"page": "reports"}, name="reports"),
|
||||
url(r"/calculator", MainHandler, {"page": "calculator"}, name="calculator"),
|
||||
url(
|
||||
r"/requests/submit/(\S+)",
|
||||
RequestsSubmit,
|
||||
{"requests_client": deps["requests_client"]},
|
||||
name="requests_submit",
|
||||
),
|
||||
url(r"/users", Main, {"page": "users"}, name="users"),
|
||||
url(r"/reports", Main, {"page": "reports"}, name="reports"),
|
||||
url(r"/calculator", Main, {"page": "calculator"}, name="calculator"),
|
||||
]
|
||||
|
||||
if not ENV == "production":
|
||||
|
@ -15,10 +15,14 @@ class FinancialForm(Form):
|
||||
"Unique Item Identifier (UII)s related to your application(s) if you already have them."
|
||||
)
|
||||
|
||||
pe_id = NewlineListField(
|
||||
"Program Element (PE) Numbers related to your request"
|
||||
pe_id = StringField(
|
||||
"Program Element (PE) Number related to your request"
|
||||
)
|
||||
|
||||
treasury_code = StringField("Please provide your Program Treasury Code")
|
||||
|
||||
ba_code = StringField("Please provide your Program BA Code")
|
||||
|
||||
fname_co = StringField("Contracting Officer First Name", validators=[Required()])
|
||||
lname_co = StringField("Contracting Officer Last Name", validators=[Required()])
|
||||
|
||||
|
@ -2,7 +2,7 @@ from wtforms.fields.html5 import IntegerField
|
||||
from wtforms.fields import RadioField, StringField, TextAreaField
|
||||
from wtforms.validators import NumberRange, InputRequired
|
||||
from wtforms_tornado import Form
|
||||
from .fields import DateField, NewlineListField
|
||||
from .fields import DateField
|
||||
from .validators import DateRange
|
||||
import pendulum
|
||||
|
||||
|
@ -23,6 +23,7 @@ class BaseHandler(tornado.web.RequestHandler):
|
||||
try:
|
||||
session = self.application.sessions.get_session(cookie)
|
||||
except SessionNotFoundError:
|
||||
self.clear_cookie("atat")
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
@ -2,7 +2,7 @@ import tornado
|
||||
from atst.handler import BaseHandler
|
||||
|
||||
|
||||
class Login(BaseHandler):
|
||||
class LoginRedirect(BaseHandler):
|
||||
def initialize(self, authnid_client, sessions):
|
||||
self.authnid_client = authnid_client
|
||||
self.sessions = sessions
|
@ -2,7 +2,7 @@ import tornado
|
||||
from atst.handler import BaseHandler
|
||||
|
||||
|
||||
class MainHandler(BaseHandler):
|
||||
class Main(BaseHandler):
|
||||
def initialize(self, page):
|
||||
self.page = page
|
||||
|
||||
|
@ -1,43 +1,15 @@
|
||||
import tornado
|
||||
from collections import defaultdict
|
||||
|
||||
from atst.handler import BaseHandler
|
||||
from atst.forms.request import RequestForm
|
||||
from atst.forms.org import OrgForm
|
||||
from atst.forms.poc import POCForm
|
||||
from atst.forms.review import ReviewForm
|
||||
from atst.forms.financial import FinancialForm
|
||||
import tornado.httputil
|
||||
|
||||
|
||||
class RequestNew(BaseHandler):
|
||||
screens = [
|
||||
{
|
||||
"title": "Details of Use",
|
||||
"section": "details_of_use",
|
||||
"form": RequestForm,
|
||||
"subitems": [
|
||||
{"title": "Overall request details", "id": "overall-request-details"},
|
||||
{"title": "Cloud Resources", "id": "cloud-resources"},
|
||||
{"title": "Support Staff", "id": "support-staff"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Information About You",
|
||||
"section": "information_about_you",
|
||||
"form": OrgForm,
|
||||
},
|
||||
{
|
||||
"title": "Primary Point of Contact",
|
||||
"section": "primary_poc",
|
||||
"form": POCForm,
|
||||
},
|
||||
{"title": "Review & Submit", "section": "review_submit", "form": ReviewForm},
|
||||
{
|
||||
"title": "Financial Verification",
|
||||
"section": "financial_verification",
|
||||
"form": FinancialForm,
|
||||
},
|
||||
]
|
||||
|
||||
def initialize(self, page, requests_client):
|
||||
self.page = page
|
||||
self.requests_client = requests_client
|
||||
@ -47,58 +19,62 @@ class RequestNew(BaseHandler):
|
||||
def post(self, screen=1, request_id=None):
|
||||
self.check_xsrf_cookie()
|
||||
screen = int(screen)
|
||||
form_metadata = self.screens[screen - 1]
|
||||
form_section = form_metadata["section"]
|
||||
form = form_metadata["form"](self.request.arguments)
|
||||
post_data = self.request.arguments
|
||||
jedi_flow = JEDIRequestFlow(
|
||||
self.requests_client, screen, post_data=post_data, request_id=request_id
|
||||
)
|
||||
|
||||
if form.validate():
|
||||
response = yield self.create_or_update_request(
|
||||
form_section, form.data, request_id
|
||||
)
|
||||
if jedi_flow.validate():
|
||||
response = yield jedi_flow.create_or_update_request(self.get_current_user())
|
||||
if response.ok:
|
||||
where = self.application.default_router.reverse_url(
|
||||
"request_form_update",
|
||||
str(screen + 1),
|
||||
request_id or response.json["id"],
|
||||
"request_form_update", str(screen + 1), jedi_flow.request_id
|
||||
)
|
||||
self.redirect(where)
|
||||
else:
|
||||
self.set_status(response.code)
|
||||
else:
|
||||
self.show_form(screen, form)
|
||||
self.render(
|
||||
"requests/screen-%d.html.to" % int(screen),
|
||||
f=jedi_flow.form,
|
||||
data=post_data,
|
||||
page=self.page,
|
||||
screens=jedi_flow.screens,
|
||||
current=screen,
|
||||
next_screen=jedi_flow.next_screen,
|
||||
request_id=jedi_flow.request_id,
|
||||
)
|
||||
|
||||
@tornado.web.authenticated
|
||||
@tornado.gen.coroutine
|
||||
def get(self, screen=1, request_id=None):
|
||||
form = None
|
||||
form_data = None
|
||||
is_review_section = screen == 4
|
||||
screen = int(screen)
|
||||
request = None
|
||||
|
||||
if request_id:
|
||||
request = yield self.get_request(request_id)
|
||||
if request.ok:
|
||||
if is_review_section:
|
||||
form_data = request.json["body"]
|
||||
else:
|
||||
form_metadata = self.screens[int(screen) - 1]
|
||||
section = form_metadata["section"]
|
||||
form_data = request.json["body"].get(section, request.json["body"])
|
||||
form = form_metadata["form"](data=form_data)
|
||||
response = yield self.requests_client.get(
|
||||
"/users/{}/requests/{}".format(
|
||||
self.get_current_user()["id"], request_id
|
||||
),
|
||||
raise_error=False,
|
||||
)
|
||||
if response.ok:
|
||||
request = response.json
|
||||
|
||||
self.show_form(screen=screen, form=form, request_id=request_id, data=form_data)
|
||||
jedi_flow = JEDIRequestFlow(
|
||||
self.requests_client, screen, request, request_id=request_id
|
||||
)
|
||||
|
||||
def show_form(self, screen=1, form=None, request_id=None, data=None):
|
||||
if not form:
|
||||
form = self.screens[int(screen) - 1]["form"](self.request.arguments)
|
||||
self.render(
|
||||
"requests/screen-%d.html.to" % int(screen),
|
||||
f=form,
|
||||
data=data,
|
||||
f=jedi_flow.form,
|
||||
data=jedi_flow.current_step_data,
|
||||
page=self.page,
|
||||
screens=self.screens,
|
||||
current=int(screen),
|
||||
next_screen=int(screen) + 1,
|
||||
screens=jedi_flow.screens,
|
||||
current=screen,
|
||||
next_screen=screen + 1,
|
||||
request_id=request_id,
|
||||
can_submit=jedi_flow.can_submit
|
||||
)
|
||||
|
||||
@tornado.gen.coroutine
|
||||
@ -109,16 +85,128 @@ class RequestNew(BaseHandler):
|
||||
)
|
||||
return request
|
||||
|
||||
|
||||
class JEDIRequestFlow(object):
|
||||
def __init__(
|
||||
self,
|
||||
requests_client,
|
||||
current_step,
|
||||
request=None,
|
||||
post_data=None,
|
||||
request_id=None,
|
||||
):
|
||||
self.requests_client = requests_client
|
||||
|
||||
self.current_step = current_step
|
||||
self.request = request
|
||||
|
||||
self.post_data = post_data
|
||||
self.is_post = self.post_data is not None
|
||||
|
||||
self.request_id = request_id
|
||||
self.form = self._form()
|
||||
|
||||
def _form(self):
|
||||
if self.is_post:
|
||||
return self.form_class()(self.post_data)
|
||||
elif self.request:
|
||||
return self.form_class()(data=self.current_step_data)
|
||||
else:
|
||||
return self.form_class()()
|
||||
|
||||
def validate(self):
|
||||
return self.form.validate()
|
||||
|
||||
@property
|
||||
def current_screen(self):
|
||||
return self.screens[self.current_step - 1]
|
||||
|
||||
@property
|
||||
def form_section(self):
|
||||
return self.current_screen["section"]
|
||||
|
||||
def form_class(self):
|
||||
return self.current_screen["form"]
|
||||
|
||||
@property
|
||||
def current_step_data(self):
|
||||
data = {}
|
||||
|
||||
if self.is_post:
|
||||
data = self.post_data
|
||||
|
||||
if self.request:
|
||||
if self.form_section == "review_submit":
|
||||
data = self.request["body"]
|
||||
else:
|
||||
data = self.request["body"].get(self.form_section, {})
|
||||
|
||||
return defaultdict(lambda: defaultdict(lambda: 'Input required'), data)
|
||||
|
||||
@property
|
||||
def can_submit(self):
|
||||
return self.request and self.request["status"] != "incomplete"
|
||||
|
||||
@property
|
||||
def next_screen(self):
|
||||
return self.current_step + 1
|
||||
|
||||
@property
|
||||
def screens(self):
|
||||
return [
|
||||
{
|
||||
"title": "Details of Use",
|
||||
"section": "details_of_use",
|
||||
"form": RequestForm,
|
||||
"subitems": [
|
||||
{
|
||||
"title": "Overall request details",
|
||||
"id": "overall-request-details",
|
||||
},
|
||||
{"title": "Cloud Resources", "id": "cloud-resources"},
|
||||
{"title": "Support Staff", "id": "support-staff"},
|
||||
],
|
||||
"show": True,
|
||||
},
|
||||
{
|
||||
"title": "Information About You",
|
||||
"section": "information_about_you",
|
||||
"form": OrgForm,
|
||||
"show": True,
|
||||
},
|
||||
{
|
||||
"title": "Primary Point of Contact",
|
||||
"section": "primary_poc",
|
||||
"form": POCForm,
|
||||
"show": True,
|
||||
},
|
||||
{
|
||||
"title": "Review & Submit",
|
||||
"section": "review_submit",
|
||||
"form": ReviewForm,
|
||||
"show":True,
|
||||
},
|
||||
{
|
||||
"title": "Financial Verification",
|
||||
"section": "financial_verification",
|
||||
"form": FinancialForm,
|
||||
"show": self.request and self.request["status"] == "approved",
|
||||
},
|
||||
]
|
||||
|
||||
@tornado.gen.coroutine
|
||||
def create_or_update_request(self, form_section, form_data, request_id=None):
|
||||
def create_or_update_request(self, user):
|
||||
request_data = {
|
||||
"creator_id": self.get_current_user()["id"],
|
||||
"request": {form_section: form_data},
|
||||
"creator_id": user["id"],
|
||||
"request": {self.form_section: self.form.data},
|
||||
}
|
||||
if request_id:
|
||||
if self.request_id:
|
||||
response = yield self.requests_client.patch(
|
||||
"/requests/{}".format(request_id), json=request_data
|
||||
"/requests/{}".format(self.request_id), json=request_data
|
||||
)
|
||||
else:
|
||||
response = yield self.requests_client.post("/requests", json=request_data)
|
||||
self.request = response.json
|
||||
self.request_id = self.request["id"]
|
||||
|
||||
return response
|
||||
|
17
atst/handlers/request_submit.py
Normal file
17
atst/handlers/request_submit.py
Normal file
@ -0,0 +1,17 @@
|
||||
import tornado
|
||||
|
||||
from atst.handler import BaseHandler
|
||||
|
||||
|
||||
class RequestsSubmit(BaseHandler):
|
||||
def initialize(self, requests_client):
|
||||
self.requests_client = requests_client
|
||||
|
||||
@tornado.web.authenticated
|
||||
@tornado.gen.coroutine
|
||||
def post(self, request_id):
|
||||
yield self.requests_client.post(
|
||||
"/requests/{}/submit".format(request_id),
|
||||
allow_nonstandard_methods=True
|
||||
)
|
||||
self.redirect("/requests")
|
@ -1,7 +1,7 @@
|
||||
from atst.handler import BaseHandler
|
||||
|
||||
|
||||
class Home(BaseHandler):
|
||||
class Root(BaseHandler):
|
||||
def initialize(self, page):
|
||||
self.page = page
|
||||
|
@ -23,12 +23,12 @@ RUN set -x ; \
|
||||
# Set working dir
|
||||
WORKDIR ${APP_DIR}
|
||||
|
||||
# Copy over alpine setup script
|
||||
COPY script/alpine_setup ./script/
|
||||
# Copy over setup scripts
|
||||
COPY script/ ./script/
|
||||
|
||||
# Add required system packages and app user
|
||||
RUN set -x ; \
|
||||
script/alpine_setup "${APP_USER}" "${APP_GROUP}"
|
||||
script/alpine_setup
|
||||
|
||||
### Items that will change almost every build
|
||||
#############################################
|
@ -3,20 +3,11 @@
|
||||
# script/alpine_setup: Adds all the system packages, directors, users, etc.
|
||||
# required to run the application on Alpine
|
||||
|
||||
# If a command fails, exit the script
|
||||
set -e
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# Ensure we are in the app root directory (not the /script directory)
|
||||
cd "$(dirname "${0}")/.."
|
||||
# Set app specific items
|
||||
APP_USER="atst"
|
||||
APP_UID="8010"
|
||||
|
||||
APP_USER=${1}
|
||||
APP_GROUP=${2}
|
||||
|
||||
apk update
|
||||
apk upgrade
|
||||
|
||||
apk add bash
|
||||
apk add dumb-init
|
||||
|
||||
addgroup -g 8000 -S "${APP_GROUP}"
|
||||
adduser -u 8010 -D -S -G "${APP_GROUP}" "${APP_USER}"
|
||||
# Run the shared alpine setup script
|
||||
source ./script/include/run_alpine_setup
|
||||
|
@ -3,33 +3,18 @@
|
||||
# script/bootstrap: Resolve all dependencies that the application requires to
|
||||
# run.
|
||||
|
||||
# If a command fails, exit the script
|
||||
set -e
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# Ensure we are in the app root directory (not the /script directory)
|
||||
cd "$(dirname "${0}")/.."
|
||||
# Set sass compiling command for this app
|
||||
COMPILE_SASS_CMD="webassets -m atst.assets build"
|
||||
|
||||
if [ -z "${CIBUILD+xxxx}" ]; then
|
||||
CMD_PREFIX='pipenv run '
|
||||
fi
|
||||
PIP_CMD="${CMD_PREFIX}pip"
|
||||
WEBASSETS_CMD="${CMD_PREFIX}webassets"
|
||||
# Enable python and node package installation
|
||||
INSTALL_PYTHON_PACKAGES="true"
|
||||
INSTALL_NODE_PACKAGES="true"
|
||||
|
||||
PIPENV_INSTALL_FLAGS='--dev'
|
||||
if [ -n "${CIBUILD}" ]; then
|
||||
PIPENV_INSTALL_FLAGS+=' --system --ignore-pipfile'
|
||||
fi
|
||||
# Run the shared bootstrap script
|
||||
source ./script/include/run_bootstrap
|
||||
|
||||
# Install Python dependencies
|
||||
${PIP_CMD} install --upgrade pip
|
||||
pipenv install ${PIPENV_INSTALL_FLAGS}
|
||||
|
||||
# Install uswds node module and dependencies
|
||||
npm install
|
||||
|
||||
# Relink uswds fonts into the /static directory
|
||||
# Link USWDS fonts into the /static directory
|
||||
rm -f ./static/fonts
|
||||
ln -s ../node_modules/uswds/src/fonts ./static/fonts
|
||||
|
||||
# Precompile assets for deployment
|
||||
${WEBASSETS_CMD} -m atst.assets build
|
||||
ln -s ../node/modules/uswds/src/fonts ./static/fonts
|
||||
|
@ -2,15 +2,7 @@
|
||||
|
||||
# script/cibuild: Run CI related checks and tests
|
||||
|
||||
# If a command fails, exit the script
|
||||
set -e
|
||||
|
||||
# Ensure we are in the app root directory (not the /script directory)
|
||||
cd "$(dirname "${0}")/.."
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# Run lint/style checks and unit tests
|
||||
script/test
|
||||
|
||||
# Run static code analysis security checks
|
||||
# (excluding the tests and node_modules subdirs)
|
||||
bandit -r . -x node_modules,tests
|
||||
source ./script/test
|
||||
|
32
script/dev_server
Executable file
32
script/dev_server
Executable file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
# script/dev_server: Launch a local dev version of the server in the background
|
||||
|
||||
#
|
||||
# WIP
|
||||
#
|
||||
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# Create a function to run after a trap is triggered
|
||||
reap() {
|
||||
kill -s SIGTERM -- "-$$"
|
||||
sleep 0.1
|
||||
exit
|
||||
}
|
||||
|
||||
# Register trapping of SIGTERM and SIGINT
|
||||
trap reap SIGTERM SIGINT
|
||||
|
||||
# Display the script PID, which will also be the process group ID for all
|
||||
# child processes
|
||||
echo "Process Group: $$"
|
||||
|
||||
# Set server launch related environment variables
|
||||
DEBUG=1
|
||||
LAUNCH_ARGS="$*"
|
||||
|
||||
|
||||
# Launch the app
|
||||
source ./script/server &
|
||||
wait
|
1
script/include
Submodule
1
script/include
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 7417942f1614d6a7ad94e94d1621dca9b422dec2
|
@ -1,26 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
reap() {
|
||||
kill -TERM $child
|
||||
sleep 0.1
|
||||
exit
|
||||
}
|
||||
# script/server: Launch the server
|
||||
|
||||
trap reap TERM INT
|
||||
|
||||
# If a command fails, exit the script
|
||||
set -e
|
||||
|
||||
# Ensure we are in the app root directory (not the /script directory)
|
||||
cd "$(dirname "${0}")/.."
|
||||
|
||||
if [ -z "${SKIP_PIPENV+xxxx}" ]; then
|
||||
CMD_PREFIX='pipenv run '
|
||||
fi
|
||||
PYTHON_CMD="${CMD_PREFIX}python"
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# Launch the app
|
||||
${PYTHON_CMD} app.py ${@} &
|
||||
child=$!
|
||||
|
||||
wait $child
|
||||
run_command "./app.py ${LAUNCH_ARGS}"
|
||||
|
24
script/setup
24
script/setup
@ -3,24 +3,10 @@
|
||||
# script/setup: Set up application for the first time after cloning, or set it
|
||||
# back to the initial first unused state.
|
||||
|
||||
# If a command fails, exit the script
|
||||
set -e
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# Ensure we are in the app root directory (not the /script directory)
|
||||
cd "$(dirname "${0}")/.."
|
||||
# Turn on sass compiler installation
|
||||
INSTALL_SASS="true"
|
||||
|
||||
# Install virtualenv
|
||||
pip install pipenv
|
||||
pipenv --python 3.6
|
||||
|
||||
if ! type sass > /dev/null; then
|
||||
if type gem > /dev/null; then
|
||||
echo 'installing a sass compiler...'
|
||||
gem install sass
|
||||
else
|
||||
echo 'Could not install a sass compiler. Please install a version of sass.'
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install application dependencies
|
||||
script/bootstrap
|
||||
# Run the shared setup script
|
||||
source ./script/include/run_setup
|
||||
|
23
script/test
23
script/test
@ -2,22 +2,13 @@
|
||||
|
||||
# script/test: Run static code checks and unit tests
|
||||
|
||||
# If a command fails, exit the script
|
||||
set -e
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# Ensure we are in the app root directory (not the /script directory)
|
||||
cd "$(dirname "${0}")/.."
|
||||
# Define all relevant python files and directories for this app
|
||||
PYTHON_FILES="./app.py ./atst ./config"
|
||||
|
||||
if [ -z "${SKIP_PIPENV+xxxx}" ]; then
|
||||
CMD_PREFIX='pipenv run '
|
||||
fi
|
||||
PYLINT_CMD="${CMD_PREFIX}pylint"
|
||||
PYTHON_CMD="${CMD_PREFIX}python"
|
||||
# Enable Python testing
|
||||
RUN_PYTHON_TESTS="true"
|
||||
|
||||
# Run lint check
|
||||
echo "Running lint..."
|
||||
${PYLINT_CMD} app.py atst/ tests/
|
||||
|
||||
# Run unit tests
|
||||
echo "Running unit tests..."
|
||||
${PYTHON_CMD} -m pytest -s $*
|
||||
# Run the shared test script
|
||||
source ./script/include/run_test
|
||||
|
@ -1,10 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# If a command fails, exit the script
|
||||
set -e
|
||||
# script/update: Update dependencies
|
||||
|
||||
# Ensure we are in the app root directory (not the /script directory)
|
||||
cd "$(dirname "${0}")/.."
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# Update dependencies
|
||||
script/bootstrap
|
||||
# Run the bootstrap script
|
||||
source ./script/bootstrap
|
||||
|
@ -47,6 +47,9 @@
|
||||
|
||||
@include media($medium-screen) {
|
||||
@include margin(($site-margins * 2) null);
|
||||
}
|
||||
|
||||
@include media($large-screen) {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
|
@ -79,6 +79,10 @@
|
||||
> ul {
|
||||
@include panel-margin;
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> li {
|
||||
&:last-child {
|
||||
> .sidenav__link {
|
||||
|
@ -1,5 +1,9 @@
|
||||
{% extends '../requests_new.html.to' %}
|
||||
|
||||
{% block form_action %}
|
||||
<form method='POST' action="{{ reverse_url('requests_submit', request_id) }}" autocomplete="off">
|
||||
{% end %}
|
||||
|
||||
{% block form %}
|
||||
|
||||
{% autoescape None %}
|
||||
@ -8,110 +12,105 @@
|
||||
{% end %}
|
||||
|
||||
<h2 id="review-submit">Review & Submit</h2>
|
||||
|
||||
<p class="usa-font-lead">Before you can submit your request, please take a moment to review the information entered in the form. You may make changes by clicking the edit link on each section. When all information looks right, go ahead and submit.</p>
|
||||
|
||||
<h3>Details of Use <a href="">Edit</a></h3>
|
||||
<h3>Details of Use <a href="{{ reverse_url('request_form_update', 1, request_id) }}">Edit</a></h3>
|
||||
|
||||
<h4>Overall Request Details</h4>
|
||||
|
||||
<label>What is the total estimated dollar value of the cloud resources you are requesting using the JEDI CSP Calculator? </label>
|
||||
<b>{{ data.get('details_of_use', {}).get('dollar_value') }}</b>
|
||||
<b>{{ data['details_of_use']['dollar_value'] }}</b>
|
||||
|
||||
<label>Please estimate the number of applications that might be supported by this request</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('num_applications') }}</b>
|
||||
<b>{{ data['details_of_use']['num_applications'] }}</b>
|
||||
|
||||
<label>Start Date</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('date_start') }}</b>
|
||||
<b>{{ data['details_of_use']['date_start'] }}</b>
|
||||
|
||||
<label>Please briefly describe how your team is expecting to use the JEDI Cloud</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('app_description') }}</b>
|
||||
<b>{{ data['details_of_use']['app_description'] }}</b>
|
||||
|
||||
<label>What organizations are supported by these applications?</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('supported_organizations') }}</b>
|
||||
|
||||
<label>Please enter the Unique Item Identifier (UII)s related to your application(s) if you already have them.</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('uii_ids') }}</b>
|
||||
|
||||
<label>Please provide the Program Element (PE) Numbers related to your request</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('pe_id') }}</b>
|
||||
<b>{{ data['details_of_use']['supported_organizations'] }}</b>
|
||||
|
||||
<h4>Cloud Resources</h4>
|
||||
|
||||
<label>Total Number of vCPU cores</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('total_cores') }}</b>
|
||||
<b>{{ data['details_of_use']['total_cores'] }}</b>
|
||||
|
||||
<label>Total RAM</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('total_ram') }}</b>
|
||||
<b>{{ data['details_of_use']['total_ram'] }}</b>
|
||||
|
||||
<label>Total object storage</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('total_object_storage') }}</b>
|
||||
<b>{{ data['details_of_use']['total_object_storage'] }}</b>
|
||||
|
||||
<label>Total server storage</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('total_server_storage') }}</b>
|
||||
<b>{{ data['details_of_use']['total_server_storage'] }}</b>
|
||||
|
||||
<h4>Support Staff</h4>
|
||||
|
||||
<label>Do you have a contractor to advise and assist you with using cloud services?</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('has_contractor_advisor') }}</b>
|
||||
<b>{{ data['details_of_use']['has_contractor_advisor'] }}</b>
|
||||
|
||||
<label>Are you using the JEDI Cloud to migrate existing applications?</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('is_migrating_application') }}</b>
|
||||
<b>{{ data['details_of_use']['is_migrating_application'] }}</b>
|
||||
|
||||
<label>Please describe the organizations that are supporting you, include both government and contractor resources</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('supporting_organization') }}</b>
|
||||
<b>{{ data['details_of_use']['supporting_organization'] }}</b>
|
||||
|
||||
<label>Do you have a migration office that you're working with to migrate to the cloud?</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('has_migration_office') }}</b>
|
||||
<b>{{ data['details_of_use']['has_migration_office'] }}</b>
|
||||
|
||||
<label>Please describe the organizations that are supporting you, include both government and contractor resources.</label>
|
||||
<b>{{ data.get('details_of_use', {}).get('supporting_organization') }}</b>
|
||||
<b>{{ data['details_of_use']['supporting_organization'] }}</b>
|
||||
|
||||
<br><br><hr>
|
||||
|
||||
<h3>Information About You <a href="">Edit</a></h3>
|
||||
<h3>Information About You <a href="{{ reverse_url('request_form_update', 2, request_id) }}">Edit</a></h3>
|
||||
|
||||
<label>First Name</label>
|
||||
<b>{{ data.get('information_about_you', {}).get('fname_request') }}</b>
|
||||
<b>{{ data['information_about_you']['fname_request'] }}</b>
|
||||
|
||||
<label>Last Name</label>
|
||||
<b>{{ data.get('information_about_you', {}).get('lname_request') }}</b>
|
||||
<b>{{ data['information_about_you']['lname_request'] }}</b>
|
||||
|
||||
<label>Email (associated with your CAC)</label>
|
||||
<b>{{ data.get('information_about_you', {}).get('email_request') }}</b>
|
||||
<b>{{ data['information_about_you']['email_request'] }}</b>
|
||||
|
||||
<label>Phone Number</label>
|
||||
<b>{{ data.get('information_about_you', {}).get('phone_number') }}</b>
|
||||
<b>{{ data['information_about_you']['phone_number'] }}</b>
|
||||
|
||||
<label>Service Branch or Agency</label>
|
||||
<b>{{ data.get('information_about_you', {}).get('service_branch') }}</b>
|
||||
<b>{{ data['information_about_you']['service_branch'] }}</b>
|
||||
|
||||
<label>Citizenship</label>
|
||||
<b>{{ data.get('information_about_you', {}).get('citizenship') }}</b>
|
||||
<b>{{ data['information_about_you']['citizenship'] }}</b>
|
||||
|
||||
<label>Designation of Person</label>
|
||||
<b>{{ data.get('information_about_you', {}).get('designation') }}</b>
|
||||
<b>{{ data['information_about_you']['designation'] }}</b>
|
||||
|
||||
<label>Latest Information Assurance (IA) Training completion date</label>
|
||||
<b>{{ data.get('information_about_you', {}).get('date_latest_training') }}</b>
|
||||
<b>{{ data['information_about_you']['date_latest_training'] }}</b>
|
||||
|
||||
|
||||
<br><br><hr>
|
||||
|
||||
<h3>Primary Government/Military Point of Contact (POC) <a href="">Edit</a></h3>
|
||||
<h3>Primary Government/Military Point of Contact (POC) <a href="{{ reverse_url('request_form_update', 3, request_id) }}">Edit</a></h3>
|
||||
|
||||
|
||||
|
||||
<label>POC First Name</label>
|
||||
<b>{{ data.get('primary_poc', {}).get('fname_poc')}}</b>
|
||||
<b>{{ data['primary_poc']['fname_poc']}}</b>
|
||||
|
||||
<label>POC Last Name</label>
|
||||
<b>{{ data.get('primary_poc', {}).get('lname_poc')}}</b>
|
||||
<b>{{ data['primary_poc']['lname_poc']}}</b>
|
||||
|
||||
<label>POC Email (associated with CAC)</label>
|
||||
<b>{{ data.get('primary_poc', {}).get('email_poc')}} </b>
|
||||
<b>{{ data['primary_poc']['email_poc']}} </b>
|
||||
|
||||
<label>DOD ID</label>
|
||||
<b>{{ data.get('primary_poc', {}).get('dodid_poc')}}</b>
|
||||
<b>{{ data['primary_poc']['dodid_poc']}}</b>
|
||||
|
||||
|
||||
<br><br>
|
||||
@ -119,5 +118,8 @@
|
||||
{% end %}
|
||||
|
||||
{% block next %}
|
||||
<input type='submit' class='usa-button usa-button-primary' value='Submit' />
|
||||
{% if not can_submit %}
|
||||
<b class="usa-input-error-message">Please complete all required fields before submitting.</b>
|
||||
{% end %}
|
||||
<input type='submit' class='usa-button usa-button-primary' value='Submit' {{ "disabled" if not can_submit else "" }} />
|
||||
{% end %}
|
||||
|
@ -35,6 +35,21 @@
|
||||
</div>
|
||||
{% end %}
|
||||
|
||||
{{ f.treasury_code.label }}
|
||||
{{ f.treasury_code(placeholder="Example: 1200") }}
|
||||
{% for e in f.treasury_code.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% end %}
|
||||
|
||||
{{ f.ba_code.label }}
|
||||
{{ f.ba_code(placeholder="Example: 02") }}
|
||||
{% for e in f.ba_code.errors %}
|
||||
<div class="usa-input-error-message">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% end %}
|
||||
|
||||
<!-- KO Information -->
|
||||
|
||||
|
@ -7,25 +7,27 @@
|
||||
|
||||
<ul>
|
||||
{% for i,s in enumerate(screens) %}
|
||||
<li>
|
||||
{% if i+1==current %}
|
||||
<a class="sidenav__link sidenav__link--active" href="{{ reverse_url('request_form_update', i+1, request_id) if request_id else reverse_url('request_form_new',i+1) }}">
|
||||
{{ i+1 }}. {{ s['title'] }}
|
||||
</a>
|
||||
{% if s.get('subitems') %}
|
||||
<ul>
|
||||
{% for j,t in enumerate(s['subitems']) %}
|
||||
<li><a class="sidenav__link" href="#{{ t['id'] }}">{{ t['title'] }}</a></li>
|
||||
{% end %}
|
||||
</ul>
|
||||
{% end %}
|
||||
{% if s["show"] %}
|
||||
<li>
|
||||
{% if i+1==current %}
|
||||
<a class="sidenav__link sidenav__link--active" href="{{ reverse_url('request_form_update', i+1, request_id) if request_id else reverse_url('request_form_new',i+1) }}">
|
||||
{{ i+1 }}. {{ s['title'] }}
|
||||
</a>
|
||||
{% if s.get('subitems') %}
|
||||
<ul>
|
||||
{% for j,t in enumerate(s['subitems']) %}
|
||||
<li><a class="sidenav__link" href="#{{ t['id'] }}">{{ t['title'] }}</a></li>
|
||||
{% end %}
|
||||
</ul>
|
||||
{% end %}
|
||||
|
||||
{% else %}
|
||||
<a class="sidenav__link" href="{{ reverse_url('request_form_update', i+1, request_id) if request_id else reverse_url('request_form_new',i+1) }}">
|
||||
{{ i+1 }}. {{ s['title'] }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="sidenav__link" href="{{ reverse_url('request_form_update', i+1, request_id) if request_id else reverse_url('request_form_new',i+1) }}">
|
||||
{{ i+1 }}. {{ s['title'] }}
|
||||
</a>
|
||||
{% end %}
|
||||
</li>
|
||||
{% end %}
|
||||
</li>
|
||||
{% end %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,18 +9,20 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="col col--grow">
|
||||
|
||||
|
||||
<div class="panel">
|
||||
|
||||
<main class="panel__content">
|
||||
<div class="panel__heading">
|
||||
<h1>New Request</h1>
|
||||
</div>
|
||||
|
||||
{% if request_id %}
|
||||
<form method='POST' action="{{ reverse_url('request_form_update', current, request_id) }}" autocomplete="off">
|
||||
{% else %}
|
||||
<form method='POST' action="{{ reverse_url('request_form_new', current) }}" autocomplete="off">
|
||||
|
||||
{% block form_action %}
|
||||
{% if request_id %}
|
||||
<form method='POST' action="{{ reverse_url('request_form_update', current, request_id) }}" autocomplete="off">
|
||||
{% else %}
|
||||
<form method='POST' action="{{ reverse_url('request_form_new', current) }}" autocomplete="off">
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
{% module xsrf_form_html() %}
|
||||
@ -31,16 +33,11 @@
|
||||
<input type='submit' class='usa-button usa-button-primary' value='Save & Continue' />
|
||||
{% end %}
|
||||
</form>
|
||||
|
||||
</main>
|
||||
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% end %}
|
||||
|
||||
|
@ -19,3 +19,23 @@ def app():
|
||||
deps.update(TEST_DEPS)
|
||||
|
||||
return make_app(config, deps)
|
||||
|
||||
class DummyForm(dict):
|
||||
pass
|
||||
|
||||
|
||||
class DummyField(object):
|
||||
def __init__(self, data=None, errors=(), raw_data=None):
|
||||
self.data = data
|
||||
self.errors = list(errors)
|
||||
self.raw_data = raw_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dummy_form():
|
||||
return DummyForm()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dummy_field():
|
||||
return DummyField()
|
||||
|
61
tests/forms/test_validators.py
Normal file
61
tests/forms/test_validators.py
Normal file
@ -0,0 +1,61 @@
|
||||
from wtforms.validators import ValidationError
|
||||
import pytest
|
||||
|
||||
from atst.forms.validators import Alphabet, IsNumber, PhoneNumber
|
||||
|
||||
|
||||
class TestIsNumber:
|
||||
|
||||
@pytest.mark.parametrize("valid", ["0", "12", "-12"])
|
||||
def test_IsNumber_accepts_integers(self, valid, dummy_form, dummy_field):
|
||||
validator = IsNumber()
|
||||
dummy_field.data = valid
|
||||
validator(dummy_form, dummy_field)
|
||||
|
||||
@pytest.mark.parametrize("invalid", ["12.1", "two", ""])
|
||||
def test_IsNumber_rejects_anything_else(self, invalid, dummy_form, dummy_field):
|
||||
validator = IsNumber()
|
||||
dummy_field.data = invalid
|
||||
with pytest.raises(ValidationError):
|
||||
validator(dummy_form, dummy_field)
|
||||
|
||||
|
||||
class TestPhoneNumber:
|
||||
|
||||
@pytest.mark.parametrize("valid", [
|
||||
"12345",
|
||||
"1234567890",
|
||||
"(123) 456-7890",
|
||||
])
|
||||
def test_PhoneNumber_accepts_valid_numbers(self, valid, dummy_form, dummy_field):
|
||||
validator = PhoneNumber()
|
||||
dummy_field.data = valid
|
||||
validator(dummy_form, dummy_field)
|
||||
|
||||
@pytest.mark.parametrize("invalid", [
|
||||
"1234",
|
||||
"123456",
|
||||
"1234567abc",
|
||||
"(123) 456-789012",
|
||||
])
|
||||
def test_PhoneNumber_rejects_invalid_numbers(self, invalid, dummy_form, dummy_field):
|
||||
validator = PhoneNumber()
|
||||
dummy_field.data = invalid
|
||||
with pytest.raises(ValidationError):
|
||||
validator(dummy_form, dummy_field)
|
||||
|
||||
|
||||
class TestAlphabet:
|
||||
|
||||
@pytest.mark.parametrize("valid", ["a", "abcde"])
|
||||
def test_Alphabet_accepts_letters(self, valid, dummy_form, dummy_field):
|
||||
validator = Alphabet()
|
||||
dummy_field.data = valid
|
||||
validator(dummy_form, dummy_field)
|
||||
|
||||
@pytest.mark.parametrize("invalid", ["", "hi mark", "cloud9"])
|
||||
def test_Alphabet_rejects_non_letters(self, invalid, dummy_form, dummy_field):
|
||||
validator = Alphabet()
|
||||
dummy_field.data = invalid
|
||||
with pytest.raises(ValidationError):
|
||||
validator(dummy_form, dummy_field)
|
@ -49,6 +49,7 @@ class MockRequestsClient(MockApiClient):
|
||||
"id": "66b8ef71-86d3-48ef-abc2-51bfa1732b6b",
|
||||
"creator": "49903ae7-da4a-49bf-a6dc-9dff5d004238",
|
||||
"body": {},
|
||||
"status": "incomplete"
|
||||
}
|
||||
return self._get_response("GET", path, 200, json=json)
|
||||
|
||||
|
@ -21,11 +21,14 @@ def test_redirects_when_not_logged_in(http_client, base_url):
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_redirects_when_session_does_not_exist(monkeypatch, http_client, base_url):
|
||||
monkeypatch.setattr("atst.handlers.main.MainHandler.get_secure_cookie", lambda s,c: 'stale cookie!')
|
||||
monkeypatch.setattr("atst.handlers.main.Main.get_secure_cookie", lambda s,c: 'stale cookie!')
|
||||
response = yield http_client.fetch(
|
||||
base_url + "/home", raise_error=False, follow_redirects=False
|
||||
)
|
||||
location = response.headers["Location"]
|
||||
cookie = response.headers._dict.get('Set-Cookie')
|
||||
# should clear session cookie
|
||||
assert 'atat=""' in cookie
|
||||
assert response.code == 302
|
||||
assert response.error
|
||||
assert re.match("/\??", location)
|
||||
@ -33,9 +36,9 @@ def test_redirects_when_session_does_not_exist(monkeypatch, http_client, base_ur
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_login_with_valid_bearer_token(app, monkeypatch, http_client, base_url):
|
||||
monkeypatch.setattr("atst.handlers.login.Login._fetch_user_info", _fetch_user_info)
|
||||
monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info)
|
||||
response = yield http_client.fetch(
|
||||
base_url + "/login?bearer-token=abc-123",
|
||||
base_url + "/login-redirect?bearer-token=abc-123",
|
||||
follow_redirects=False,
|
||||
raise_error=False,
|
||||
)
|
||||
@ -65,10 +68,10 @@ def test_login_with_invalid_bearer_token(http_client, base_url):
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_valid_login_creates_session(app, monkeypatch, http_client, base_url):
|
||||
monkeypatch.setattr("atst.handlers.login.Login._fetch_user_info", _fetch_user_info)
|
||||
monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info)
|
||||
assert len(app.sessions.sessions) == 0
|
||||
yield http_client.fetch(
|
||||
base_url + "/login?bearer-token=abc-123",
|
||||
base_url + "/login-redirect?bearer-token=abc-123",
|
||||
follow_redirects=False,
|
||||
raise_error=False,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user