Merge branch 'master' into revert-user-deletion

This commit is contained in:
richard-dds 2019-10-15 17:06:06 -04:00 committed by GitHub
commit 96c1fcbe85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 372 additions and 432 deletions

View File

@ -135,6 +135,67 @@ jobs:
- run: "docker tag ${AZURE_SERVER_NAME}/atat:atat-${CIRCLE_SHA1} ${AZURE_SERVER_NAME}/atat:latest"
- run: "docker push ${AZURE_SERVER_NAME}/atat:latest"
integration-tests:
docker:
- image: docker:17.05.0-ce-git
steps:
- setup_remote_docker:
version: 18.06.0-ce
- checkout
- run:
name: Set up temporary docker network
command: docker network create atat
- run:
name: Build image
command: docker build . -t atat:latest
- run:
name: Get storage containers
command: docker pull postgres:latest && docker pull redis:latest
- run:
name: Start redis
command: docker run -d --network atat --link redis:redis -p 6379:6379 --name redis redis:latest
- run:
name: Start postgres
command: docker run -d --network atat --link postgres:postgres -p 5432:5432 --name postgres postgres:latest
- run:
name: Start application container
command: |
docker run -d \
-e DISABLE_CRL_CHECK=true \
-e PGHOST=postgres \
-e REDIS_URI=redis://redis:6379 \
-p 8000:8000 \
--network atat \
--name test-atat \
atat:latest \
uwsgi \
--callable app \
--module app \
--plugin python3 \
--virtualenv /opt/atat/atst/.venv \
--http-socket :8000
- run:
name: Wait for containers
command: sleep 3
- run:
name: Create database
command: docker exec postgres createdb -U postgres atat
- run:
name: Apply migrations
command: docker exec test-atat .venv/bin/python .venv/bin/alembic upgrade head
- run:
name: Execute Ghost Inspector test suite
command: |
docker pull ghostinspector/test-runner-standalone:latest
docker run \
-e NGROK_TOKEN=$NGROK_TOKEN \
-e GI_API_KEY=$GI_API_KEY \
-e GI_SUITE=$GI_SUITE \
-e GI_PARAMS_JSON='{}' \
-e APP_PORT="test-atat:8000" \
--network atat \
ghostinspector/test-runner-standalone:latest
workflows:
version: 2
run-tests:
@ -143,9 +204,12 @@ workflows:
- test:
requires:
- app_setup
- azure-build-and-push-image:
- integration-tests:
requires:
- test
- azure-build-and-push-image:
requires:
- integration-tests
filters:
branches:
only:
@ -204,7 +268,7 @@ workflows:
repo: atat
tag: "atat-${CIRCLE_SHA1},latest"
requires:
- test
- integration-tests
filters:
branches:
only:

6
.gitignore vendored
View File

@ -52,10 +52,12 @@ ssl/client-certs/*.srl
# je coverage output
coverage
# selenium testing
# BrowserStack
browserstacklocal
# decompiled CircleCI yaml
local-ci.yml
# python config
.python-version

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$",
"lines": null
},
"generated_at": "2019-10-03T14:34:50Z",
"generated_at": "2019-10-14T19:14:26Z",
"plugins_used": [
{
"base64_limit": 4.5,
@ -40,6 +40,13 @@
"is_verified": false,
"line_number": 156,
"type": "Secret Keyword"
},
{
"hashed_secret": "81b127e2222d9bfc4609053faec85300f7525463",
"is_secret": false,
"is_verified": false,
"line_number": 244,
"type": "Secret Keyword"
}
],
"alembic.ini": [
@ -153,24 +160,6 @@
"type": "Private Key"
}
],
"tests/acceptance/conftest.py": [
{
"hashed_secret": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f",
"is_secret": false,
"is_verified": false,
"line_number": 48,
"type": "Basic Auth Credentials"
}
],
"tests/fixtures/chain/make-chain.sh": [
{
"hashed_secret": "bad2e396920ce37fe53fc291f90b130d915375fb",
"is_secret": false,
"is_verified": false,
"line_number": 35,
"type": "Secret Keyword"
}
],
"tests/forms/test_validators.py": [
{
"hashed_secret": "260408f687da9094705a841acda9b029563053ee",
@ -194,7 +183,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false,
"is_verified": false,
"line_number": 651,
"line_number": 656,
"type": "Hex High Entropy String"
}
]

View File

@ -220,30 +220,37 @@ To generate coverage reports for the Javascript tests:
yarn test:coverage
### Selenium Tests
### Ghost Inspector Tests
Selenium tests rely on BrowserStack. In order to run the Selenium tests
locally, you need BrowserStack credentials. The user email and key can
be found on the account settings page. To run the selenium tests:
AT-AT uses [Ghost Inpsector](https://app.ghostinspector.com/) for
integration testing. These tests do not run locally as part of the
regular test suite but do run in CI. To run them locally, you will
need the following:
- [docker](https://docs.docker.com/v17.12/install/)
- [circleci CLI tool](https://circleci.com/docs/2.0/local-cli/#installation)
- the prerequisite variable information listed [here](https://ghostinspector.com/docs/integration/circle-ci/)
The version of our CircleCI config (2.1) is incompatible with the
`circleci` tool. First run:
```
BROWSERSTACK_TOKEN=<token> BROWSERSTACK_EMAIL=<email> ./script/selenium_test
circleci config process .circleci/config.yml > local-ci.yml
```
The selenium tests are in `tests/acceptance`. This directory is ignored by
pytest for normal test runs.
The `selenium_test` script manages the setup of a separate database and
launching the BrowserStackLocal client. If you already have the client running
locally, you can run the selenium tests with:
Then run the job:
```
BROWSERSTACK_TOKEN=<token> BROWSERSTACK_EMAIL=<email> pipenv run pytest tests/acceptance
circleci local execute -e GI_SUITE=<SUITE_ID> -e GI_API_KEY=<API KEY> -e NGROK_TOKEN=<NGROK TOKEN> --job integration-tests -c local-ci.yml
```
The BrowserStack email is the one associated with the account. The token is
available in the BrowserStack profile information page. Go to the dashboard,
then "Account" > "Settings", then the token is under "Local Testing".
If the job fails and you want to re-run it, you may receive errors
about running docker containers or the network already existing.
Some version of the following should reset your local docker state:
```
docker container stop redis postgres test-atat; docker container rm redis postgres test-atat ; docker network rm atat
```
## Notes

View File

@ -142,3 +142,9 @@ class PortfolioInvitations(BaseInvitations):
class ApplicationInvitations(BaseInvitations):
model = ApplicationInvitation
role_domain_class = ApplicationRoles
@classmethod
def revoke(cls, token):
invite = super().revoke(token)
ApplicationRoles.disable(invite.role)
return invite

View File

@ -183,11 +183,7 @@ def handle_create_member(application_id, form_data):
token=invite.token,
)
flash(
"new_application_member",
user_name=invite.user_name,
application_name=application.name,
)
flash("new_application_member", user_name=invite.first_name)
except AlreadyExistsError:
return render_template(

View File

@ -8,9 +8,9 @@ MESSAGES = {
"category": "success",
},
"application_created": {
"title_template": translate("flash.success"),
"title_template": translate("flash.application.created.title"),
"message_template": """
{{ "flash.application.created" | translate({"application_name": application_name}) }}
{{ "flash.application.created.message" | translate({"application_name": application_name}) }}
""",
"category": "success",
},
@ -40,7 +40,7 @@ MESSAGES = {
"category": "error",
},
"application_invite_resent": {
"title_template": "Application invitation revoked",
"title_template": "Application invitation resent",
"message_template": "You have successfully resent the invite for {{ user_name }} from {{ application_name }}",
"category": "success",
},
@ -104,9 +104,9 @@ MESSAGES = {
"category": "warning",
},
"new_application_member": {
"title_template": translate("flash.success"),
"title_template": """{{ "flash.new_application_member.title" | translate({ "user_name": user_name }) }}""",
"message_template": """
<p>{{ "flash.new_application_member" | translate({ "user_name": user_name, "application_name": application_name }) }}</p>
<p>{{ "flash.new_application_member.message" | translate({ "user_name": user_name }) }}</p>
""",
"category": "success",
},

View File

@ -10,6 +10,10 @@ export default {
components: {
textinput,
},
created: function() {
this.$root.$on('field-change', this.handleFieldChange)
if (this.initialData) this.changed = true
},
data: function() {
return {

View File

@ -1,13 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
<title>Combined Shape</title>
<desc>Created with Sketch.</desc>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Portfolio-Nav-/-Applications" transform="translate(-26.000000, 0.000000)" fill="#0071BC">
<g id="Group-4">
<path d="M41,30 C32.7157288,30 26,23.2842712 26,15 C26,6.71572875 32.7157288,0 41,0 C49.2842712,0 56,6.71572875 56,15 C56,23.2842712 49.2842712,30 41,30 Z M38.7142901,17.9107118 C38.7142901,17.5262247 38.4023478,17.2142824 38.0178607,17.2142824 L35.6964294,17.2142824 C35.3119423,17.2142824 35,17.5262247 35,17.9107118 L35,19.3035706 C35,19.6880577 35.3119423,20 35.6964294,20 L38.0178607,20 C38.4023478,20 38.7142901,19.6880577 38.7142901,19.3035706 L38.7142901,17.9107118 Z M38.7142901,14.1964217 C38.7142901,13.8119346 38.4023478,13.4999923 38.0178607,13.4999923 L35.6964294,13.4999923 C35.3119423,13.4999923 35,13.8119346 35,14.1964217 L35,15.5892805 C35,15.9737675 35.3119423,16.2857099 35.6964294,16.2857099 L38.0178607,16.2857099 C38.4023478,16.2857099 38.7142901,15.9737675 38.7142901,15.5892805 L38.7142901,14.1964217 Z M48.0000155,17.9107118 C48.0000155,17.5262247 47.6880732,17.2142824 47.3035861,17.2142824 L40.3392921,17.2142824 C39.954805,17.2142824 39.6428627,17.5262247 39.6428627,17.9107118 L39.6428627,19.3035706 C39.6428627,19.6880577 39.954805,20 40.3392921,20 L47.3035861,20 C47.6880732,20 48.0000155,19.6880577 48.0000155,19.3035706 L48.0000155,17.9107118 Z M38.7142901,10.4821315 C38.7142901,10.0976444 38.4023478,9.78570211 38.0178607,9.78570211 L35.6964294,9.78570211 C35.3119423,9.78570211 35,10.0976444 35,10.4821315 L35,11.8749903 C35,12.2594774 35.3119423,12.5714197 35.6964294,12.5714197 L38.0178607,12.5714197 C38.4023478,12.5714197 38.7142901,12.2594774 38.7142901,11.8749903 L38.7142901,10.4821315 Z M48.0000155,14.1964217 C48.0000155,13.8119346 47.6880732,13.4999923 47.3035861,13.4999923 L40.3392921,13.4999923 C39.954805,13.4999923 39.6428627,13.8119346 39.6428627,14.1964217 L39.6428627,15.5892805 C39.6428627,15.9737675 39.954805,16.2857099 40.3392921,16.2857099 L47.3035861,16.2857099 C47.6880732,16.2857099 48.0000155,15.9737675 48.0000155,15.5892805 L48.0000155,14.1964217 Z M48.0000155,10.4821315 C48.0000155,10.0976444 47.6880732,9.78570211 47.3035861,9.78570211 L40.3392921,9.78570211 C39.954805,9.78570211 39.6428627,10.0976444 39.6428627,10.4821315 L39.6428627,11.8749903 C39.6428627,12.2594774 39.954805,12.5714197 40.3392921,12.5714197 L47.3035861,12.5714197 C47.6880732,12.5714197 48.0000155,12.2594774 48.0000155,11.8749903 L48.0000155,10.4821315 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
</svg>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="list-alt" class="svg-inline--fa fa-list-alt fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M464 480H48c-26.51 0-48-21.49-48-48V80c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v352c0 26.51-21.49 48-48 48zM128 120c-22.091 0-40 17.909-40 40s17.909 40 40 40 40-17.909 40-40-17.909-40-40-40zm0 96c-22.091 0-40 17.909-40 40s17.909 40 40 40 40-17.909 40-40-17.909-40-40-40zm0 96c-22.091 0-40 17.909-40 40s17.909 40 40 40 40-17.909 40-40-17.909-40-40-40zm288-136v-32c0-6.627-5.373-12-12-12H204c-6.627 0-12 5.373-12 12v32c0 6.627 5.373 12 12 12h200c6.627 0 12-5.373 12-12zm0 96v-32c0-6.627-5.373-12-12-12H204c-6.627 0-12 5.373-12 12v32c0 6.627 5.373 12 12 12h200c6.627 0 12-5.373 12-12zm0 96v-32c0-6.627-5.373-12-12-12H204c-6.627 0-12 5.373-12 12v32c0 6.627 5.373 12 12 12h200c6.627 0 12-5.373 12-12z"></path></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 940 B

View File

@ -1,13 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
<title>Shape</title>
<desc>Created with Sketch.</desc>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Portfolio-Nav-/-Funding" transform="translate(-12.000000, 0.000000)" fill="#0071BC" fill-rule="nonzero">
<g id="Group">
<path d="M27,0 C18.7333333,0 12,6.73333333 12,15 C12,23.2666667 18.7333333,30 27,30 C35.2666667,30 42,23.2666667 42,15 C42,6.73333333 35.2666667,0 27,0 Z M29.5138284,20.9349809 C28.9570383,21.2107813 28.2731789,21.325742 27.589873,21.4089879 L27.5500151,23.6924464 C27.5406042,24.2315964 27.0789776,24.6359554 26.559629,24.6268901 C26.0078211,24.6172583 25.5936005,24.1658869 25.6024578,23.6584517 L25.6428693,21.3432785 C24.6718585,21.1677076 23.7360749,20.8341298 22.9664629,20.281382 C22.5175686,19.9563029 22.4307088,19.3520238 22.7624983,18.9453985 C23.0948413,18.5070584 23.7132286,18.4226793 24.1296635,18.7471919 C25.3157105,19.6244522 27.3903371,19.8192868 28.6343082,19.2382375 C29.354502,18.9018405 29.6890593,18.3366417 29.7028989,17.5437741 C29.7167384,16.7509066 28.910792,16.4195951 26.6463919,15.9359287 C24.5437347,15.48681 21.6967281,14.8978011 21.7459969,12.0751927 C21.7731225,10.5211723 22.5737351,9.29789683 24.0135691,8.65681758 C24.5698056,8.41273181 25.1893,8.2649233 25.839593,8.21282548 L25.8683793,6.563661 C25.8777902,6.02451107 26.3394168,5.62015206 26.8587654,5.62921732 C27.4105733,5.63884917 27.8247939,6.09022051 27.8159366,6.59765574 L27.7860432,8.31024962 C28.8544318,8.48752024 29.8534732,8.9173753 30.6544374,9.53411911 C31.0708723,9.85863165 31.1252729,10.4623441 30.7929298,10.9006842 C30.4611404,11.3073095 29.8427531,11.3916886 29.3944125,11.0348948 C28.3074041,10.06419 26.1695197,9.77307822 24.86063,10.3529944 C24.107977,10.6888248 23.7409603,11.253457 23.7265672,12.0780393 C23.7071918,13.1880538 24.7722589,13.5556127 27.0691183,14.0398456 C29.1393163,14.4883978 31.6952964,15.008878 31.6510098,17.5460542 C31.6568972,19.0689264 30.8892974,20.2610537 29.5138284,20.9349809 Z" id="Shape"></path>
</g>
</g>
</g>
</svg>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="file-invoice-dollar" class="svg-inline--fa fa-file-invoice-dollar fa-w-12" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M377 105L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128v-6.1c0-6.3-2.5-12.4-7-16.9zm-153 31V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V160H248c-13.2 0-24-10.8-24-24zM64 72c0-4.42 3.58-8 8-8h80c4.42 0 8 3.58 8 8v16c0 4.42-3.58 8-8 8H72c-4.42 0-8-3.58-8-8V72zm0 80v-16c0-4.42 3.58-8 8-8h80c4.42 0 8 3.58 8 8v16c0 4.42-3.58 8-8 8H72c-4.42 0-8-3.58-8-8zm144 263.88V440c0 4.42-3.58 8-8 8h-16c-4.42 0-8-3.58-8-8v-24.29c-11.29-.58-22.27-4.52-31.37-11.35-3.9-2.93-4.1-8.77-.57-12.14l11.75-11.21c2.77-2.64 6.89-2.76 10.13-.73 3.87 2.42 8.26 3.72 12.82 3.72h28.11c6.5 0 11.8-5.92 11.8-13.19 0-5.95-3.61-11.19-8.77-12.73l-45-13.5c-18.59-5.58-31.58-23.42-31.58-43.39 0-24.52 19.05-44.44 42.67-45.07V232c0-4.42 3.58-8 8-8h16c4.42 0 8 3.58 8 8v24.29c11.29.58 22.27 4.51 31.37 11.35 3.9 2.93 4.1 8.77.57 12.14l-11.75 11.21c-2.77 2.64-6.89 2.76-10.13.73-3.87-2.43-8.26-3.72-12.82-3.72h-28.11c-6.5 0-11.8 5.92-11.8 13.19 0 5.95 3.61 11.19 8.77 12.73l45 13.5c18.59 5.58 31.58 23.42 31.58 43.39 0 24.53-19.05 44.44-42.67 45.07z"></path></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,4 +1,5 @@
.app-footer {
z-index: 3;
background-color: $color-white;
border-top: 1px solid $color-gray-lightest;
display: flex;

View File

@ -1,6 +1,9 @@
.global-navigation {
z-index: 2;
background-color: $color-white;
height: auto;
box-shadow: $box-shadow;
margin-bottom: -$footer-height * 2.5;
.sidenav__link {
padding-right: $gap * 2;

View File

@ -29,9 +29,15 @@
&__name {
@include h1;
h1 {
margin: ($gap * 2) $gap ($gap * 2) $gap;
margin: 0 $gap ($gap * 2) 0;
font-size: 3.5rem;
}
p {
font-size: $small-font-size;
margin: 0 0 (-$gap * 0.5);
color: $color-gray-medium;
}
}
&__budget {
@ -69,17 +75,26 @@
font-size: $small-font-size;
.icon-link {
padding: 0.8rem 1.6rem;
padding: $gap;
border-radius: 0;
color: $color-blue-darkest;
&:hover {
background-color: $color-aqua-lightest;
}
color: $color-gray;
opacity: 0.3;
.icon {
@include icon-color($color-gray);
@include icon-color($color-blue-darkest);
}
&.active {
opacity: 1;
color: $color-blue;
background-color: $color-gray-lightest;
&:hover {
background-color: $color-aqua-lightest;
}
.icon {
@include icon-color($color-blue);
}
@ -121,7 +136,7 @@
}
.portfolio-content {
margin: 1 * $gap $gap 0 $gap;
margin: (4 * $gap) $gap 0 $gap;
.panel {
@include shadow-panel;
@ -133,11 +148,6 @@
padding-bottom: 0;
}
.subheading {
font-size: 1.4rem;
color: $color-gray;
}
.responsive-table-wrapper {
padding-bottom: $gap * 3;
margin-bottom: 0;
@ -219,6 +229,8 @@
}
.application-content {
margin-top: $gap * 4;
.subheading {
@include subheading;
position: relative;
@ -247,7 +259,6 @@
textarea {
max-height: 9rem;
max-width: none;
}
.panel__footer {

View File

@ -44,4 +44,9 @@
flex-grow: 1;
padding-right: $spacing-small;
}
&.col--half {
width: 50%;
max-width: 30em;
}
}

View File

@ -29,3 +29,24 @@
}
}
}
.action-group-footer {
@extend .action-group;
&:last-child {
margin-bottom: 0;
}
margin-top: 0;
margin-bottom: 0;
padding-top: $gap;
padding-bottom: $gap;
position: fixed;
bottom: $footer-height;
background: white;
right: 0;
padding-right: $gap * 4;
border-top: 1px solid $color-gray-light;
width: 100%;
z-index: 1;
}

View File

@ -57,13 +57,9 @@
}
.usa-input {
margin: ($gap * 4) ($gap * 2) ($gap * 4) 0;
margin: ($gap * 2) 0;
max-width: 75rem;
@include media($medium-screen) {
margin: ($gap * 4) 0;
}
label {
padding: 0 0 ($gap / 2) 0;
margin: 0;
@ -363,6 +359,8 @@ select {
margin-right: $gap * 4;
.usa-input {
margin: 0;
input,
label,
.usa-input__message {
@ -379,6 +377,8 @@ select {
margin-left: $gap * 4;
.usa-input {
margin: 0;
input {
max-width: 12rem;
}

View File

@ -18,7 +18,6 @@
}
.usa-input {
margin: 0 ($gap * 4) 0 0;
flex-grow: 2;
}
}
@ -28,6 +27,10 @@
.form-content--app-mem {
text-align: left;
.modal__body {
min-width: 75rem;
}
input[type="checkbox"] + label::before {
margin-left: 0;
}
@ -58,11 +61,8 @@
}
}
.form-row {
margin-top: 0;
.application-member__user-info {
.usa-input {
margin: 0;
width: 45rem;
input,

View File

@ -5,7 +5,7 @@
{% block portfolio_header %}
{% include "portfolios/header.html" %}
{% if application %}
{{ StickyCTA(text=application.name, return_link_url=url_for('applications.portfolio_applications', portfolio_id=application.portfolio_id), return_link_text="BACK TO APPLICATIONS") }}
{{ StickyCTA(text=application.name) }}
{% endif %}
{% endblock %}

View File

@ -52,20 +52,12 @@
{% endmacro %}
{% macro InfoFields(member_form) %}
<div class='form-row'>
<div class="application-member__user-info">
{{ TextInput(member_form.first_name, validation='requiredField', optional=False) }}
</div>
<div class='form-row'>
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
</div>
<div class='form-row'>
{{ TextInput(member_form.email, validation='email', optional=False) }}
</div>
<div class="form-row">
{{ PhoneInput(member_form.phone_number, member_form.phone_ext)}}
</div>
<div class='form-row'>
{{ TextInput(member_form.dod_id, validation='dodId', optional=False) }}
<a href="#">How do I find the DoD ID?</a>
</div>
<a href="#">How do I find the DoD ID?</a>
{% endmacro %}

View File

@ -23,29 +23,28 @@
<application-name-and-description inline-template v-bind:initial-data='{{ form.data|tojson }}'>
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit">
<div class="panel__content">
{{ form.csrf_token }}
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(form.name, optional=False) }}
{{ ('portfolios.applications.new.step_1_form_help_text.description' | translate | safe) }}
</div>
</div>
<hr>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(form.description, paragraph=True, optional=True) }}
{{ ('portfolios.applications.new.step_1_form_help_text.description' | translate | safe) }}
</div>
</div>
{{ form.csrf_token }}
<div class="form-row">
<div class="form-col">
{{ TextInput(form.name, optional=False) }}
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
</div>
</div>
<hr class="panel__break">
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(form.description, paragraph=True, optional=True) }}
{{ ('portfolios.applications.new.step_1_form_help_text.description' | translate | safe) }}
</div>
</div>
<span class="action-group">
<span class="action-group-footer">
{% block next_button %}
{{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }}
{% endblock %}
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
<button disabled class="usa-button usa-button-secondary">Previous</button>
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
Cancel
</a>
</span>

View File

@ -16,66 +16,63 @@
{% set modalName = "newApplicationConfirmation" %}
{% include "fragments/flash.html" %}
<div class="panel__content">
<p>
{{ 'portfolios.applications.new.step_2_description' | translate }}
</p>
<hr>
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit">
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
<div class="panel">
<div class="panel__content">
{{ form.csrf_token }}
<div> {# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #}
<div v-cloak v-for="title in errors" :key="title">
{{ Alert(message=None, level="error", vue_template=True) }}
</div>
<p>
{{ 'portfolios.applications.new.step_2_description' | translate }}
</p>
<hr class="panel__break">
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit">
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
<div class="panel">
<div class="panel__content">
{{ form.csrf_token }}
<div> {# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #}
<div v-cloak v-for="title in errors" :key="title">
{{ Alert(message=None, level="error", vue_template=True) }}
</div>
<div class="application-list-item">
<ul>
<li v-for="(environment, i) in environments" class="application-edit__env-list-item">
<div class="usa-input">
<label :for="'environment_names-' + i">Environment Name</label>
<input type="text" :id="'environment_names-' + i" v-model="environment.name" @input="onInput" placeholder="e.g. Development, Staging, Production"/> <input type="hidden" :name="'environment_names-' + i" v-model="environment.name"/>
</div>
<div class="application-edit__env-list-item-block">
<button v-on:click="removeEnvironment(i)" v-if="environments.length > 1" type="button" class="application-edit__env-list-item__remover">
{{ Icon('trash') }}
<span>Remove</span>
</button>
</div>
</li>
</ul>
<div class="block-list__footer">
<button
v-on:click="addEnvironment"
class="icon-link"
tabindex="0"
type="button">
{{ 'portfolios.applications.add_another_environment' | translate }}
{{ Icon("plus") }}
</button>
</div>
</div>
<div class="application-list-item">
<ul>
<li v-for="(environment, i) in environments" class="application-edit__env-list-item">
<div class="usa-input">
<label :for="'environment_names-' + i">Environment Name</label>
<input type="text" :id="'environment_names-' + i" v-model="environment.name" @input="onInput" placeholder="e.g. Development, Staging, Production"/> <input type="hidden" :name="'environment_names-' + i" v-model="environment.name"/>
</div>
<div class="application-edit__env-list-item-block">
<button v-on:click="removeEnvironment(i)" v-if="environments.length > 1" type="button" class="application-edit__env-list-item__remover">
{{ Icon('trash') }}
<span>Remove</span>
</button>
</div>
</li>
</ul>
<div class="block-list__footer">
<button
v-on:click="addEnvironment"
class="icon-link"
tabindex="0"
type="button">
{{ 'portfolios.applications.add_another_environment' | translate }}
{{ Icon("plus") }}
</button>
</div>
</div>
</div>
<span class="action-group">
{% block next_button %}
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
{% endblock %}
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_1', application_id=application.id) }}">
Previous
</a>
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
Cancel
</a>
</span>
</form>
</application-environments>
</div>
</div>
<span class="action-group-footer">
{% block next_button %}
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
{% endblock %}
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_1', application_id=application.id) }}">
Previous
</a>
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
Cancel
</a>
</span>
</form>
</application-environments>
{% endblock %}

View File

@ -11,31 +11,30 @@
{% block application_content %}
{% include "fragments/flash.html" %}
<div class="panel__content">
<p>
{{ ('portfolios.applications.new.step_3_description' | translate) }}
</p>
<hr>
<p>
{{ ('portfolios.applications.new.step_3_description' | translate) }}
</p>
<hr class="panel__break">
{{ MemberManagementTemplate(
application,
members,
new_member_form,
"applications.update_new_application_step_3",
user_can(permissions.CREATE_APPLICATION_MEMBER)) }}
{{ MemberManagementTemplate(
application,
members,
new_member_form,
"applications.update_new_application_step_3",
user_can(permissions.CREATE_APPLICATION_MEMBER)) }}
<span class="action-group">
<a class="usa-button" href="{{ url_for('applications.settings', application_id=application_id) }}">
Return to Application Settings
</a>
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_2', application_id=application.id) }}">
Previous
</a>
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
Cancel
</a>
</span>
</div>
<span class="action-group-footer">
<a class="usa-button" href="{{ url_for('applications.settings', application_id=application_id) }}">
Return to Application Settings
</a>
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_2', application_id=application.id) }}">
Previous
</a>
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
Cancel
</a>
</span>
{% endblock %}

View File

@ -19,58 +19,40 @@
{% block application_content %}
<div class='subheading'>{{ 'portfolios.applications.settings.name_description' | translate }}</div>
<h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3>
{% if user_can(permissions.EDIT_APPLICATION) %}
<base-form inline-template>
<form method="POST" action="{{ url_for('applications.update', application_id=application.id) }}">
<div class="panel">
<div class="panel__content">
{{ application_form.csrf_token }}
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(application_form.name, optional=False) }}
{{ TextInput(application_form.description, paragraph=True, optional=True) }}
</div>
</div>
</div>
<div class="panel__footer">
<div class="action-group">
{{ SaveButton('common.save_changes'|translate) }}
</div>
</div>
<form method="POST" action="{{ url_for('applications.update', application_id=application.id) }}" class="col col--half">
{{ application_form.csrf_token }}
{{ TextInput(application_form.name, optional=False) }}
{{ TextInput(application_form.description, paragraph=True, optional=True, showOptional=False) }}
<div class="action-group action-group--tight">
{{ SaveButton('common.save_changes'|translate) }}
</div>
</form>
</base-form>
{% else %}
<div class="panel">
<div class="panel__content">
<p>
{{ "fragments.edit_application_form.explain" | translate }}
</p>
<div class="form-row">
<div class="form-col">
<div class="usa-input usa-input__title__view-only">
{{ application_form.name.label() }}
</div>
<p>
{{ application_form.name.data }}
</p>
</div>
</div>
<div class="form-row">
<div class="form-col">
<div class="usa-input usa-input__title__view-only">
{{ application_form.description.label() }}
</div>
<p>
{{ application_form.description.data }}
</p>
</div>
</div>
<!-- TODO: use new spacing styling to add in bottom margin here -->
<div class="">
<p>
{{ "fragments.edit_application_form.explain" | translate }}
</p>
<div class="usa-input usa-input__title__view-only">
{{ application_form.name.label() }}
</div>
<p>
{{ application_form.name.data }}
</p>
<div class="usa-input usa-input__title__view-only">
{{ application_form.description.label() }}
</div>
<p>
{{ application_form.description.data }}
</p>
</div>
{% endif %}
<hr>
{{ MemberManagementTemplate(
application,

View File

@ -27,7 +27,7 @@
{% if vue_template %}
<h3 class='usa-alert-heading' v-html='title'></h3>
{% elif title %}
<h3 class='usa-alert-heading'>{{title}}</h3>
<h3 class='usa-alert-heading'>{{ title | safe }}</h3>
{% endif %}
{% if message %}

View File

@ -1,14 +1,7 @@
{% from 'components/icon.html' import Icon %}
{% macro StickyCTA(text, context=None, return_link_url=None, return_link_text=None) -%}
{% macro StickyCTA(text, context=None) -%}
<div class="sticky-cta" v-sticky='{ "stickyBitStickyOffset": 76 }'>
{% if return_link_url and return_link_text %}
<div class="sticky-cta-return-link">
<a href="{{ return_link_url }}">
{{ Icon('caret_left', classes="icon--tiny icon--blue") }} {{ return_link_text}}
</a>
</div>
{% endif %}
<div class="sticky-cta-container">
<div class="sticky-cta-text">
<h3>{{ text }}</h3>

View File

@ -15,6 +15,7 @@
classes='',
noMaxWidth=False,
optional=True,
showOptional=True,
showLabel=True,
watch=False,
show_validation=True) -%}
@ -40,7 +41,7 @@
<label for={{field.name}}>
<div class="usa-input__title">
{{ label }}
{% if optional %}
{% if optional and showOptional %}
<span class="usa-input-label-helper">(optional)</span>
{% endif %}
{% if tooltip and not disabled %}
@ -108,12 +109,7 @@
/>
{% if show_validation %}
<template v-if='showError'>
<span class='usa-input__message' v-html='validationError'></span>
</template>
<template v-else>
<span class='usa-input__message'></span>
</template>
<span v-if='showError' class='usa-input__message' v-html='validationError'></span>
{% endif %}
</div>

View File

@ -17,13 +17,16 @@
) %}
<div class="subheading" id="application-members">
{{ 'portfolios.applications.settings.team_members' | translate }}
</div>
{% if g.matchesPath("application-members") %}
{% include "fragments/flash.html" %}
{% endif %}
<div class="subheading">
{{ 'portfolios.applications.settings.team_members' | translate }}
</div>
<div class="panel">
{% if not application.members %}
<div class='empty-state panel__content'>
@ -87,7 +90,7 @@
{{ member.update_invite_form.csrf_token }}
{{ member_fields.InfoFields(member.update_invite_form) }}
<div class="action-group">
{{ SaveButton(text='Resend Invite', element='input', additional_classes='action-group__action') }}
<input type="submit" class="usa-button usa-button-primary action-group__action" tabindex="0" value="Resend Invite" />
<a class='action-group__action' v-on:click="closeModal('{{ resend_invite_modal }}')">{{ "common.cancel" | translate }}</a>
</div>
</form>
@ -113,7 +116,7 @@
{%- endif %}
{% endfor %}
<section class="member-list application-list" id="application-members">
<section class="member-list application-list">
<div class='responsive-table-wrapper'>
<table class="atat-table">
<thead>

View File

@ -13,11 +13,19 @@
<div class='portfolio-header row'>
<div class='col col--grow'>
<div class='portfolio-header__name'>
<p>{{ "portfolios.header" | translate }}</p>
<h1>{{ portfolio.name }}</h1>
</div>
</div>
<div class='row links'>
{% if user_can(permissions.VIEW_PORTFOLIO_FUNDING) %}
{% if user_can(permissions.VIEW_PORTFOLIO_ADMIN) %}
{{ Link(
icon='cog',
text='navigation.portfolio_navigation.breadcrumbs.admin' | translate,
url=url_for("portfolios.admin", portfolio_id=portfolio.id),
active=request.url_rule.endpoint == "portfolios.admin",
) }}
{% endif %}{% if user_can(permissions.VIEW_PORTFOLIO_FUNDING) %}
{{ Link(
icon='funding',
text='navigation.portfolio_navigation.breadcrumbs.funding' | translate,
@ -39,13 +47,6 @@
active=request.url_rule.endpoint == "portfolios.reports",
) }}
{% endif %}
{% if user_can(permissions.VIEW_PORTFOLIO_ADMIN) %}
{{ Link(
icon='cog',
text='navigation.portfolio_navigation.breadcrumbs.admin' | translate,
url=url_for("portfolios.admin", portfolio_id=portfolio.id),
active=request.url_rule.endpoint == "portfolios.admin",
) }}
{% endif %}
</div>
</div>

View File

@ -1,18 +0,0 @@
BROWSERSTACK_CONFIG = {
"win7_ie10": {
"browser": "IE",
"browser_version": "10.0",
"os": "Windows",
"os_version": "7",
"resolution": "1024x768",
"browserstack.local": True,
},
"win10_chrome62": {
"browser": "Chrome",
"browser_version": "62.0",
"os": "Windows",
"os_version": "10",
"resolution": "1024x768",
"browserstack.local": True,
},
}

View File

@ -1,65 +0,0 @@
import os
import pytest
import logging
from collections import Mapping
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from .browsers import BROWSERSTACK_CONFIG
@pytest.fixture(scope="function", autouse=True)
def session(db, request):
"""
Override base test session
"""
pass
class DriverCollection(Mapping):
"""
Allows access to drivers with dictionary syntax. Keeps track of which ones
have already been initialized. Allows teardown of all existing drivers.
"""
def __init__(self):
self._drivers = {}
def __iter__(self):
return iter(self._drivers)
def __len__(self):
return len(self._drivers)
def __getitem__(self, name):
if name in self._drivers:
return self._drivers[name]
elif name in BROWSERSTACK_CONFIG:
self._drivers[name] = self._build_driver(name)
return self._drivers[name]
else:
raise AttributeError("Driver {} not found".format(name))
def _build_driver(self, config_key):
return webdriver.Remote(
command_executor="http://{}:{}@hub.browserstack.com:80/wd/hub".format(
os.getenv("BROWSERSTACK_EMAIL"), os.getenv("BROWSERSTACK_TOKEN")
),
desired_capabilities=BROWSERSTACK_CONFIG.get(config_key),
)
def teardown(self):
for driver in self._drivers.values():
driver.quit()
@pytest.fixture(scope="session")
def drivers():
driver_collection = DriverCollection()
yield driver_collection
driver_collection.teardown()

View File

@ -1,50 +0,0 @@
import pytest
import requests
from flask import url_for
from urllib.parse import urljoin
from .browsers import BROWSERSTACK_CONFIG
from atst.domain.users import Users
import atst.domain.exceptions as exceptions
from atst.routes.dev import _DEV_USERS as DEV_USERS
from tests.test_auth import _login
import cryptography.x509 as x509
from cryptography.hazmat.backends import default_backend
USER_CERT = "ssl/client-certs/atat.mil.crt"
@pytest.mark.parametrize("browser_type", BROWSERSTACK_CONFIG.keys())
@pytest.mark.usefixtures("live_server")
def test_can_get_title(browser_type, app, drivers):
driver = drivers[browser_type]
driver.get(url_for("atst.root", _external=True))
assert "JEDI" in driver.title
def _get_common_name(cert_path):
with open(USER_CERT, "rb") as cert_file:
cert = x509.load_pem_x509_certificate(cert_file.read(), default_backend())
common_names = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
return common_names[0].value
@pytest.fixture(scope="module")
def valid_user_from_cert():
cn = _get_common_name(USER_CERT)
cn_parts = cn.split(".")
user_info = {
"last_name": cn_parts[0],
"first_name": cn_parts[1],
"dod_id": cn_parts[-1],
"atat_role_name": "developer",
}
return Users.get_or_create_by_dod_id(**user_info)
@pytest.mark.usefixtures("live_server")
def test_login(drivers, client, app, valid_user_from_cert):
driver = drivers["win7_ie10"]
driver.get(url_for("dev.login_dev", _external=True))
assert "Sign in" not in driver.title

View File

@ -1,3 +1,5 @@
from atst.domain.application_roles import ApplicationRoles
from atst.models import ApplicationRoleStatus
from atst.models import AuditEvent
from tests.factories import (
@ -16,6 +18,22 @@ def test_application_environments_excludes_deleted():
assert app.environments[0].id == env.id
def test_application_members_excludes_deleted(session):
app = ApplicationFactory.create()
member_role = ApplicationRoleFactory.create(
application=app, status=ApplicationRoleStatus.ACTIVE
)
disabled_role = ApplicationRoleFactory.create(
application=app, status=ApplicationRoleStatus.DISABLED
)
disabled_role.deleted = True
session.add(disabled_role)
session.commit()
assert len(app.members) == 1
assert app.members[0].id == member_role.id
def test_audit_event_for_application_deletion(session):
app = ApplicationFactory.create()
app.deleted = True

View File

@ -10,6 +10,7 @@ from atst.domain.application_roles import ApplicationRoles
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.common import Paginator
from atst.domain.permission_sets import PermissionSets
from atst.models.application_role import Status as ApplicationRoleStatus
from atst.models.environment_role import CSPRole
from atst.models.permissions import Permissions
from atst.forms.application import EditEnvironmentForm
@ -560,6 +561,8 @@ def test_revoke_invite(client, user_session):
)
assert invite.is_revoked
assert app_role.status == ApplicationRoleStatus.DISABLED
assert app_role.deleted
def test_filter_environment_roles():

View File

@ -107,7 +107,9 @@ email:
environment_ready: JEDI cloud environment ready
flash:
application:
created: 'You have successfully created the {application_name} application.'
created:
title: Application Saved
message: '{application_name} has been successfully created. You may continue on to provision environments and assign team members now, or come back and complete these tasks at a later time.'
updated: 'You have successfully updated the {application_name} application.'
deleted: 'You have successfully deleted the {application_name} application. To view the retained activity log, visit the portfolio administration page.'
delete_member_success: 'You have successfully deleted {member_name} from the portfolio.'
@ -119,7 +121,9 @@ flash:
new_ppoc_message: 'You have successfully added {ppoc_name} as the primary point of contact. You are no longer the PPoC.'
new_ppoc_title: Primary point of contact updated
success: Success!
new_application_member: '{user_name} has been added to {application_name}. They will not have access until they accept the invitation e-mailed to them and CSP processing is complete.'
new_application_member:
title: "{user_name}'s invitation has been sent"
message: "{user_name}'s access to this Application is pending until they sign in for the first time."
updated_application_team_settings: 'You have updated the {application_name} team settings.'
logged_out: Logged out
footer:
@ -127,11 +131,11 @@ footer:
login: 'Last login:'
forms:
application:
description_label: Description
description_label: Application Description
environment_names_label: Environment Name
environment_names_required_validation_message: Provide at least one environment name.
environment_names_unique_validation_message: Environment names must be unique.
name_label: Name
name_label: Application Name
assign_ppoc:
dod_id: 'Select new primary point of contact:'
environments:
@ -283,8 +287,8 @@ login:
navigation:
portfolio_navigation:
breadcrumbs:
admin: Admin
funding: Funding
admin: Settings
funding: Task Orders
reports: Reports
applications: Applications
topbar:
@ -311,12 +315,12 @@ portfolios:
app_settings_text: App settings
new:
step_1_header: Name and Describe New Application
step_1_button_text: "Save and Add Environments"
step_1_button_text: "Next: Add Environments"
step_1_form_help_text:
name: |
<div style="margin-top: -3rem;">
<div>
<p>
The name of your application should be intuitive and easily recognizable for all of your team members.
The name of your Application should be intuitive and easily recognizable for all of your team members.
</p>
<p>
<strong>Writer's Block? A naming example includes:</strong>
@ -326,12 +330,12 @@ portfolios:
</p>
</div>
description: |
<div style="margin-top: -3rem;">
<div>
<p>
Add a brief one to two sentence description of your application. You should be able to reference your TO Description of Work.
</p>
<p>
<strong>Writer's Block? A naming example includes:</strong>
<strong>Writer's Block? A description example includes:</strong>
<ul>
<li>Build security applications for FOB Clark</li>
</ul>
@ -339,9 +343,9 @@ portfolios:
</div>
step_2_header: Add Environments to {application_name}
step_2_description: "Production, Staging, Testing, and Development environments are included by default. However, you can add, edit, and delete environments based on the needs of your Application."
step_2_button_text: "Save and Add Members"
step_3_header: Invite Members to {application_name}
step_3_description: "To proceed, you will need each member's email address and DOD ID. Within this section, you will also assign application-level permissions and environment-level roles for each member."
step_2_button_text: "Next: Add Members"
step_3_header: Add Members to {application_name}
step_3_description: "To proceed, you will need each member's email address and DOD ID. Within this section, you will also assign Application-level permissions and environment-level roles for each member."
step_3_button_text: Save Application
create_new_env: Create a new environment.
create_new_env_info: Creating an environment gives you access to the Cloud Service Provider. This environment will function within the constraints of the task order, and any costs will be billed against the portfolio.
@ -371,7 +375,7 @@ portfolios:
new_application_title: New Application
settings_heading: Application Settings
settings:
name_description: Name and Description
name_description: Application name and description
team_members: Team Members
team_settings:
blank_slate:
@ -424,6 +428,7 @@ portfolios:
empty:
start_button: Start a new JEDI portfolio
title: You have no apps yet
header: PORTFOLIO
members:
archive_button: Delete member
permissions: