Merge branch 'master' into ui/input-field-frontend-validation
This commit is contained in:
commit
3f5d961513
@ -16,12 +16,14 @@ log/*
|
|||||||
LICENSE
|
LICENSE
|
||||||
*.md
|
*.md
|
||||||
|
|
||||||
# Skip pipenv/virtualenv related things
|
# Skip envrc
|
||||||
.envrc
|
.envrc
|
||||||
.venv
|
|
||||||
|
|
||||||
# Skip ansible-container stuff
|
# Skip ansible-container stuff
|
||||||
ansible*
|
ansible*
|
||||||
container.yml
|
container.yml
|
||||||
meta.yml
|
meta.yml
|
||||||
requirements.yml
|
requirements.yml
|
||||||
|
|
||||||
|
# Skip kubernetes and Docker config stuff
|
||||||
|
deploy
|
||||||
|
12
Pipfile.lock
generated
12
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "2b149e0d8c23814a2c701b53f5c75b36714a2ccd4e2a2769924ef6e2a3f09e97"
|
"sha256": "5fc8273838354406366b401529a6f512a73ac6a8ecea6699afa4ab7b4996bf13"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -271,6 +271,7 @@
|
|||||||
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
|
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
|
||||||
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
|
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'",
|
||||||
"version": "==2018.5"
|
"version": "==2018.5"
|
||||||
},
|
},
|
||||||
"redis": {
|
"redis": {
|
||||||
@ -501,6 +502,7 @@
|
|||||||
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
|
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
|
||||||
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
|
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
|
||||||
"version": "==4.3.4"
|
"version": "==4.3.4"
|
||||||
},
|
},
|
||||||
"itsdangerous": {
|
"itsdangerous": {
|
||||||
@ -618,6 +620,7 @@
|
|||||||
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
||||||
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
|
||||||
"version": "==0.7.1"
|
"version": "==0.7.1"
|
||||||
},
|
},
|
||||||
"prompt-toolkit": {
|
"prompt-toolkit": {
|
||||||
@ -640,6 +643,7 @@
|
|||||||
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
|
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
|
||||||
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
|
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
|
||||||
"version": "==1.5.4"
|
"version": "==1.5.4"
|
||||||
},
|
},
|
||||||
"pygments": {
|
"pygments": {
|
||||||
@ -689,15 +693,11 @@
|
|||||||
},
|
},
|
||||||
"pyyaml": {
|
"pyyaml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1cbc199009e78f92d9edf554be4fe40fb7b0bef71ba688602a00e97a51909110",
|
|
||||||
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
|
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
|
||||||
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
|
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
|
||||||
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
|
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
|
||||||
"sha256:6f89b5c95e93945b597776163403d47af72d243f366bf4622ff08bdfd1c950b7",
|
|
||||||
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
|
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
|
||||||
"sha256:be622cc81696e24d0836ba71f6272a2b5767669b0d79fdcf0295d51ac2e156c8",
|
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b"
|
||||||
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b",
|
|
||||||
"sha256:f39411e380e2182ad33be039e8ee5770a5d9efe01a2bfb7ae58d9ba31c4a2a9d"
|
|
||||||
],
|
],
|
||||||
"version": "==4.2b4"
|
"version": "==4.2b4"
|
||||||
},
|
},
|
||||||
|
43
alembic/versions/05d6272bdb43_rename_request_creator_.py
Normal file
43
alembic/versions/05d6272bdb43_rename_request_creator_.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
"""rename request creator
|
||||||
|
|
||||||
|
Revision ID: 05d6272bdb43
|
||||||
|
Revises: 77b065750596
|
||||||
|
Create Date: 2018-08-07 20:21:22.559283
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '05d6272bdb43'
|
||||||
|
down_revision = '77b065750596'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
db = op.get_bind()
|
||||||
|
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('requests', sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True))
|
||||||
|
op.create_foreign_key('requests_user_id_fk', 'requests', 'users', ['user_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
db.execute("UPDATE requests SET user_id = creator")
|
||||||
|
|
||||||
|
op.alter_column('requests', 'user_id', nullable=False)
|
||||||
|
op.drop_column('requests', 'creator')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
db = op.get_bind()
|
||||||
|
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('requests', sa.Column('creator', postgresql.UUID(), autoincrement=False, nullable=True))
|
||||||
|
op.drop_constraint('requests_user_id_fk', 'requests', type_='foreignkey')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
db.execute("UPDATE requests SET creator = user_id")
|
||||||
|
op.drop_column('requests', 'user_id')
|
@ -34,6 +34,4 @@ def upgrade():
|
|||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
db = op.get_bind()
|
pass
|
||||||
db.execute("DELETE FROM roles WHERE name = 'default'")
|
|
||||||
|
|
||||||
|
49
alembic/versions/77b065750596_new_request_statuses.py
Normal file
49
alembic/versions/77b065750596_new_request_statuses.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""new request statuses
|
||||||
|
|
||||||
|
Revision ID: 77b065750596
|
||||||
|
Revises: 1f57f784ed5b
|
||||||
|
Create Date: 2018-08-07 16:42:11.502361
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.orm.session import sessionmaker
|
||||||
|
|
||||||
|
from atst.models.request_status_event import RequestStatus
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '77b065750596'
|
||||||
|
down_revision = '1f57f784ed5b'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
"""
|
||||||
|
Update all existing request statuses so that the state of the
|
||||||
|
table reflects the statuses listed in RequestStatus.
|
||||||
|
|
||||||
|
This involves fixing the casing on existing statuses, and
|
||||||
|
deleting statuses that have no match.
|
||||||
|
"""
|
||||||
|
|
||||||
|
db = op.get_bind()
|
||||||
|
|
||||||
|
status_events = db.execute("SELECT * FROM request_status_events").fetchall()
|
||||||
|
for status_event in status_events:
|
||||||
|
try:
|
||||||
|
status = RequestStatus[status_event["new_status"].upper()]
|
||||||
|
query = sa.text("""
|
||||||
|
UPDATE request_status_events
|
||||||
|
SET new_status = :status
|
||||||
|
WHERE id = :id"""
|
||||||
|
)
|
||||||
|
db.execute(query, id=status_event["id"], status=status.name)
|
||||||
|
except KeyError:
|
||||||
|
query = sa.text("DELETE FROM request_status_events WHERE id = :id")
|
||||||
|
db.execute(query, id=status_event["id"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
@ -169,15 +169,4 @@ def upgrade():
|
|||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
db = op.get_bind()
|
pass
|
||||||
db.execute("""
|
|
||||||
DELETE FROM roles
|
|
||||||
WHERE name IN (
|
|
||||||
'ccpo',
|
|
||||||
'owner',
|
|
||||||
'admin',
|
|
||||||
'developer',
|
|
||||||
'billing_auditor',
|
|
||||||
'security_auditor'
|
|
||||||
);
|
|
||||||
""")
|
|
||||||
|
@ -15,6 +15,7 @@ from atst.routes import bp
|
|||||||
from atst.routes.workspaces import bp as workspace_routes
|
from atst.routes.workspaces import bp as workspace_routes
|
||||||
from atst.routes.requests import requests_bp
|
from atst.routes.requests import requests_bp
|
||||||
from atst.routes.dev import bp as dev_routes
|
from atst.routes.dev import bp as dev_routes
|
||||||
|
from atst.routes.errors import make_error_pages
|
||||||
from atst.domain.authnid.crl.validator import Validator
|
from atst.domain.authnid.crl.validator import Validator
|
||||||
from atst.domain.auth import apply_authentication
|
from atst.domain.auth import apply_authentication
|
||||||
|
|
||||||
@ -45,10 +46,11 @@ def make_app(config):
|
|||||||
Session(app)
|
Session(app)
|
||||||
assets_environment.init_app(app)
|
assets_environment.init_app(app)
|
||||||
|
|
||||||
|
make_error_pages(app)
|
||||||
app.register_blueprint(bp)
|
app.register_blueprint(bp)
|
||||||
app.register_blueprint(workspace_routes)
|
app.register_blueprint(workspace_routes)
|
||||||
app.register_blueprint(requests_bp)
|
app.register_blueprint(requests_bp)
|
||||||
if ENV != "production":
|
if ENV != "prod":
|
||||||
app.register_blueprint(dev_routes)
|
app.register_blueprint(dev_routes)
|
||||||
|
|
||||||
apply_authentication(app)
|
apply_authentication(app)
|
||||||
|
@ -14,3 +14,19 @@ class AlreadyExistsError(Exception):
|
|||||||
@property
|
@property
|
||||||
def message(self):
|
def message(self):
|
||||||
return "{} already exists".format(self.resource_name)
|
return "{} already exists".format(self.resource_name)
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedError(Exception):
|
||||||
|
def __init__(self, user, action):
|
||||||
|
self.user = user
|
||||||
|
self.action = action
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "User {} not authorized to {}".format(self.user.id, self.action)
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthenticatedError(Exception):
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return str(self)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from sqlalchemy import exists, and_
|
from sqlalchemy import exists, and_, exc
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
|
||||||
from atst.models import Request, RequestStatusEvent
|
from atst.models.request import Request
|
||||||
|
from atst.models.request_status_event import RequestStatusEvent, RequestStatus
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
|
|
||||||
from .exceptions import NotFoundError
|
from .exceptions import NotFoundError
|
||||||
@ -30,11 +31,9 @@ class Requests(object):
|
|||||||
AUTO_APPROVE_THRESHOLD = 1000000
|
AUTO_APPROVE_THRESHOLD = 1000000
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, creator_id, body):
|
def create(cls, creator, body):
|
||||||
request = Request(creator=creator_id, body=body)
|
request = Request(creator=creator, body=body)
|
||||||
|
request = Requests.set_status(request, RequestStatus.STARTED)
|
||||||
status_event = RequestStatusEvent(new_status="incomplete")
|
|
||||||
request.status_events.append(status_event)
|
|
||||||
|
|
||||||
db.session.add(request)
|
db.session.add(request)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -42,12 +41,15 @@ class Requests(object):
|
|||||||
return request
|
return request
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def exists(cls, request_id, creator_id):
|
def exists(cls, request_id, creator):
|
||||||
return db.session.query(
|
try:
|
||||||
exists().where(
|
return db.session.query(
|
||||||
and_(Request.id == request_id, Request.creator == creator_id)
|
exists().where(
|
||||||
)
|
and_(Request.id == request_id, Request.creator == creator)
|
||||||
).scalar()
|
)
|
||||||
|
).scalar()
|
||||||
|
except exc.DataError:
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, request_id):
|
def get(cls, request_id):
|
||||||
@ -59,10 +61,10 @@ class Requests(object):
|
|||||||
return request
|
return request
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_many(cls, creator_id=None):
|
def get_many(cls, creator=None):
|
||||||
filters = []
|
filters = []
|
||||||
if creator_id:
|
if creator:
|
||||||
filters.append(Request.creator == creator_id)
|
filters.append(Request.creator == creator)
|
||||||
|
|
||||||
requests = (
|
requests = (
|
||||||
db.session.query(Request)
|
db.session.query(Request)
|
||||||
@ -74,10 +76,13 @@ class Requests(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def submit(cls, request):
|
def submit(cls, request):
|
||||||
request.status_events.append(RequestStatusEvent(new_status="submitted"))
|
new_status = None
|
||||||
|
|
||||||
if Requests.should_auto_approve(request):
|
if Requests.should_auto_approve(request):
|
||||||
request.status_events.append(RequestStatusEvent(new_status="approved"))
|
new_status = RequestStatus.PENDING_FINANCIAL_VERIFICATION
|
||||||
|
else:
|
||||||
|
new_status = RequestStatus.PENDING_CCPO_APPROVAL
|
||||||
|
|
||||||
|
request = Requests.set_status(request, new_status)
|
||||||
|
|
||||||
db.session.add(request)
|
db.session.add(request)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -100,11 +105,6 @@ class Requests(object):
|
|||||||
|
|
||||||
request.body = deep_merge(request_delta, request.body)
|
request.body = deep_merge(request_delta, request.body)
|
||||||
|
|
||||||
if Requests.should_allow_submission(request):
|
|
||||||
request.status_events.append(
|
|
||||||
RequestStatusEvent(new_status="pending_submission")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Without this, sqlalchemy won't notice the change to request.body,
|
# Without this, sqlalchemy won't notice the change to request.body,
|
||||||
# since it doesn't track dictionary mutations by default.
|
# since it doesn't track dictionary mutations by default.
|
||||||
flag_modified(request, "body")
|
flag_modified(request, "body")
|
||||||
@ -112,6 +112,20 @@ class Requests(object):
|
|||||||
db.session.add(request)
|
db.session.add(request)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_status(cls, request: Request, status: RequestStatus):
|
||||||
|
status_event = RequestStatusEvent(new_status=status)
|
||||||
|
request.status_events.append(status_event)
|
||||||
|
return request
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def action_required_by(cls, request):
|
||||||
|
return {
|
||||||
|
RequestStatus.STARTED: "mission_owner",
|
||||||
|
RequestStatus.PENDING_FINANCIAL_VERIFICATION: "mission_owner",
|
||||||
|
RequestStatus.PENDING_CCPO_APPROVAL: "ccpo"
|
||||||
|
}.get(request.status)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def should_auto_approve(cls, request):
|
def should_auto_approve(cls, request):
|
||||||
try:
|
try:
|
||||||
@ -129,6 +143,10 @@ class Requests(object):
|
|||||||
"primary_poc",
|
"primary_poc",
|
||||||
]
|
]
|
||||||
existing_request_sections = request.body.keys()
|
existing_request_sections = request.body.keys()
|
||||||
return request.status == "incomplete" and all(
|
return request.status == RequestStatus.STARTED and all(
|
||||||
section in existing_request_sections for section in all_request_sections
|
section in existing_request_sections for section in all_request_sections
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_pending_financial_verification(cls, request):
|
||||||
|
return request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
|
||||||
|
@ -37,6 +37,7 @@ class Users(object):
|
|||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
|
db.session.rollback()
|
||||||
raise AlreadyExistsError("user")
|
raise AlreadyExistsError("user")
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
@ -29,8 +29,10 @@ class NewlineListField(Field):
|
|||||||
widget = TextArea()
|
widget = TextArea()
|
||||||
|
|
||||||
def _value(self):
|
def _value(self):
|
||||||
if self.data:
|
if isinstance(self.data, list):
|
||||||
return "\n".join(self.data)
|
return '\n'.join(self.data)
|
||||||
|
elif self.data:
|
||||||
|
return self.data
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ class FinancialForm(ValidatedForm):
|
|||||||
"Unique Item Identifier (UII)s related to your application(s) if you already have them."
|
"Unique Item Identifier (UII)s related to your application(s) if you already have them."
|
||||||
)
|
)
|
||||||
|
|
||||||
pe_id = StringField("Program Element (PE) Number related to your request")
|
pe_id = StringField("Program Element (PE) Number related to your request", validators=[Required()])
|
||||||
|
|
||||||
treasury_code = StringField("Program Treasury Code")
|
treasury_code = StringField("Program Treasury Code")
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ class RequestForm(ValidatedForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
engineering_assessment = RadioField(
|
engineering_assessment = RadioField(
|
||||||
description="Have you completed an engineering assessment of your software systems for cloud readiness?",
|
"Have you completed an engineering assessment of your software systems for cloud readiness?",
|
||||||
choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")],
|
choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from sqlalchemy import Column, func
|
from sqlalchemy import Column, func, ForeignKey
|
||||||
from sqlalchemy.types import DateTime
|
from sqlalchemy.types import DateTime
|
||||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from atst.models import Base
|
from atst.models import Base
|
||||||
@ -11,22 +11,19 @@ class Request(Base):
|
|||||||
__tablename__ = "requests"
|
__tablename__ = "requests"
|
||||||
|
|
||||||
id = Id()
|
id = Id()
|
||||||
creator = Column(UUID(as_uuid=True))
|
|
||||||
time_created = Column(DateTime(timezone=True), server_default=func.now())
|
time_created = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
body = Column(JSONB)
|
body = Column(JSONB)
|
||||||
status_events = relationship(
|
status_events = relationship(
|
||||||
"RequestStatusEvent", backref="request", order_by="RequestStatusEvent.sequence"
|
"RequestStatusEvent", backref="request", order_by="RequestStatusEvent.sequence"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_id = Column(ForeignKey("users.id"), nullable=False)
|
||||||
|
creator = relationship("User")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
return self.status_events[-1].new_status
|
return self.status_events[-1].new_status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def action_required_by(self):
|
def status_displayname(self):
|
||||||
return {
|
return self.status_events[-1].displayname
|
||||||
"incomplete": "mission_owner",
|
|
||||||
"pending_submission": "mission_owner",
|
|
||||||
"submitted": "ccpo",
|
|
||||||
"approved": "mission_owner",
|
|
||||||
}.get(self.status)
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from sqlalchemy import Column, func, ForeignKey
|
from enum import Enum
|
||||||
from sqlalchemy.types import DateTime, String, BigInteger
|
from sqlalchemy import Column, func, ForeignKey, Enum as SQLAEnum
|
||||||
|
from sqlalchemy.types import DateTime, BigInteger
|
||||||
from sqlalchemy.schema import Sequence
|
from sqlalchemy.schema import Sequence
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
|
||||||
@ -7,11 +8,20 @@ from atst.models import Base
|
|||||||
from atst.models.types import Id
|
from atst.models.types import Id
|
||||||
|
|
||||||
|
|
||||||
|
class RequestStatus(Enum):
|
||||||
|
STARTED = "Started"
|
||||||
|
PENDING_FINANCIAL_VERIFICATION = "Pending Financial Verification"
|
||||||
|
PENDING_CCPO_APPROVAL = "Pending CCPO Approval"
|
||||||
|
APPROVED = "Approved"
|
||||||
|
EXPIRED = "Expired"
|
||||||
|
DELETED = "Deleted"
|
||||||
|
|
||||||
|
|
||||||
class RequestStatusEvent(Base):
|
class RequestStatusEvent(Base):
|
||||||
__tablename__ = "request_status_events"
|
__tablename__ = "request_status_events"
|
||||||
|
|
||||||
id = Id()
|
id = Id()
|
||||||
new_status = Column(String())
|
new_status = Column(SQLAEnum(RequestStatus))
|
||||||
time_created = Column(DateTime(timezone=True), server_default=func.now())
|
time_created = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
request_id = Column(
|
request_id = Column(
|
||||||
UUID(as_uuid=True), ForeignKey("requests.id", ondelete="CASCADE")
|
UUID(as_uuid=True), ForeignKey("requests.id", ondelete="CASCADE")
|
||||||
@ -19,3 +29,7 @@ class RequestStatusEvent(Base):
|
|||||||
sequence = Column(
|
sequence = Column(
|
||||||
BigInteger, Sequence("request_status_events_sequence_seq"), nullable=False
|
BigInteger, Sequence("request_status_events_sequence_seq"), nullable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def displayname(self):
|
||||||
|
return self.new_status.value
|
||||||
|
@ -5,6 +5,7 @@ import pendulum
|
|||||||
from atst.domain.requests import Requests
|
from atst.domain.requests import Requests
|
||||||
from atst.domain.users import Users
|
from atst.domain.users import Users
|
||||||
from atst.domain.authnid.utils import parse_sdn
|
from atst.domain.authnid.utils import parse_sdn
|
||||||
|
from atst.domain.exceptions import UnauthenticatedError
|
||||||
|
|
||||||
bp = Blueprint("atst", __name__)
|
bp = Blueprint("atst", __name__)
|
||||||
|
|
||||||
@ -29,6 +30,9 @@ def catch_all(path):
|
|||||||
return render_template("{}.html".format(path))
|
return render_template("{}.html".format(path))
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: this should be partly consolidated into a domain function that takes
|
||||||
|
# all the necessary UWSGI environment values as args and either returns a user
|
||||||
|
# or raises the UnauthenticatedError
|
||||||
@bp.route('/login-redirect')
|
@bp.route('/login-redirect')
|
||||||
def login_redirect():
|
def login_redirect():
|
||||||
if request.environ.get('HTTP_X_SSL_CLIENT_VERIFY') == 'SUCCESS' and _is_valid_certificate(request):
|
if request.environ.get('HTTP_X_SSL_CLIENT_VERIFY') == 'SUCCESS' and _is_valid_certificate(request):
|
||||||
@ -39,15 +43,7 @@ def login_redirect():
|
|||||||
|
|
||||||
return redirect(url_for("atst.home"))
|
return redirect(url_for("atst.home"))
|
||||||
else:
|
else:
|
||||||
return redirect(url_for("atst.unauthorized"))
|
raise UnauthenticatedError()
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/unauthorized")
|
|
||||||
def unauthorized():
|
|
||||||
template = render_template('unauthorized.html')
|
|
||||||
response = app.make_response(template)
|
|
||||||
response.status_code = 401
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_certificate(request):
|
def _is_valid_certificate(request):
|
||||||
|
@ -9,50 +9,56 @@ _DEV_USERS = {
|
|||||||
"dod_id": "1234567890",
|
"dod_id": "1234567890",
|
||||||
"first_name": "Sam",
|
"first_name": "Sam",
|
||||||
"last_name": "Seeceepio",
|
"last_name": "Seeceepio",
|
||||||
"atat_role": "ccpo",
|
"atat_role_name": "ccpo",
|
||||||
|
"email": "sam@test.com"
|
||||||
},
|
},
|
||||||
"amanda": {
|
"amanda": {
|
||||||
"dod_id": "2345678901",
|
"dod_id": "2345678901",
|
||||||
"first_name": "Amanda",
|
"first_name": "Amanda",
|
||||||
"last_name": "Adamson",
|
"last_name": "Adamson",
|
||||||
"atat_role": "default",
|
"atat_role_name": "default",
|
||||||
|
"email": "amanda@test.com"
|
||||||
},
|
},
|
||||||
"brandon": {
|
"brandon": {
|
||||||
"dod_id": "3456789012",
|
"dod_id": "3456789012",
|
||||||
"first_name": "Brandon",
|
"first_name": "Brandon",
|
||||||
"last_name": "Buchannan",
|
"last_name": "Buchannan",
|
||||||
"atat_role": "default",
|
"atat_role_name": "default",
|
||||||
|
"email": "brandon@test.com"
|
||||||
},
|
},
|
||||||
"christina": {
|
"christina": {
|
||||||
"dod_id": "4567890123",
|
"dod_id": "4567890123",
|
||||||
"first_name": "Christina",
|
"first_name": "Christina",
|
||||||
"last_name": "Collins",
|
"last_name": "Collins",
|
||||||
"atat_role": "default",
|
"atat_role_name": "default",
|
||||||
|
"email": "christina@test.com"
|
||||||
},
|
},
|
||||||
"dominick": {
|
"dominick": {
|
||||||
"dod_id": "5678901234",
|
"dod_id": "5678901234",
|
||||||
"first_name": "Dominick",
|
"first_name": "Dominick",
|
||||||
"last_name": "Domingo",
|
"last_name": "Domingo",
|
||||||
"atat_role": "default",
|
"atat_role_name": "default",
|
||||||
|
"email": "dominick@test.com"
|
||||||
},
|
},
|
||||||
"erica": {
|
"erica": {
|
||||||
"dod_id": "6789012345",
|
"dod_id": "6789012345",
|
||||||
"first_name": "Erica",
|
"first_name": "Erica",
|
||||||
"last_name": "Eichner",
|
"last_name": "Eichner",
|
||||||
"atat_role": "default",
|
"atat_role_name": "default",
|
||||||
|
"email": "erica@test.com"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/login-dev")
|
@bp.route("/login-dev")
|
||||||
def login_dev():
|
def login_dev():
|
||||||
role = request.args.get("username", "amanda")
|
role = request.args.get("username", "amanda")
|
||||||
user_data = _DEV_USERS[role]
|
user_data = _DEV_USERS[role]
|
||||||
basic_data = {k:v for k,v in user_data.items() if k not in ["dod_id", "atat_role"]}
|
user = Users.get_or_create_by_dod_id(
|
||||||
user = _set_user_permissions(user_data["dod_id"], user_data["atat_role"], basic_data)
|
user_data["dod_id"],
|
||||||
|
atat_role_name=user_data["atat_role_name"],
|
||||||
|
first_name=user_data["first_name"],
|
||||||
|
last_name=user_data["last_name"],
|
||||||
|
email=user_data["email"]
|
||||||
|
)
|
||||||
session["user_id"] = user.id
|
session["user_id"] = user.id
|
||||||
return redirect(url_for("atst.home"))
|
return redirect(url_for("atst.home"))
|
||||||
|
|
||||||
|
|
||||||
def _set_user_permissions(dod_id, role, user_data):
|
|
||||||
return Users.get_or_create_by_dod_id(dod_id, atat_role_name=role, **user_data)
|
|
||||||
|
21
atst/routes/errors.py
Normal file
21
atst/routes/errors.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from flask import render_template
|
||||||
|
|
||||||
|
import atst.domain.exceptions as exceptions
|
||||||
|
|
||||||
|
|
||||||
|
def make_error_pages(app):
|
||||||
|
@app.errorhandler(exceptions.NotFoundError)
|
||||||
|
@app.errorhandler(exceptions.UnauthorizedError)
|
||||||
|
# pylint: disable=unused-variable
|
||||||
|
def not_found(e):
|
||||||
|
app.logger.error(e.message)
|
||||||
|
return render_template("not_found.html"), 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(exceptions.UnauthenticatedError)
|
||||||
|
# pylint: disable=unused-variable
|
||||||
|
def unauthorized(e):
|
||||||
|
app.logger.error(e.message)
|
||||||
|
return render_template('unauthenticated.html'), 401
|
||||||
|
|
||||||
|
return app
|
@ -25,10 +25,10 @@ def update_financial_verification(request_id):
|
|||||||
|
|
||||||
if form.validate():
|
if form.validate():
|
||||||
request_data = {"financial_verification": post_data}
|
request_data = {"financial_verification": post_data}
|
||||||
Requests.update(request_id, request_data)
|
|
||||||
valid = form.perform_extra_validation(
|
valid = form.perform_extra_validation(
|
||||||
existing_request.body.get("financial_verification")
|
existing_request.body.get("financial_verification")
|
||||||
)
|
)
|
||||||
|
Requests.update(request_id, request_data)
|
||||||
if valid:
|
if valid:
|
||||||
return redirect(url_for("requests.financial_verification_submitted"))
|
return redirect(url_for("requests.financial_verification_submitted"))
|
||||||
else:
|
else:
|
||||||
@ -41,4 +41,4 @@ def update_financial_verification(request_id):
|
|||||||
|
|
||||||
@requests_bp.route("/requests/financial_verification_submitted")
|
@requests_bp.route("/requests/financial_verification_submitted")
|
||||||
def financial_verification_submitted():
|
def financial_verification_submitted():
|
||||||
pass
|
return render_template("requests/financial_verification_submitted.html")
|
||||||
|
@ -1,35 +1,36 @@
|
|||||||
import pendulum
|
import pendulum
|
||||||
from flask import render_template, g
|
from flask import render_template, g, url_for
|
||||||
|
|
||||||
from . import requests_bp
|
from . import requests_bp
|
||||||
from atst.domain.requests import Requests
|
from atst.domain.requests import Requests
|
||||||
|
|
||||||
|
|
||||||
def map_request(user, request):
|
def map_request(request):
|
||||||
time_created = pendulum.instance(request.time_created)
|
time_created = pendulum.instance(request.time_created)
|
||||||
is_new = time_created.add(days=1) > pendulum.now()
|
is_new = time_created.add(days=1) > pendulum.now()
|
||||||
|
app_count = request.body.get("details_of_use", {}).get("num_software_systems", 0)
|
||||||
|
update_url = url_for('requests.requests_form_update', screen=1, request_id=request.id)
|
||||||
|
verify_url = url_for('requests.financial_verification', request_id=request.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"order_id": request.id,
|
"order_id": request.id,
|
||||||
"is_new": is_new,
|
"is_new": is_new,
|
||||||
"status": request.status,
|
"status": request.status_displayname,
|
||||||
"app_count": 1,
|
"app_count": app_count,
|
||||||
"date": time_created.format("M/DD/YYYY"),
|
"date": time_created.format("M/DD/YYYY"),
|
||||||
"full_name": user.full_name
|
"full_name": request.creator.full_name,
|
||||||
|
"edit_link": verify_url if Requests.is_pending_financial_verification(request) else update_url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@requests_bp.route("/requests", methods=["GET"])
|
@requests_bp.route("/requests", methods=["GET"])
|
||||||
def requests_index():
|
def requests_index():
|
||||||
requests = []
|
requests = []
|
||||||
if (
|
if "review_and_approve_jedi_workspace_request" in g.current_user.atat_permissions:
|
||||||
"review_and_approve_jedi_workspace_request"
|
|
||||||
in g.current_user.atat_permissions
|
|
||||||
):
|
|
||||||
requests = Requests.get_many()
|
requests = Requests.get_many()
|
||||||
else:
|
else:
|
||||||
requests = Requests.get_many(creator_id=g.current_user.id)
|
requests = Requests.get_many(creator=g.current_user)
|
||||||
|
|
||||||
mapped_requests = [map_request(g.current_user, r) for r in requests]
|
mapped_requests = [map_request(r) for r in requests]
|
||||||
|
|
||||||
return render_template("requests.html", requests=mapped_requests)
|
return render_template("requests.html", requests=mapped_requests)
|
||||||
|
@ -76,7 +76,7 @@ class JEDIRequestFlow(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def can_submit(self):
|
def can_submit(self):
|
||||||
return self.request and self.request.status != "incomplete"
|
return self.request and Requests.should_allow_submission(self.request)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def next_screen(self):
|
def next_screen(self):
|
||||||
@ -124,5 +124,5 @@ class JEDIRequestFlow(object):
|
|||||||
if self.request_id:
|
if self.request_id:
|
||||||
Requests.update(self.request_id, request_data)
|
Requests.update(self.request_id, request_data)
|
||||||
else:
|
else:
|
||||||
request = Requests.create(self.current_user.id, request_data)
|
request = Requests.create(self.current_user, request_data)
|
||||||
self.request_id = request.id
|
self.request_id = request.id
|
||||||
|
@ -3,6 +3,8 @@ from flask import g, redirect, render_template, url_for, request as http_request
|
|||||||
from . import requests_bp
|
from . import requests_bp
|
||||||
from atst.domain.requests import Requests
|
from atst.domain.requests import Requests
|
||||||
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
|
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
|
||||||
|
from atst.models.permissions import Permissions
|
||||||
|
from atst.domain.exceptions import UnauthorizedError
|
||||||
|
|
||||||
|
|
||||||
@requests_bp.route("/requests/new/<int:screen>", methods=["GET"])
|
@requests_bp.route("/requests/new/<int:screen>", methods=["GET"])
|
||||||
@ -25,6 +27,9 @@ def requests_form_new(screen):
|
|||||||
)
|
)
|
||||||
@requests_bp.route("/requests/new/<int:screen>/<string:request_id>", methods=["GET"])
|
@requests_bp.route("/requests/new/<int:screen>/<string:request_id>", methods=["GET"])
|
||||||
def requests_form_update(screen=1, request_id=None):
|
def requests_form_update(screen=1, request_id=None):
|
||||||
|
if request_id:
|
||||||
|
_check_can_view_request(request_id)
|
||||||
|
|
||||||
request = Requests.get(request_id) if request_id is not None else None
|
request = Requests.get(request_id) if request_id is not None else None
|
||||||
jedi_flow = JEDIRequestFlow(screen, request, request_id=request_id)
|
jedi_flow = JEDIRequestFlow(screen, request, request_id=request_id)
|
||||||
|
|
||||||
@ -79,10 +84,12 @@ def requests_update(screen=1, request_id=None):
|
|||||||
request_id=jedi_flow.request_id,
|
request_id=jedi_flow.request_id,
|
||||||
)
|
)
|
||||||
return redirect(where)
|
return redirect(where)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return render_template(
|
return render_template(
|
||||||
"requests/screen-%d.html" % int(screen), **rerender_args
|
"requests/screen-%d.html" % int(screen), **rerender_args
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return render_template("requests/screen-%d.html" % int(screen), **rerender_args)
|
return render_template("requests/screen-%d.html" % int(screen), **rerender_args)
|
||||||
|
|
||||||
@ -94,5 +101,18 @@ def requests_submit(request_id=None):
|
|||||||
|
|
||||||
if request.status == "approved":
|
if request.status == "approved":
|
||||||
return redirect("/requests?modal=True")
|
return redirect("/requests?modal=True")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return redirect("/requests")
|
return redirect("/requests")
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: generalize this, along with other authorizations, into a policy-pattern
|
||||||
|
# for authorization in the application
|
||||||
|
def _check_can_view_request(request_id):
|
||||||
|
if Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST in g.current_user.atat_permissions:
|
||||||
|
pass
|
||||||
|
elif Requests.exists(request_id, g.current_user):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise UnauthorizedError(g.current_user, "view request {}".format(request_id))
|
||||||
|
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
[default]
|
[default]
|
||||||
PORT=8000
|
|
||||||
ENVIRONMENT = dev
|
|
||||||
DEBUG = true
|
|
||||||
COOKIE_SECRET = some-secret-please-replace
|
|
||||||
SECRET = change_me_into_something_secret
|
|
||||||
SECRET_KEY = change_me_into_something_secret
|
|
||||||
CAC_URL = http://localhost:8000/login-redirect
|
CAC_URL = http://localhost:8000/login-redirect
|
||||||
|
CA_CHAIN = ssl/server-certs/ca-chain.pem
|
||||||
|
COOKIE_SECRET = some-secret-please-replace
|
||||||
|
CRL_DIRECTORY = crl
|
||||||
|
DEBUG = true
|
||||||
|
ENVIRONMENT = dev
|
||||||
|
PERMANENT_SESSION_LIFETIME = 600
|
||||||
PE_NUMBER_CSV_URL = http://c95e1ebb198426ee57b8-174bb05a294821bedbf46b6384fe9b1f.r31.cf5.rackcdn.com/penumbers.csv
|
PE_NUMBER_CSV_URL = http://c95e1ebb198426ee57b8-174bb05a294821bedbf46b6384fe9b1f.r31.cf5.rackcdn.com/penumbers.csv
|
||||||
REDIS_URI = redis://localhost:6379
|
|
||||||
PGAPPNAME = atst
|
PGAPPNAME = atst
|
||||||
|
PGDATABASE = atat
|
||||||
PGHOST = localhost
|
PGHOST = localhost
|
||||||
|
PGPASSWORD = postgres
|
||||||
PGPORT = 5432
|
PGPORT = 5432
|
||||||
PGUSER = postgres
|
PGUSER = postgres
|
||||||
PGPASSWORD = postgres
|
PORT=8000
|
||||||
PGDATABASE = atat
|
REDIS_URI = redis://localhost:6379
|
||||||
SESSION_TYPE = redis
|
SECRET = change_me_into_something_secret
|
||||||
|
SECRET_KEY = change_me_into_something_secret
|
||||||
SESSION_COOKIE_NAME=atat
|
SESSION_COOKIE_NAME=atat
|
||||||
|
SESSION_TYPE = redis
|
||||||
SESSION_USE_SIGNER = True
|
SESSION_USE_SIGNER = True
|
||||||
PERMANENT_SESSION_LIFETIME = 600
|
|
||||||
CRL_DIRECTORY = crl
|
|
||||||
CA_CHAIN = ssl/server-certs/ca-chain.pem
|
|
||||||
WTF_CSRF_ENABLED = true
|
WTF_CSRF_ENABLED = true
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
[default]
|
[default]
|
||||||
SESSION_COOKIE_SECURE=True
|
SESSION_COOKIE_SECURE=True
|
||||||
|
SESSION_COOKIE_DOMAIN=atat.codes
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.6.5-alpine
|
FROM alpine:3.8
|
||||||
|
|
||||||
### Very low chance of changing
|
### Very low chance of changing
|
||||||
###############################
|
###############################
|
||||||
@ -7,12 +7,12 @@ ARG APP_USER=atst
|
|||||||
ARG APP_GROUP=atat
|
ARG APP_GROUP=atat
|
||||||
ARG APP_DIR=/opt/atat/atst
|
ARG APP_DIR=/opt/atat/atst
|
||||||
ARG APP_PORT=8000
|
ARG APP_PORT=8000
|
||||||
ARG SITE_PACKAGES_DIR=/usr/local/lib/python3.6/site-packages
|
ARG LOCAL_BIN_DIR=/usr/bin
|
||||||
|
ARG SITE_PACKAGES_DIR=/usr/lib/python3.6/site-packages
|
||||||
|
|
||||||
ENV APP_USER "${APP_USER}"
|
ENV APP_USER "${APP_USER}"
|
||||||
ENV APP_GROUP "${APP_GROUP}"
|
ENV APP_GROUP "${APP_GROUP}"
|
||||||
ENV APP_DIR "${APP_DIR}"
|
ENV APP_DIR "${APP_DIR}"
|
||||||
ENV SKIP_PIPENV true
|
|
||||||
|
|
||||||
# Set port to open
|
# Set port to open
|
||||||
EXPOSE "${APP_PORT}"
|
EXPOSE "${APP_PORT}"
|
||||||
@ -21,13 +21,16 @@ EXPOSE "${APP_PORT}"
|
|||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
|
|
||||||
# Default command is to launch the server
|
# Default command is to launch the server
|
||||||
CMD ["bash", "-c", "${APP_DIR}/script/server"]
|
CMD ["bash", "-c", "${APP_DIR}/script/uwsgi_server"]
|
||||||
|
|
||||||
### Items that will change almost every build
|
### Items that will change almost every build
|
||||||
#############################################
|
#############################################
|
||||||
# Copy installed python packages from the tester image
|
# Copy installed python packages from the tester image
|
||||||
COPY --from=atst-tester:latest "${SITE_PACKAGES_DIR}" "${SITE_PACKAGES_DIR}"
|
COPY --from=atst-tester:latest "${SITE_PACKAGES_DIR}" "${SITE_PACKAGES_DIR}"
|
||||||
|
|
||||||
|
# Copy local bin directory (contains python system package wrappers)
|
||||||
|
COPY --from=atst-tester:latest "${LOCAL_BIN_DIR}" "${LOCAL_BIN_DIR}"
|
||||||
|
|
||||||
# Copy the app directory contents from the tester image (includes node modules)
|
# Copy the app directory contents from the tester image (includes node modules)
|
||||||
COPY --from=atst-tester:latest "${APP_DIR}" "${APP_DIR}"
|
COPY --from=atst-tester:latest "${APP_DIR}" "${APP_DIR}"
|
||||||
|
|
||||||
|
15
deploy/kubernetes/atst-configmap.yml
Normal file
15
deploy/kubernetes/atst-configmap.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: atst-config
|
||||||
|
namespace: atat
|
||||||
|
data:
|
||||||
|
uwsgi-config: |-
|
||||||
|
[uwsgi]
|
||||||
|
callable = app
|
||||||
|
module = app
|
||||||
|
socket = /var/run/uwsgi/uwsgi.socket
|
||||||
|
plugins = python3
|
||||||
|
virtualenv = /opt/atat/atst/.venv
|
||||||
|
chmod-socket = 666
|
10
deploy/kubernetes/atst-envvars-configmap.yml
Normal file
10
deploy/kubernetes/atst-envvars-configmap.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: atst-envvars
|
||||||
|
namespace: atat
|
||||||
|
data:
|
||||||
|
FLASK_ENV: dev
|
||||||
|
OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst/atst-overrides.ini
|
||||||
|
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi-config.ini
|
79
deploy/kubernetes/atst-nginx-configmap.yml
Normal file
79
deploy/kubernetes/atst-nginx-configmap.yml
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: atst-nginx
|
||||||
|
namespace: atat
|
||||||
|
data:
|
||||||
|
nginx-config: |-
|
||||||
|
server {
|
||||||
|
server_name www.atat.codes atat.codes;
|
||||||
|
listen 8442;
|
||||||
|
listen [::]:8442 ipv6only=on;
|
||||||
|
if ($http_x_forwarded_proto != 'https') {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
location /login-redirect {
|
||||||
|
return 301 https://auth.atat.codes$request_uri;
|
||||||
|
}
|
||||||
|
location /login-dev {
|
||||||
|
try_files $uri @appbasicauth;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
|
try_files $uri @app;
|
||||||
|
}
|
||||||
|
location @app {
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket;
|
||||||
|
}
|
||||||
|
location @appbasicauth {
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket;
|
||||||
|
auth_basic "Developer Access";
|
||||||
|
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
server_name auth.atat.codes;
|
||||||
|
listen 8443 ssl;
|
||||||
|
listen [::]:8443 ssl ipv6only=on;
|
||||||
|
# SSL server certificate and private key
|
||||||
|
ssl_certificate /etc/ssl/private/auth.atat.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/private/auth.atat.key;
|
||||||
|
# Set SSL protocols, ciphers, and related options
|
||||||
|
ssl_protocols TLSv1.3 TLSv1.2;
|
||||||
|
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ecdh_curve secp384r1;
|
||||||
|
ssl_dhparam /etc/ssl/dhparam.pem;
|
||||||
|
# SSL session options
|
||||||
|
ssl_session_timeout 4h;
|
||||||
|
ssl_session_cache shared:SSL:10m; # 1mb = ~4000 sessions
|
||||||
|
ssl_session_tickets off;
|
||||||
|
# OCSP Stapling
|
||||||
|
ssl_stapling on;
|
||||||
|
ssl_stapling_verify on;
|
||||||
|
resolver 8.8.8.8 8.8.4.4;
|
||||||
|
# Request and validate client certificate
|
||||||
|
#ssl_verify_client on;
|
||||||
|
#ssl_verify_depth 10;
|
||||||
|
#ssl_client_certificate /etc/nginx/ssl/ca/client-ca.pem;
|
||||||
|
# Guard against HTTPS -> HTTP downgrade
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always";
|
||||||
|
location / {
|
||||||
|
return 301 https://www.atat.codes$request_uri;
|
||||||
|
}
|
||||||
|
location /login-redirect {
|
||||||
|
try_files $uri @app;
|
||||||
|
}
|
||||||
|
location @app {
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket;
|
||||||
|
uwsgi_param HTTP_X_SSL_CLIENT_VERIFY $ssl_client_verify;
|
||||||
|
uwsgi_param HTTP_X_SSL_CLIENT_CERT $ssl_client_raw_cert;
|
||||||
|
uwsgi_param HTTP_X_SSL_CLIENT_S_DN $ssl_client_s_dn;
|
||||||
|
uwsgi_param HTTP_X_SSL_CLIENT_S_DN_LEGACY $ssl_client_s_dn_legacy;
|
||||||
|
uwsgi_param HTTP_X_SSL_CLIENT_I_DN $ssl_client_i_dn;
|
||||||
|
uwsgi_param HTTP_X_SSL_CLIENT_I_DN_LEGACY $ssl_client_i_dn_legacy;
|
||||||
|
}
|
||||||
|
}
|
165
deploy/kubernetes/atst.yml
Normal file
165
deploy/kubernetes/atst.yml
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: atat
|
||||||
|
---
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: atst
|
||||||
|
name: atst
|
||||||
|
namespace: atat
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: atst
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
fsGroup: 101
|
||||||
|
containers:
|
||||||
|
- name: atst
|
||||||
|
image: registry.atat.codes:443/atst-prod:e9b6f76
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: atst-envvars
|
||||||
|
volumeMounts:
|
||||||
|
- name: atst-config
|
||||||
|
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
||||||
|
subPath: atst-overrides.ini
|
||||||
|
- name: uwsgi-config
|
||||||
|
mountPath: "/opt/atat/atst/uwsgi-config.ini"
|
||||||
|
subPath: uwsgi-config.ini
|
||||||
|
- name: uwsgi-socket-dir
|
||||||
|
mountPath: "/var/run/uwsgi"
|
||||||
|
- name: atst-nginx
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- containerPort: 8442
|
||||||
|
name: http
|
||||||
|
- containerPort: 8443
|
||||||
|
name: https
|
||||||
|
volumeMounts:
|
||||||
|
- name: nginx-auth-tls
|
||||||
|
mountPath: "/etc/ssl/private"
|
||||||
|
- name: nginx-config
|
||||||
|
mountPath: "/etc/nginx/conf.d/atst.conf"
|
||||||
|
subPath: atst.conf
|
||||||
|
- name: nginx-dhparam
|
||||||
|
mountPath: "/etc/ssl/dhparam.pem"
|
||||||
|
subPath: dhparam.pem
|
||||||
|
- name: nginx-htpasswd
|
||||||
|
mountPath: "/etc/nginx/.htpasswd"
|
||||||
|
subPath: .htpasswd
|
||||||
|
- name: uwsgi-socket-dir
|
||||||
|
mountPath: "/var/run/uwsgi"
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: regcred
|
||||||
|
volumes:
|
||||||
|
- name: atst-config
|
||||||
|
secret:
|
||||||
|
secretName: atst-config-ini
|
||||||
|
items:
|
||||||
|
- key: atst-overrides.ini
|
||||||
|
path: atst-overrides.ini
|
||||||
|
mode: 0644
|
||||||
|
- name: nginx-auth-tls
|
||||||
|
secret:
|
||||||
|
secretName: auth-atst-ingress-tls
|
||||||
|
items:
|
||||||
|
- key: tls.crt
|
||||||
|
path: auth.atat.crt
|
||||||
|
mode: 0644
|
||||||
|
- key: tls.key
|
||||||
|
path: auth.atat.key
|
||||||
|
mode: 0640
|
||||||
|
- name: nginx-config
|
||||||
|
configMap:
|
||||||
|
name: atst-nginx
|
||||||
|
items:
|
||||||
|
- key: nginx-config
|
||||||
|
path: atst.conf
|
||||||
|
- name: nginx-dhparam
|
||||||
|
secret:
|
||||||
|
secretName: dhparam-4096
|
||||||
|
items:
|
||||||
|
- key: dhparam.pem
|
||||||
|
path: dhparam.pem
|
||||||
|
mode: 0640
|
||||||
|
- name: nginx-htpasswd
|
||||||
|
secret:
|
||||||
|
secretName: atst-nginx-htpasswd
|
||||||
|
items:
|
||||||
|
- key: htpasswd
|
||||||
|
path: .htpasswd
|
||||||
|
mode: 0640
|
||||||
|
- name: uwsgi-config
|
||||||
|
configMap:
|
||||||
|
name: atst-config
|
||||||
|
items:
|
||||||
|
- key: uwsgi-config
|
||||||
|
path: uwsgi-config.ini
|
||||||
|
mode: 0644
|
||||||
|
- name: uwsgi-socket-dir
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: atst
|
||||||
|
name: atst
|
||||||
|
namespace: atat
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 8442
|
||||||
|
selector:
|
||||||
|
app: atst
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: atst
|
||||||
|
name: atst-auth
|
||||||
|
namespace: atat
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
ports:
|
||||||
|
- name: https
|
||||||
|
protocol: TCP
|
||||||
|
nodePort: 32751
|
||||||
|
port: 8443
|
||||||
|
selector:
|
||||||
|
app: atst
|
||||||
|
---
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: atst
|
||||||
|
namespace: atat
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/tls-acme: "true"
|
||||||
|
kubernetes.io/ingress.class: "nginx"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: 10m
|
||||||
|
spec:
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- www.atat.codes
|
||||||
|
secretName: atst-ingress-tls
|
||||||
|
rules:
|
||||||
|
- host: www.atat.codes
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
backend:
|
||||||
|
serviceName: atst
|
||||||
|
servicePort: 80
|
4
deploy/kubernetes/set_atstconfig_secret.sh
Executable file
4
deploy/kubernetes/set_atstconfig_secret.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
kubectl -n atat delete secret atst-config-ini
|
||||||
|
kubectl -n atat create secret generic atst-config-ini --from-file="${1}"
|
4
deploy/kubernetes/set_dhparam_secret.sh
Executable file
4
deploy/kubernetes/set_dhparam_secret.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
kubectl -n atat delete secret dhparam-4096
|
||||||
|
kubectl -n atat create secret generic dhparam-4096 --from-file="${1}"
|
4
deploy/kubernetes/set_htpasswd_secret.sh
Executable file
4
deploy/kubernetes/set_htpasswd_secret.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
kubectl -n atat delete secret atst-nginx-htpasswd
|
||||||
|
kubectl -n atat create secret generic atst-nginx-htpasswd --from-file="${1}"
|
@ -9,5 +9,8 @@ source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
|||||||
APP_USER="atst"
|
APP_USER="atst"
|
||||||
APP_UID="8010"
|
APP_UID="8010"
|
||||||
|
|
||||||
|
# Add additional packages required by app dependencies
|
||||||
|
ADDITIONAL_PACKAGES="postgresql-libs python3 uwsgi uwsgi-python3"
|
||||||
|
|
||||||
# Run the shared alpine setup script
|
# Run the shared alpine setup script
|
||||||
source ./script/include/run_alpine_setup
|
source ./script/include/run_alpine_setup
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 8cf96c9776e7fd73c11d57160d26fc1715bf00da
|
Subproject commit c44ca5070da78fd522a2e485aaa225cc638e11d3
|
37
script/seed.py
Normal file
37
script/seed.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Add root project dir to the python path
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
sys.path.append(parent_dir)
|
||||||
|
|
||||||
|
from atst.app import make_config, make_app
|
||||||
|
from atst.domain.users import Users
|
||||||
|
from atst.domain.requests import Requests
|
||||||
|
from atst.domain.exceptions import AlreadyExistsError
|
||||||
|
from tests.factories import RequestFactory
|
||||||
|
from atst.routes.dev import _DEV_USERS as DEV_USERS
|
||||||
|
|
||||||
|
|
||||||
|
def seed_db():
|
||||||
|
users = []
|
||||||
|
for dev_user in DEV_USERS.values():
|
||||||
|
try:
|
||||||
|
user = Users.create(**dev_user)
|
||||||
|
users.append(user)
|
||||||
|
except AlreadyExistsError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
for dollar_value in [1, 200, 3000, 40000, 500000, 1000000]:
|
||||||
|
request = Requests.create(
|
||||||
|
user, RequestFactory.build_request_body(user, dollar_value)
|
||||||
|
)
|
||||||
|
Requests.submit(request)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
config = make_config()
|
||||||
|
app = make_app(config)
|
||||||
|
with app.app_context():
|
||||||
|
seed_db()
|
@ -5,9 +5,6 @@
|
|||||||
|
|
||||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||||
|
|
||||||
# Turn on sass compiler installation
|
|
||||||
INSTALL_SASS="true"
|
|
||||||
|
|
||||||
# Enable database resetting
|
# Enable database resetting
|
||||||
RESET_DB="true"
|
RESET_DB="true"
|
||||||
|
|
||||||
@ -16,3 +13,6 @@ source ./script/include/run_setup
|
|||||||
|
|
||||||
# Fetch and import the PE numbers
|
# Fetch and import the PE numbers
|
||||||
run_command "python script/ingest_pe_numbers.py"
|
run_command "python script/ingest_pe_numbers.py"
|
||||||
|
|
||||||
|
# Compile assets and generate hash-named static files
|
||||||
|
yarn build
|
||||||
|
@ -10,7 +10,7 @@ mkdir -p crl
|
|||||||
rsync -rq crl-tmp/. crl/.
|
rsync -rq crl-tmp/. crl/.
|
||||||
rm -rf crl-tmp
|
rm -rf crl-tmp
|
||||||
|
|
||||||
if [[ $FLASK_ENV != "production" ]]; then
|
if [[ $FLASK_ENV != "prod" ]]; then
|
||||||
# place our test CRL there
|
# place our test CRL there
|
||||||
cp ssl/client-certs/client-ca.der.crl crl/
|
cp ssl/client-certs/client-ca.der.crl crl/
|
||||||
fi
|
fi
|
||||||
|
@ -9,7 +9,7 @@ echo "Resetting CA bundle..."
|
|||||||
rm ssl/server-certs/ca-chain.pem &> /dev/null || true
|
rm ssl/server-certs/ca-chain.pem &> /dev/null || true
|
||||||
touch $CA_CHAIN
|
touch $CA_CHAIN
|
||||||
|
|
||||||
if [[ $FLASK_ENV != "production" ]]; then
|
if [[ $FLASK_ENV != "prod" ]]; then
|
||||||
# only for testing and development
|
# only for testing and development
|
||||||
echo "Copy in testing client CA..."
|
echo "Copy in testing client CA..."
|
||||||
cat ssl/client-certs/client-ca.crt >> $CA_CHAIN
|
cat ssl/client-certs/client-ca.crt >> $CA_CHAIN
|
||||||
|
11
script/uwsgi_server
Executable file
11
script/uwsgi_server
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# script/uwsgi_server: Launch the UWSGI server
|
||||||
|
|
||||||
|
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||||
|
|
||||||
|
# Before starting the server, apply any pending migrations to the DB
|
||||||
|
migrate_db
|
||||||
|
|
||||||
|
# Launch UWSGI
|
||||||
|
run_command "uwsgi --ini ${UWSGI_CONFIG_FULLPATH}"
|
@ -65,20 +65,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
padding: 0 0 $gap 0;
|
padding: 0 0 $gap/2 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@include h4;
|
@include h4;
|
||||||
@include line-max;
|
@include line-max;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.usa-input__help {
|
|
||||||
display: block;
|
|
||||||
@include h5;
|
|
||||||
font-weight: normal;
|
|
||||||
padding-top: $gap / 2;
|
|
||||||
@include line-max;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 100%;
|
left: 100%;
|
||||||
@ -88,6 +80,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-input__help {
|
||||||
|
display: block;
|
||||||
|
@include h4;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: $gap/2 0;
|
||||||
|
@include line-max;
|
||||||
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
@ -100,9 +100,14 @@
|
|||||||
padding: 0 0 $gap 0;
|
padding: 0 0 $gap 0;
|
||||||
@include h4;
|
@include h4;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: $font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
@ -20,6 +20,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
|
|
||||||
+ .subtitle * {
|
+ .subtitle * {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
color: $color-gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
{% macro OptionsInput(field, inline=False) -%}
|
{% macro OptionsInput(field, inline=False) -%}
|
||||||
<div class='usa-input {% if errors %}usa-input--error{% endif %}'>
|
<div class='usa-input {% if field.errors %}usa-input--error{% endif %}'>
|
||||||
|
|
||||||
<fieldset class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
<fieldset class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
||||||
<legend>
|
<legend>
|
||||||
{{ field.label }}
|
{{ field.label | striptags}}
|
||||||
|
|
||||||
{% if field.description %}
|
{% if field.description %}
|
||||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||||
@ -26,4 +26,7 @@
|
|||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if errors %}
|
{% if field.errors %}
|
||||||
{{ Icon('alert') }}
|
{{ Icon('alert') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -54,6 +54,7 @@
|
|||||||
placeholder='{{ placeholder }}'
|
placeholder='{{ placeholder }}'
|
||||||
{% if field.errors %}aria-invalid='true'{% endif %}>
|
{% if field.errors %}aria-invalid='true'{% endif %}>
|
||||||
</masked-input>
|
</masked-input>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<main class="usa-section usa-content">
|
<main class="usa-section usa-content">
|
||||||
|
|
||||||
<h1>Unauthorized</h1>
|
<h1>Not Found</h1>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -88,7 +88,7 @@
|
|||||||
{% for r in requests %}
|
{% for r in requests %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<a class='icon-link icon-link--large' href="{{ url_for('requests.requests_form_update', screen=1, request_id=r['order_id']) if r["status"] != "approved" else url_for('requests.financial_verification', request_id=r['order_id']) }}">{{ r['order_id'] }}</a>
|
<a class='icon-link icon-link--large' href="{{ r['edit_link'] }}">{{ r['order_id'] }}</a>
|
||||||
{% if r['is_new'] %}<span class="usa-label">New</span>
|
{% if r['is_new'] %}<span class="usa-label">New</span>
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends "../base.html.to" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -15,4 +15,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% end %}
|
{% endblock %}
|
@ -1,13 +1,21 @@
|
|||||||
<div class="progress-menu progress-menu--four">
|
<div class="progress-menu progress-menu--four">
|
||||||
<ul>
|
<ul>
|
||||||
{% for s in screens %}
|
{% for s in screens %}
|
||||||
<li class="progress-menu__item">
|
{% if loop.index < current %}
|
||||||
<a href="{{ url_for('requests.requests_form_update', screen=loop.index, request_id=request_id) if request_id else url_for('requests.requests_form_new', screen=loop.index) }}"
|
{% set step_indicator = 'complete' %}
|
||||||
{% if g.matchesPath('/requests/new/{{ loop.index + 1 }}') %}class="active"{% endif %}
|
{% elif loop.index == current %}
|
||||||
>
|
{% set step_indicator = 'active' %}
|
||||||
{{ s['title'] }}
|
{% else %}
|
||||||
</a>
|
{% set step_indicator = 'incomplete' %}
|
||||||
</li>
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="progress-menu__item progress-menu__item--{{ step_indicator }}">
|
||||||
|
<a href="{{ url_for('requests.requests_form_update', screen=loop.index, request_id=request_id) if request_id else url_for('requests.requests_form_new', screen=loop.index) }}"
|
||||||
|
{% if g.matchesPath('/requests/new/{{ loop.index + 1 }}') %}class="active"{% endif %}
|
||||||
|
>
|
||||||
|
{{ s['title'] }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
12
templates/unauthenticated.html
Normal file
12
templates/unauthenticated.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends "error_base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<main class="usa-section usa-content">
|
||||||
|
|
||||||
|
<h1>Log in Failed</h1>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -5,10 +5,10 @@ import alembic.command
|
|||||||
|
|
||||||
from atst.app import make_app, make_config
|
from atst.app import make_app, make_config
|
||||||
from atst.database import db as _db
|
from atst.database import db as _db
|
||||||
from .mocks import MOCK_USER
|
import tests.factories as factories
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope="session")
|
||||||
def app(request):
|
def app(request):
|
||||||
config = make_config()
|
config = make_config()
|
||||||
|
|
||||||
@ -27,11 +27,11 @@ def apply_migrations():
|
|||||||
alembic_config = os.path.join(os.path.dirname(__file__), "../", "alembic.ini")
|
alembic_config = os.path.join(os.path.dirname(__file__), "../", "alembic.ini")
|
||||||
config = alembic.config.Config(alembic_config)
|
config = alembic.config.Config(alembic_config)
|
||||||
app_config = make_config()
|
app_config = make_config()
|
||||||
config.set_main_option('sqlalchemy.url', app_config["DATABASE_URI"])
|
config.set_main_option("sqlalchemy.url", app_config["DATABASE_URI"])
|
||||||
alembic.command.upgrade(config, 'head')
|
alembic.command.upgrade(config, "head")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope="session")
|
||||||
def db(app, request):
|
def db(app, request):
|
||||||
|
|
||||||
_db.app = app
|
_db.app = app
|
||||||
@ -43,7 +43,7 @@ def db(app, request):
|
|||||||
_db.drop_all()
|
_db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function', autouse=True)
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
def session(db, request):
|
def session(db, request):
|
||||||
"""Creates a new database session for a test."""
|
"""Creates a new database session for a test."""
|
||||||
connection = db.engine.connect()
|
connection = db.engine.connect()
|
||||||
@ -54,6 +54,14 @@ def session(db, request):
|
|||||||
|
|
||||||
db.session = session
|
db.session = session
|
||||||
|
|
||||||
|
factory_list = [
|
||||||
|
cls
|
||||||
|
for _name, cls in factories.__dict__.items()
|
||||||
|
if isinstance(cls, type) and cls.__module__ == "tests.factories"
|
||||||
|
]
|
||||||
|
for factory in factory_list:
|
||||||
|
factory._meta.sqlalchemy_session = session
|
||||||
|
|
||||||
yield session
|
yield session
|
||||||
|
|
||||||
transaction.rollback()
|
transaction.rollback()
|
||||||
@ -81,10 +89,13 @@ def dummy_form():
|
|||||||
def dummy_field():
|
def dummy_field():
|
||||||
return DummyField()
|
return DummyField()
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def user_session(monkeypatch):
|
|
||||||
|
|
||||||
def set_user_session(user = MOCK_USER):
|
@pytest.fixture
|
||||||
monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: user)
|
def user_session(monkeypatch, session):
|
||||||
|
def set_user_session(user=None):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"atst.domain.auth.get_current_user",
|
||||||
|
lambda *args: user or factories.UserFactory.build(),
|
||||||
|
)
|
||||||
|
|
||||||
return set_user_session
|
return set_user_session
|
||||||
|
@ -6,20 +6,8 @@ from atst.domain.pe_numbers import PENumbers
|
|||||||
from tests.factories import PENumberFactory
|
from tests.factories import PENumberFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
def test_can_get_pe_number():
|
||||||
def new_pe_number(session):
|
new_pen = PENumberFactory.create(number="0701367F", description="Combat Support - Offensive")
|
||||||
def make_pe_number(**kwargs):
|
|
||||||
pen = PENumberFactory.create(**kwargs)
|
|
||||||
session.add(pen)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
return pen
|
|
||||||
|
|
||||||
return make_pe_number
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_get_pe_number(new_pe_number):
|
|
||||||
new_pen = new_pe_number(number="0701367F", description="Combat Support - Offensive")
|
|
||||||
pen = PENumbers.get(new_pen.number)
|
pen = PENumbers.get(new_pen.number)
|
||||||
|
|
||||||
assert pen.number == new_pen.number
|
assert pen.number == new_pen.number
|
||||||
|
@ -3,17 +3,14 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
from atst.domain.requests import Requests
|
from atst.domain.requests import Requests
|
||||||
|
from atst.models.request_status_event import RequestStatus
|
||||||
|
|
||||||
from tests.factories import RequestFactory
|
from tests.factories import RequestFactory, UserFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def new_request(session):
|
def new_request(session):
|
||||||
created_request = RequestFactory.create()
|
return RequestFactory.create()
|
||||||
session.add(created_request)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
return created_request
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_get_request(new_request):
|
def test_can_get_request(new_request):
|
||||||
@ -27,22 +24,42 @@ def test_nonexistent_request_raises():
|
|||||||
Requests.get(uuid4())
|
Requests.get(uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_request_has_started_status():
|
||||||
|
request = Requests.create(UserFactory.build(), {})
|
||||||
|
assert request.status == RequestStatus.STARTED
|
||||||
|
|
||||||
|
|
||||||
def test_auto_approve_less_than_1m(new_request):
|
def test_auto_approve_less_than_1m(new_request):
|
||||||
new_request.body = {"details_of_use": {"dollar_value": 999999}}
|
new_request.body = {"details_of_use": {"dollar_value": 999999}}
|
||||||
request = Requests.submit(new_request)
|
request = Requests.submit(new_request)
|
||||||
|
|
||||||
assert request.status == 'approved'
|
assert request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
|
||||||
|
|
||||||
|
|
||||||
def test_dont_auto_approve_if_dollar_value_is_1m_or_above(new_request):
|
def test_dont_auto_approve_if_dollar_value_is_1m_or_above(new_request):
|
||||||
new_request.body = {"details_of_use": {"dollar_value": 1000000}}
|
new_request.body = {"details_of_use": {"dollar_value": 1000000}}
|
||||||
request = Requests.submit(new_request)
|
request = Requests.submit(new_request)
|
||||||
|
|
||||||
assert request.status == 'submitted'
|
assert request.status == RequestStatus.PENDING_CCPO_APPROVAL
|
||||||
|
|
||||||
|
|
||||||
def test_dont_auto_approve_if_no_dollar_value_specified(new_request):
|
def test_dont_auto_approve_if_no_dollar_value_specified(new_request):
|
||||||
new_request.body = {"details_of_use": {}}
|
new_request.body = {"details_of_use": {}}
|
||||||
request = Requests.submit(new_request)
|
request = Requests.submit(new_request)
|
||||||
|
|
||||||
assert request.status == 'submitted'
|
assert request.status == RequestStatus.PENDING_CCPO_APPROVAL
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_allow_submission(new_request):
|
||||||
|
assert Requests.should_allow_submission(new_request)
|
||||||
|
|
||||||
|
del new_request.body['details_of_use']
|
||||||
|
assert not Requests.should_allow_submission(new_request)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exists(session):
|
||||||
|
user_allowed = UserFactory.create()
|
||||||
|
user_denied = UserFactory.create()
|
||||||
|
request = RequestFactory.create(creator=user_allowed)
|
||||||
|
assert Requests.exists(request.id, user_allowed)
|
||||||
|
assert not Requests.exists(request.id, user_denied)
|
||||||
|
@ -6,20 +6,8 @@ from atst.domain.task_orders import TaskOrders
|
|||||||
from tests.factories import TaskOrderFactory
|
from tests.factories import TaskOrderFactory
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
def test_can_get_task_order():
|
||||||
def new_task_order(session):
|
new_to = TaskOrderFactory.create(number="0101969F")
|
||||||
def make_task_order(**kwargs):
|
|
||||||
to = TaskOrderFactory.create(**kwargs)
|
|
||||||
session.add(to)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
return to
|
|
||||||
|
|
||||||
return make_task_order
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_get_task_order(new_task_order):
|
|
||||||
new_to = new_task_order(number="0101969F")
|
|
||||||
to = TaskOrders.get(new_to.number)
|
to = TaskOrders.get(new_to.number)
|
||||||
|
|
||||||
assert to.id == to.id
|
assert to.id == to.id
|
||||||
|
@ -1,40 +1,102 @@
|
|||||||
|
import random
|
||||||
|
import string
|
||||||
import factory
|
import factory
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from atst.models import Request
|
from atst.models.request import Request
|
||||||
|
from atst.models.request_status_event import RequestStatusEvent, RequestStatus
|
||||||
from atst.models.pe_number import PENumber
|
from atst.models.pe_number import PENumber
|
||||||
from atst.models.task_order import TaskOrder
|
from atst.models.task_order import TaskOrder
|
||||||
from atst.models.user import User
|
from atst.models.user import User
|
||||||
from atst.models.role import Role
|
from atst.models.role import Role
|
||||||
|
from atst.models.request_status_event import RequestStatusEvent
|
||||||
|
from atst.domain.roles import Roles
|
||||||
|
|
||||||
|
|
||||||
class RequestFactory(factory.Factory):
|
|
||||||
class Meta:
|
|
||||||
model = Request
|
|
||||||
|
|
||||||
id = factory.Sequence(lambda x: uuid4())
|
class RoleFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||||
|
|
||||||
class PENumberFactory(factory.Factory):
|
|
||||||
class Meta:
|
|
||||||
model = PENumber
|
|
||||||
|
|
||||||
class TaskOrderFactory(factory.Factory):
|
|
||||||
class Meta:
|
|
||||||
model = TaskOrder
|
|
||||||
|
|
||||||
class RoleFactory(factory.Factory):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
|
|
||||||
permissions = []
|
permissions = []
|
||||||
|
|
||||||
class UserFactory(factory.Factory):
|
|
||||||
|
class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
|
||||||
id = factory.Sequence(lambda x: uuid4())
|
id = factory.Sequence(lambda x: uuid4())
|
||||||
email = "fake.user@mail.com"
|
email = factory.Faker("email")
|
||||||
first_name = "Fake"
|
first_name = factory.Faker("first_name")
|
||||||
last_name = "User"
|
last_name = factory.Faker("last_name")
|
||||||
atat_role = factory.SubFactory(RoleFactory)
|
atat_role = factory.SubFactory(RoleFactory)
|
||||||
|
dod_id = factory.LazyFunction(lambda: "".join(random.choices(string.digits, k=10)))
|
||||||
|
|
||||||
|
|
||||||
|
class RequestStatusEventFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RequestStatusEvent
|
||||||
|
|
||||||
|
id = factory.Sequence(lambda x: uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
class RequestFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = Request
|
||||||
|
|
||||||
|
id = factory.Sequence(lambda x: uuid4())
|
||||||
|
status_events = factory.RelatedFactory(
|
||||||
|
RequestStatusEventFactory, "request", new_status=RequestStatus.STARTED
|
||||||
|
)
|
||||||
|
creator = factory.SubFactory(UserFactory)
|
||||||
|
body = factory.LazyAttribute(lambda r: RequestFactory.build_request_body(r.creator))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build_request_body(cls, user, dollar_value=1000000):
|
||||||
|
return {
|
||||||
|
"primary_poc": {
|
||||||
|
"dodid_poc": user.dod_id,
|
||||||
|
"email_poc": user.email,
|
||||||
|
"fname_poc": user.first_name,
|
||||||
|
"lname_poc": user.last_name
|
||||||
|
},
|
||||||
|
"details_of_use": {
|
||||||
|
"jedi_usage": "adf",
|
||||||
|
"start_date": "2018-08-08",
|
||||||
|
"cloud_native": "yes",
|
||||||
|
"dollar_value": dollar_value,
|
||||||
|
"dod_component": "us_navy",
|
||||||
|
"data_transfers": "less_than_100gb",
|
||||||
|
"jedi_migration": "yes",
|
||||||
|
"num_software_systems": 1,
|
||||||
|
"number_user_sessions": 2,
|
||||||
|
"average_daily_traffic": 1,
|
||||||
|
"engineering_assessment": "yes",
|
||||||
|
"technical_support_team": "yes",
|
||||||
|
"estimated_monthly_spend": 100,
|
||||||
|
"expected_completion_date": "less_than_1_month",
|
||||||
|
"rationalization_software_systems": "yes",
|
||||||
|
"organization_providing_assistance": "in_house_staff"
|
||||||
|
},
|
||||||
|
"information_about_you": {
|
||||||
|
"citizenship": "United States",
|
||||||
|
"designation": "military",
|
||||||
|
"phone_number": "1234567890",
|
||||||
|
"email_request": user.email,
|
||||||
|
"fname_request": user.first_name,
|
||||||
|
"lname_request": user.last_name,
|
||||||
|
"service_branch": "ads",
|
||||||
|
"date_latest_training": "2018-08-06"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = PENumber
|
||||||
|
|
||||||
|
|
||||||
|
class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = TaskOrder
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from tests.factories import RequestFactory, UserFactory
|
from tests.factories import RequestFactory, UserFactory
|
||||||
|
|
||||||
|
|
||||||
MOCK_USER = UserFactory.create()
|
MOCK_USER = UserFactory.build()
|
||||||
MOCK_REQUEST = RequestFactory.create(
|
MOCK_REQUEST = RequestFactory.build(
|
||||||
creator=MOCK_USER.id,
|
creator=MOCK_USER.id,
|
||||||
body={
|
body={
|
||||||
"financial_verification": {
|
"financial_verification": {
|
||||||
|
0
tests/models/__init__.py
Normal file
0
tests/models/__init__.py
Normal file
70
tests/models/test_requests.py
Normal file
70
tests/models/test_requests.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from tests.factories import RequestFactory, UserFactory
|
||||||
|
from atst.domain.requests import Requests, RequestStatus
|
||||||
|
|
||||||
|
|
||||||
|
def test_started_request_requires_mo_action():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
assert Requests.action_required_by(request) == "mission_owner"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pending_financial_requires_mo_action():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
request = Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION)
|
||||||
|
|
||||||
|
assert Requests.action_required_by(request) == "mission_owner"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pending_ccpo_approval_requires_ccpo():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL)
|
||||||
|
|
||||||
|
assert Requests.action_required_by(request) == "ccpo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_has_creator():
|
||||||
|
user = UserFactory.create()
|
||||||
|
request = RequestFactory.create(creator=user)
|
||||||
|
|
||||||
|
assert request.creator == user
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_status_started_displayname():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
request = Requests.set_status(request, RequestStatus.STARTED)
|
||||||
|
|
||||||
|
assert request.status_displayname == "Started"
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_status_pending_financial_displayname():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
request = Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION)
|
||||||
|
|
||||||
|
assert request.status_displayname == "Pending Financial Verification"
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_status_pending_ccpo_displayname():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL)
|
||||||
|
|
||||||
|
assert request.status_displayname == "Pending CCPO Approval"
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_status_pending_approved_displayname():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
request = Requests.set_status(request, RequestStatus.APPROVED)
|
||||||
|
|
||||||
|
assert request.status_displayname == "Approved"
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_status_pending_expired_displayname():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
request = Requests.set_status(request, RequestStatus.EXPIRED)
|
||||||
|
|
||||||
|
assert request.status_displayname == "Expired"
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_status_pending_deleted_displayname():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
request = Requests.set_status(request, RequestStatus.DELETED)
|
||||||
|
|
||||||
|
assert request.status_displayname == "Deleted"
|
@ -61,11 +61,9 @@ class TestPENumberInForm:
|
|||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
assert "/requests/financial_verification_submitted" in response.headers.get("Location")
|
assert "/requests/financial_verification_submitted" in response.headers.get("Location")
|
||||||
|
|
||||||
def test_submit_request_form_with_new_valid_pe_id(self, session, monkeypatch, client):
|
def test_submit_request_form_with_new_valid_pe_id(self, monkeypatch, client):
|
||||||
self._set_monkeypatches(monkeypatch)
|
self._set_monkeypatches(monkeypatch)
|
||||||
pe = PENumberFactory.create(number="8675309U", description="sample PE number")
|
pe = PENumberFactory.create(number="8675309U", description="sample PE number")
|
||||||
session.add(pe)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
data = dict(self.required_data)
|
data = dict(self.required_data)
|
||||||
data['pe_id'] = pe.number
|
data['pe_id'] = pe.number
|
||||||
@ -74,3 +72,14 @@ class TestPENumberInForm:
|
|||||||
|
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
assert "/requests/financial_verification_submitted" in response.headers.get("Location")
|
assert "/requests/financial_verification_submitted" in response.headers.get("Location")
|
||||||
|
|
||||||
|
def test_submit_request_form_with_missing_pe_id(self, monkeypatch, client):
|
||||||
|
self._set_monkeypatches(monkeypatch)
|
||||||
|
|
||||||
|
data = dict(self.required_data)
|
||||||
|
data['pe_id'] = ''
|
||||||
|
|
||||||
|
response = self.submit_data(client, data)
|
||||||
|
|
||||||
|
assert "There were some errors, see below" in response.data.decode()
|
||||||
|
assert response.status_code == 200
|
||||||
|
@ -2,7 +2,8 @@ import re
|
|||||||
import pytest
|
import pytest
|
||||||
import urllib
|
import urllib
|
||||||
from tests.mocks import MOCK_USER, MOCK_REQUEST
|
from tests.mocks import MOCK_USER, MOCK_REQUEST
|
||||||
from tests.factories import RequestFactory
|
from tests.factories import RequestFactory, UserFactory
|
||||||
|
from atst.domain.roles import Roles
|
||||||
|
|
||||||
|
|
||||||
ERROR_CLASS = "alert--error"
|
ERROR_CLASS = "alert--error"
|
||||||
@ -27,3 +28,41 @@ def test_submit_valid_request_form(monkeypatch, client, user_session):
|
|||||||
data="meaning=42",
|
data="meaning=42",
|
||||||
)
|
)
|
||||||
assert "/requests/new/2" in response.headers.get("Location")
|
assert "/requests/new/2" in response.headers.get("Location")
|
||||||
|
|
||||||
|
|
||||||
|
def test_owner_can_view_request(client, user_session):
|
||||||
|
user = UserFactory.create()
|
||||||
|
user_session(user)
|
||||||
|
request = RequestFactory.create(creator=user)
|
||||||
|
|
||||||
|
response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_owner_cannot_view_request(client, user_session):
|
||||||
|
user = UserFactory.create()
|
||||||
|
user_session(user)
|
||||||
|
request = RequestFactory.create()
|
||||||
|
|
||||||
|
response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_ccpo_can_view_request(client, user_session):
|
||||||
|
ccpo = Roles.get("ccpo")
|
||||||
|
user = UserFactory.create(atat_role=ccpo)
|
||||||
|
user_session(user)
|
||||||
|
request = RequestFactory.create()
|
||||||
|
|
||||||
|
response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonexistent_request(client, user_session):
|
||||||
|
user_session()
|
||||||
|
response = client.get("/requests/new/1/foo", follow_redirects=True)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
@ -27,8 +27,7 @@ def test_successful_login_redirect(client, monkeypatch):
|
|||||||
def test_unsuccessful_login_redirect(client, monkeypatch):
|
def test_unsuccessful_login_redirect(client, monkeypatch):
|
||||||
resp = client.get("/login-redirect")
|
resp = client.get("/login-redirect")
|
||||||
|
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 401
|
||||||
assert "unauthorized" in resp.headers["Location"]
|
|
||||||
assert "user_id" not in session
|
assert "user_id" not in session
|
||||||
|
|
||||||
|
|
||||||
@ -55,7 +54,6 @@ def test_routes_are_protected(client, app):
|
|||||||
|
|
||||||
|
|
||||||
UNPROTECTED_ROUTES = ["/", "/login-dev", "/login-redirect", "/unauthorized"]
|
UNPROTECTED_ROUTES = ["/", "/login-dev", "/login-redirect", "/unauthorized"]
|
||||||
|
|
||||||
# this implicitly relies on the test config and test CRL in tests/fixtures/crl
|
# this implicitly relies on the test config and test CRL in tests/fixtures/crl
|
||||||
|
|
||||||
|
|
||||||
@ -72,8 +70,7 @@ def test_crl_validation_on_login(client):
|
|||||||
"HTTP_X_SSL_CLIENT_CERT": bad_cert.decode(),
|
"HTTP_X_SSL_CLIENT_CERT": bad_cert.decode(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 401
|
||||||
assert "unauthorized" in resp.headers["Location"]
|
|
||||||
assert "user_id" not in session
|
assert "user_id" not in session
|
||||||
|
|
||||||
# good cert is not on the test CRL, passes
|
# good cert is not on the test CRL, passes
|
||||||
|
Loading…
x
Reference in New Issue
Block a user