Merge pull request #197 from dod-ccpo/workspaces2

Create workspaces and projects
This commit is contained in:
richard-dds 2018-08-22 15:10:20 -04:00 committed by GitHub
commit 63d2e222a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 788 additions and 219 deletions

56
Pipfile.lock generated
View File

@ -271,7 +271,7 @@
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622", "sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5" "sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
], ],
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7'", "markers": "python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==2018.5" "version": "==2018.5"
}, },
"redis": { "redis": {
@ -317,7 +317,7 @@
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
], ],
"markers": "python_version < '4' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.6' and python_version != '3.1.*'", "markers": "python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.0.*' and python_version < '4' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==1.23" "version": "==1.23"
}, },
"webassets": { "webassets": {
@ -350,14 +350,6 @@
], ],
"version": "==1.4.3" "version": "==1.4.3"
}, },
"appnope": {
"hashes": [
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
],
"markers": "sys_platform == 'darwin'",
"version": "==0.1.0"
},
"argh": { "argh": {
"hashes": [ "hashes": [
"sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3",
@ -449,6 +441,7 @@
"sha256:ea7cfd3aeb1544732d08bd9cfba40c5b78e3a91e17b1a0698ab81bfc5554c628", "sha256:ea7cfd3aeb1544732d08bd9cfba40c5b78e3a91e17b1a0698ab81bfc5554c628",
"sha256:f6d67f04abfb2b4bea7afc7fa6c18cf4c523a67956e455668be9ae42bccc21ad" "sha256:f6d67f04abfb2b4bea7afc7fa6c18cf4c523a67956e455668be9ae42bccc21ad"
], ],
"markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7'",
"version": "==0.9.0" "version": "==0.9.0"
}, },
"flask": { "flask": {
@ -501,7 +494,7 @@
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
], ],
"markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.0.*'", "markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*'",
"version": "==4.3.4" "version": "==4.3.4"
}, },
"itsdangerous": { "itsdangerous": {
@ -619,7 +612,7 @@
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
], ],
"markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.0.*'", "markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*'",
"version": "==0.7.1" "version": "==0.7.1"
}, },
"prompt-toolkit": { "prompt-toolkit": {
@ -642,7 +635,7 @@
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
], ],
"markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.0.*'", "markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*'",
"version": "==1.5.4" "version": "==1.5.4"
}, },
"pygments": { "pygments": {
@ -754,43 +747,6 @@
], ],
"version": "==4.3.2" "version": "==4.3.2"
}, },
"typed-ast": {
"hashes": [
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
"sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d",
"sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291",
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
"sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51",
"sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f",
"sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129",
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
],
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
"version": "==1.1.0"
},
"typing": {
"hashes": [
"sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf",
"sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8",
"sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2"
],
"version": "==3.6.4"
},
"watchdog": { "watchdog": {
"hashes": [ "hashes": [
"sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162" "sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162"

View File

@ -0,0 +1,37 @@
"""add workspaces table
Revision ID: 4be312655ceb
Revises: 05d6272bdb43
Create Date: 2018-08-16 09:25:19.888549
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '4be312655ceb'
down_revision = '05d6272bdb43'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('workspaces',
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('request_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('task_order_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['request_id'], ['requests.id'], ),
sa.ForeignKeyConstraint(['task_order_id'], ['task_order.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('workspaces')
# ### end Alembic commands ###

View File

@ -0,0 +1,30 @@
"""workspace timestamps
Revision ID: a2b499a1dd62
Revises: f549c7cee17c
Create Date: 2018-08-17 10:43:13.165829
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a2b499a1dd62'
down_revision = 'f549c7cee17c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('workspaces', sa.Column('time_created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False))
op.add_column('workspaces', sa.Column('time_updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('workspaces', 'time_updated')
op.drop_column('workspaces', 'time_created')
# ### end Alembic commands ###

View File

@ -0,0 +1,47 @@
"""projects and environments
Revision ID: f064247f2988
Revises: a2b499a1dd62
Create Date: 2018-08-17 11:30:53.684954
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'f064247f2988'
down_revision = 'a2b499a1dd62'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('projects',
sa.Column('time_created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('time_updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('description', sa.String(), nullable=False),
sa.Column('workspace_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('environments',
sa.Column('time_created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('time_updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('project_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('environments')
op.drop_table('projects')
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""add workspace_role workspace_id fk
Revision ID: f36f130622b9
Revises: f064247f2988
Create Date: 2018-08-20 10:36:23.920881
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f36f130622b9'
down_revision = 'f064247f2988'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_foreign_key('workspace_role_workspace_id_fk', 'workspace_role', 'workspaces', ['workspace_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('workspace_role_workspace_id_fk', 'workspace_role', type_='foreignkey')
# ### end Alembic commands ###

View File

@ -0,0 +1,30 @@
"""remove workspaces task order association
Revision ID: f549c7cee17c
Revises: 4be312655ceb
Create Date: 2018-08-16 16:42:48.581510
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f549c7cee17c'
down_revision = '4be312655ceb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('workspaces_task_order_id_fkey', 'workspaces', type_='foreignkey')
op.drop_column('workspaces', 'task_order_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('workspaces', sa.Column('task_order_id', sa.INTEGER(), autoincrement=False))
op.create_foreign_key('workspaces_task_order_id_fkey', 'workspaces', 'task_order', ['task_order_id'], ['id'])
# ### end Alembic commands ###

12
atst/domain/authz.py Normal file
View File

@ -0,0 +1,12 @@
from atst.domain.workspace_users import WorkspaceUsers
class Authorization(object):
@classmethod
def has_workspace_permission(cls, user, workspace, permission):
workspace_user = WorkspaceUsers.get(workspace.id, user.id)
return permission in workspace_user.permissions()
@classmethod
def is_in_workspace(cls, user, workspace):
return user in workspace.users

View File

@ -0,0 +1,12 @@
from atst.database import db
from atst.models.environment import Environment
class Environments(object):
@classmethod
def create(cls, project, name):
environment = Environment(project=project, name=name)
db.session.add(environment)
db.session.commit()
return environment

13
atst/domain/projects.py Normal file
View File

@ -0,0 +1,13 @@
from atst.database import db
from atst.models.project import Project
class Projects(object):
@classmethod
def create(cls, workspace, name, description):
project = Project(workspace=workspace, name=name, description=description)
db.session.add(project)
db.session.commit()
return project

View File

@ -6,6 +6,7 @@ from sqlalchemy.orm.attributes import flag_modified
from atst.models.request import Request from atst.models.request import Request
from atst.models.request_status_event import RequestStatusEvent, RequestStatus from atst.models.request_status_event import RequestStatusEvent, RequestStatus
from atst.domain.workspaces import Workspaces
from atst.database import db from atst.database import db
from .exceptions import NotFoundError from .exceptions import NotFoundError
@ -114,6 +115,18 @@ class Requests(object):
db.session.add(request) db.session.add(request)
db.session.commit() db.session.commit()
return request
@classmethod
def approve_and_create_workspace(cls, request):
approved_request = Requests.set_status(request, RequestStatus.APPROVED)
workspace = Workspaces.create(approved_request)
db.session.add(approved_request)
db.session.commit()
return workspace
@classmethod @classmethod
def set_status(cls, request: Request, status: RequestStatus): def set_status(cls, request: Request, status: RequestStatus):
status_event = RequestStatusEvent(new_status=status) status_event = RequestStatusEvent(new_status=status)

View File

@ -21,7 +21,8 @@ class WorkspaceUsers(object):
try: try:
workspace_role = ( workspace_role = (
WorkspaceRole.query.join(User) db.session.query(WorkspaceRole)
.join(User)
.filter(User.id == user_id, WorkspaceRole.workspace_id == workspace_id) .filter(User.id == user_id, WorkspaceRole.workspace_id == workspace_id)
.one() .one()
) )
@ -30,6 +31,29 @@ class WorkspaceUsers(object):
return WorkspaceUser(user, workspace_role) return WorkspaceUser(user, workspace_role)
@classmethod
def add(cls, user, workspace_id, role_name):
role = Roles.get(role_name)
try:
existing_workspace_role = (
db.session.query(WorkspaceRole)
.filter(
WorkspaceRole.user == user,
WorkspaceRole.workspace_id == workspace_id,
)
.one()
)
new_workspace_role = existing_workspace_role
new_workspace_role.role = role
except NoResultFound:
new_workspace_role = WorkspaceRole(
user=user, role_id=role.id, workspace_id=workspace_id
)
user.workspace_roles.append(new_workspace_role)
db.session.add(user)
db.session.commit()
@classmethod @classmethod
def add_many(cls, workspace_id, workspace_user_dicts): def add_many(cls, workspace_id, workspace_user_dicts):
workspace_users = [] workspace_users = []

View File

@ -1,112 +1,71 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models.workspace import Workspace
from atst.models.workspace_role import WorkspaceRole
from atst.domain.exceptions import NotFoundError, UnauthorizedError
from atst.domain.roles import Roles
from atst.domain.authz import Authorization
from atst.models.permissions import Permissions
class Workspaces(object): class Workspaces(object):
MOCK_WORKSPACES = [ @classmethod
{ def create(cls, request, name=None):
"name": "Unclassified IaaS and PaaS for Defense Digital Service (DDS)", name = name or request.id
"id": "5966187a-eff9-44c3-aa15-4de7a65ac7ff", workspace = Workspace(request=request, name=name)
"task_order": {"number": 123456}, Workspaces._create_workspace_role(request.creator, workspace, "owner")
"user_count": 23,
} db.session.add(workspace)
] db.session.commit()
return workspace
@classmethod @classmethod
def get(cls, workspace_id): def get(cls, user, workspace_id):
return cls.MOCK_WORKSPACES[0] try:
workspace = db.session.query(Workspace).filter_by(id=workspace_id).one()
except NoResultFound:
raise NotFoundError("workspace")
if not Authorization.is_in_workspace(user, workspace):
raise UnauthorizedError(user, "get workspace")
return workspace
@classmethod @classmethod
def get_many(cls): def get_for_update(cls, user, workspace_id):
return cls.MOCK_WORKSPACES workspace = Workspaces.get(user, workspace_id)
if not Authorization.has_workspace_permission(
class Projects(object): user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE
):
raise UnauthorizedError(user, "add project")
return workspace
@classmethod @classmethod
def create(cls, creator_id, body): def get_by_request(cls, request):
pass try:
workspace = db.session.query(Workspace).filter_by(request=request).one()
except NoResultFound:
raise NotFoundError("workspace")
return workspace
@classmethod @classmethod
def get(cls, project_id): def get_many(cls, user):
pass workspaces = (
db.session.query(Workspace)
.join(WorkspaceRole)
.filter(WorkspaceRole.user == user)
.all()
)
return workspaces
@classmethod @classmethod
def get_many(cls, workspace_id): def _create_workspace_role(cls, user, workspace, role_name):
return [ role = Roles.get(role_name)
{ workspace_role = WorkspaceRole(
"id": "187c9bea-9541-45d7-801f-cf8e7a642e93", user=user, role=role, workspace=workspace
"name": "Code.mil", )
"environments": [ db.session.add(workspace_role)
{ return workspace_role
"id": "b1154fdd-31c9-437f-b580-2e4d757de5cb",
"name": "Development",
},
{"id": "b1e2077a-6a3d-4e7f-a80c-bf1143433adf", "name": "Sandbox"},
{
"id": "8ea95eea-7cc0-4500-adf7-8a13eaa6b752",
"name": "production",
},
],
},
{
"id": "ececfd73-b19d-45aa-9199-a950ba2c7269",
"name": "Digital Dojo",
"environments": [
{
"id": "f56167cb-ca3d-4e29-8b60-91052957a118",
"name": "Development",
},
{
"id": "7c18689c-5b77-4b68-8d64-d4d8a830bf47",
"name": "production",
},
],
},
]
@classmethod
def update(cls, request_id, request_delta):
pass
class Members(object):
@classmethod
def create(cls, creator_id, body):
pass
@classmethod
def get(cls, request_id):
pass
@classmethod
def get_many(cls, workspace_id):
return [
{
"first_name": "Danny",
"last_name": "Knight",
"email": "dknight@thenavy.mil",
"dod_id": "1257892124",
"workspace_role": "Developer",
"status": "Pending",
"num_projects": "4",
},
{
"first_name": "Mario",
"last_name": "Hudson",
"email": "mhudson@thearmy.mil",
"dod_id": "4357892125",
"workspace_role": "CCPO",
"status": "Active",
"num_projects": "0",
},
{
"first_name": "Louise",
"last_name": "Greer",
"email": "lgreer@theairforce.mil",
"dod_id": "7257892125",
"workspace_role": "Admin",
"status": "Pending",
"num_projects": "43",
},
]
@classmethod
def update(cls, request_id, request_delta):
pass

View File

@ -0,0 +1,9 @@
from flask_wtf import Form
from wtforms.fields import StringField, TextAreaField
class NewProjectForm(Form):
name = StringField(label="Project Name")
description = TextAreaField(label="Description")
environment_name = StringField(label="Environment Name")

View File

@ -10,3 +10,6 @@ from .user import User
from .workspace_role import WorkspaceRole from .workspace_role import WorkspaceRole
from .pe_number import PENumber from .pe_number import PENumber
from .task_order import TaskOrder from .task_order import TaskOrder
from .workspace import Workspace
from .project import Project
from .environment import Environment

View File

@ -0,0 +1,16 @@
from sqlalchemy import Column, ForeignKey, String
from sqlalchemy.orm import relationship
from atst.models import Base
from atst.models.types import Id
from atst.models.mixins import TimestampsMixin
class Environment(Base, TimestampsMixin):
__tablename__ = "environments"
id = Id()
name = Column(String, nullable=False)
project_id = Column(ForeignKey("projects.id"))
project = relationship("Project")

7
atst/models/mixins.py Normal file
View File

@ -0,0 +1,7 @@
from sqlalchemy import Column, func, TIMESTAMP
class TimestampsMixin(object):
time_created = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
time_updated = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now(), onupdate=func.current_timestamp())

18
atst/models/project.py Normal file
View File

@ -0,0 +1,18 @@
from sqlalchemy import Column, ForeignKey, String
from sqlalchemy.orm import relationship
from atst.models import Base
from atst.models.types import Id
from atst.models.mixins import TimestampsMixin
class Project(Base, TimestampsMixin):
__tablename__ = "projects"
id = Id()
name = Column(String, nullable=False)
description = Column(String, nullable=False)
workspace_id = Column(ForeignKey("workspaces.id"), nullable=False)
workspace = relationship("Workspace")
environments = relationship("Environment", back_populates="project")

75
atst/models/workspace.py Normal file
View File

@ -0,0 +1,75 @@
from sqlalchemy import Column, ForeignKey, String
from sqlalchemy.orm import relationship
from atst.models import Base
from atst.models.types import Id
from atst.models.mixins import TimestampsMixin
MOCK_MEMBERS = [
{
"first_name": "Danny",
"last_name": "Knight",
"email": "dknight@thenavy.mil",
"dod_id": "1257892124",
"workspace_role": "Developer",
"status": "Pending",
"num_projects": "4",
},
{
"first_name": "Mario",
"last_name": "Hudson",
"email": "mhudson@thearmy.mil",
"dod_id": "4357892125",
"workspace_role": "CCPO",
"status": "Active",
"num_projects": "0",
},
{
"first_name": "Louise",
"last_name": "Greer",
"email": "lgreer@theairforce.mil",
"dod_id": "7257892125",
"workspace_role": "Admin",
"status": "Pending",
"num_projects": "43",
},
]
class Workspace(Base, TimestampsMixin):
__tablename__ = "workspaces"
id = Id()
name = Column(String, unique=True)
request_id = Column(ForeignKey("requests.id"), nullable=False)
request = relationship("Request")
projects = relationship("Project", back_populates="workspace")
roles = relationship("WorkspaceRole")
@property
def owner(self):
return next(
(
workspace_role.user
for workspace_role in self.roles
if workspace_role.role.name == "owner"
),
None,
)
@property
def users(self):
return set(role.user for role in self.roles)
@property
def user_count(self):
return len(self.users)
@property
def task_order(self):
return {"number": "task-order-number"}
@property
def members(self):
return MOCK_MEMBERS

View File

@ -10,11 +10,14 @@ class WorkspaceRole(Base):
__tablename__ = "workspace_role" __tablename__ = "workspace_role"
id = Id() id = Id()
workspace_id = Column(UUID(as_uuid=True), index=True) workspace_id = Column(UUID(as_uuid=True), ForeignKey("workspaces.id"), index=True)
workspace = relationship("Workspace", back_populates="roles")
role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id")) role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"))
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
role = relationship("Role") role = relationship("Role")
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
Index( Index(
"workspace_role_user_workspace", "workspace_role_user_workspace",

View File

@ -31,17 +31,19 @@ def update_financial_verification(request_id):
existing_request = Requests.get(request_id) existing_request = Requests.get(request_id)
form = financial_form(post_data) form = financial_form(post_data)
rerender_args = dict(request_id=request_id, f=form, extended=http_request.args.get("extended")) rerender_args = dict(
request_id=request_id, f=form, extended=http_request.args.get("extended")
)
if form.validate(): if form.validate():
request_data = {"financial_verification": form.data} request_data = {"financial_verification": form.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) updated_request = Requests.update(request_id, request_data)
if valid: if valid:
return redirect(url_for("requests.financial_verification_submitted")) new_workspace = Requests.approve_and_create_workspace(updated_request)
return redirect(url_for("workspaces.workspace_projects", workspace_id=new_workspace.id, newWorkspace=True))
else: else:
form.reset() form.reset()
return render_template( return render_template(

View File

@ -1,35 +1,76 @@
from flask import Blueprint, render_template, request as http_request from flask import (
Blueprint,
from atst.domain.workspaces import Members, Projects, Workspaces render_template,
request as http_request,
g,
redirect,
url_for,
)
from atst.domain.workspaces import Workspaces
from atst.domain.projects import Projects
from atst.domain.environments import Environments
from atst.forms.new_project import NewProjectForm
bp = Blueprint("workspaces", __name__) bp = Blueprint("workspaces", __name__)
@bp.context_processor @bp.context_processor
def workspace(): def workspace():
workspace = None workspace = None
if "workspace_id" in http_request.view_args: if "workspace_id" in http_request.view_args:
workspace = Workspaces.get(http_request.view_args["workspace_id"]) workspace = Workspaces.get(
return { "workspace": workspace } g.current_user, http_request.view_args["workspace_id"]
)
return {"workspace": workspace}
@bp.route("/workspaces") @bp.route("/workspaces")
def workspaces(): def workspaces():
return render_template("workspaces.html", page=5, workspaces=Workspaces.get_many()) workspaces = Workspaces.get_many(g.current_user)
return render_template("workspaces.html", page=5, workspaces=workspaces)
@bp.route("/workspaces/<workspace_id>/projects") @bp.route("/workspaces/<workspace_id>/projects")
def workspace_projects(workspace_id): def workspace_projects(workspace_id):
projects = Projects.get_many(workspace_id) workspace = Workspaces.get(g.current_user, workspace_id)
return render_template("workspace_projects.html", projects=projects) return render_template("workspace_projects.html", workspace=workspace)
@bp.route("/workspaces/<workspace_id>")
def show_workspace(workspace_id):
return redirect(url_for("workspaces.workspace_projects", workspace_id=workspace_id))
@bp.route("/workspaces/<workspace_id>/members") @bp.route("/workspaces/<workspace_id>/members")
def workspace_members(workspace_id): def workspace_members(workspace_id):
members = Members.get_many(workspace_id) workspace = Workspaces.get(g.current_user, workspace_id)
return render_template("workspace_members.html", members=members) return render_template("workspace_members.html", workspace=workspace)
@bp.route("/workspaces/<workspace_id>/reports") @bp.route("/workspaces/<workspace_id>/reports")
def workspace_reports(workspace_id): def workspace_reports(workspace_id):
return render_template("workspace_reports.html") return render_template("workspace_reports.html", workspace_id=workspace_id)
@bp.route("/workspaces/<workspace_id>/projects/new")
def new_project(workspace_id):
workspace = Workspaces.get_for_update(g.current_user, workspace_id)
form = NewProjectForm()
return render_template("workspace_project_new.html", workspace=workspace, form=form)
@bp.route("/workspaces/<workspace_id>/projects", methods=["POST"])
def update_project(workspace_id):
workspace = Workspaces.get_for_update(g.current_user, workspace_id)
form = NewProjectForm(http_request.form)
if form.validate():
project_data = form.data
project = Projects.create(
workspace, project_data["name"], project_data["description"]
)
Environments.create(project, project_data["environment_name"])
return redirect(
url_for("workspaces.workspace_projects", workspace_id=workspace.id)
)

View File

@ -0,0 +1,46 @@
import textinput from '../text_input'
export default {
name: 'new-project',
components: {
textinput
},
props: {
initialData: {
type: Object,
default: () => ({})
}
},
data: function () {
const {
name,
description,
environments = ['']
} = this.initialData
return {
name,
description,
environments,
}
},
mounted: function () {
this.$root.$on('onEnvironmentAdded', this.addEnvironment)
},
methods: {
addEnvironment: function (event) {
this.environments.push('')
},
removeEnvironment: function (index) {
if (this.environments.length > 1) {
this.environments.splice(index, 1)
}
}
}
}

View File

@ -8,6 +8,7 @@ import checkboxinput from './components/checkbox_input'
import DetailsOfUse from './components/forms/details_of_use' import DetailsOfUse from './components/forms/details_of_use'
import poc from './components/forms/poc' import poc from './components/forms/poc'
import financial from './components/forms/financial' import financial from './components/forms/financial'
import NewProject from './components/forms/new_project'
Vue.use(VTooltip) Vue.use(VTooltip)
@ -21,6 +22,7 @@ const app = new Vue({
DetailsOfUse, DetailsOfUse,
poc, poc,
financial, financial,
NewProject
}, },
methods: { methods: {
closeModal: function(name) { closeModal: function(name) {

View File

@ -8,6 +8,8 @@ sys.path.append(parent_dir)
from atst.app import make_config, make_app from atst.app import make_config, make_app
from atst.domain.users import Users from atst.domain.users import Users
from atst.domain.requests import Requests from atst.domain.requests import Requests
from atst.domain.workspaces import Workspaces
from atst.domain.projects import Projects
from atst.domain.exceptions import AlreadyExistsError from atst.domain.exceptions import AlreadyExistsError
from tests.factories import RequestFactory from tests.factories import RequestFactory
from atst.routes.dev import _DEV_USERS as DEV_USERS from atst.routes.dev import _DEV_USERS as DEV_USERS
@ -23,11 +25,20 @@ def seed_db():
pass pass
for user in users: for user in users:
requests = []
for dollar_value in [1, 200, 3000, 40000, 500000, 1000000]: for dollar_value in [1, 200, 3000, 40000, 500000, 1000000]:
request = Requests.create( request = Requests.create(
user, RequestFactory.build_request_body(user, dollar_value) user, RequestFactory.build_request_body(user, dollar_value)
) )
Requests.submit(request) Requests.submit(request)
requests.append(request)
workspace = Workspaces.create(requests[0], name="{}'s workspace".format(user.first_name))
Projects.create(
workspace=workspace,
name="First Project",
description="This is our first project."
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -15,11 +15,16 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
.icon-tooltip {
padding: 0.25rem 0.5rem;
}
} }
@mixin block-list__title { @mixin block-list__title {
@include h4; @include h4;
margin: 0; margin: 0;
line-height: 3rem;
} }
@mixin block-list__footer { @mixin block-list__footer {

View File

@ -22,6 +22,7 @@
background: none; background: none;
transition: background-color $hover-transition-time; transition: background-color $hover-transition-time;
border-radius: $gap / 2; border-radius: $gap / 2;
cursor: pointer;
.icon { .icon {
@include icon-color($color-primary); @include icon-color($color-primary);

View File

@ -49,4 +49,8 @@
&.icon--large { &.icon--large {
@include icon-size(24); @include icon-size(24);
} }
&.icon--remove {
@include icon-color($color-red);
}
} }

View File

@ -58,7 +58,7 @@
} }
.usa-input { .usa-input {
margin: ($gap * 4) ($gap * 2) ($gap * 4) 0; margin: ($gap * 4) ($gap * 2) ($gap * 4) 0;
@include media($medium-screen) { @include media($medium-screen) {
margin: ($gap * 4) 0; margin: ($gap * 4) 0;

View File

@ -63,13 +63,23 @@
.panel__heading { .panel__heading {
margin: $gap * 2; margin: $gap * 2;
@include media($medium-screen) { @include media($medium-screen) {
margin: $gap * 4; margin: $gap * 4;
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
margin: 0; margin: 0;
display: inline-block;
}
.icon-tooltip {
margin-left: $gap*2;
}
&--grow {
display: flex;
flex-direction: row;
justify-content: space-between;
} }
} }
} }

View File

@ -4,6 +4,10 @@
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
.usa-input {
margin: 0;
}
.project-list-item__environment__link { .project-list-item__environment__link {
@include icon-link; @include icon-link;
@include icon-link-large; @include icon-link-large;

View File

@ -9,8 +9,8 @@
subnav=[ subnav=[
{ {
"label": "Add New Project", "label": "Add New Project",
"href":"/", "href": url_for('workspaces.new_project', workspace_id=workspace.id),
"active": g.matchesPath('workspaces/projects/new'), "active": g.matchesPath('\/workspaces\/[A-Za-z0-9-]*\/projects'),
"icon": "plus" "icon": "plus"
} }
] ]

View File

@ -88,7 +88,7 @@
<footer class='block-list__footer'> <footer class='block-list__footer'>
<a href='/' class='icon-link'> <a href='/' class='icon-link'>
{% module Icon('plus') %} {% module Icon('plus') %}
<span>Add another environment</span> <span class="icon-link">Add another environment</span>
</a> </a>
</footer> </footer>
</section> </section>

View File

@ -4,7 +4,7 @@
{% block workspace_content %} {% block workspace_content %}
{% if not members %} {% if not workspace.members %}
{{ EmptyState( {{ EmptyState(
'There are currently no members in this Workspace.', 'There are currently no members in this Workspace.',
@ -58,7 +58,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for m in members %} {% for m in workspace.members %}
<tr> <tr>
<td><a href="/workspaces/123456/members/789/edit" class="icon-link icon-link--large">{{ m['first_name'] }} {{ m['last_name'] }}</a></td> <td><a href="/workspaces/123456/members/789/edit" class="icon-link icon-link--large">{{ m['first_name'] }} {{ m['last_name'] }}</a></td>
<td class='table-cell--shrink'>{% if m['num_projects'] == '0' %} <span class="label label--info">No Project Access</span> {% endif %}</td> <td class='table-cell--shrink'>{% if m['num_projects'] == '0' %} <span class="label label--info">No Project Access</span> {% endif %}</td>

View File

@ -0,0 +1,59 @@
{% from "components/icon.html" import Icon %}
{% from "components/text_input.html" import TextInput %}
{% from "components/tooltip.html" import Tooltip %}
{% extends "base_workspace.html" %}
{% block workspace_content %}
<new-project inline-template v-bind:initial-data='{{ form.data|tojson }}'>
<form method="POST" action="{{ url_for('workspaces.update_project', workspace_id=workspace.id) }}" >
{{ form.csrf_token }}
<div class="panel">
<div class="panel__heading panel__heading--grow">
<h1>Add a new project</h1>
{{ Tooltip(
"AT-AT allows you to organize your workspace into multiple projects, each of which may have environments.",
title="learn more"
)}}
</div>
<div class="panel__content">
{{ TextInput(form.name) }}
{{ TextInput(form.description, paragraph=True) }}
</div>
</div>
<div class="block-list project-list-item">
<header class="block-list__header">
<h2 class="block-list__title">Project Environments</h2>
{{ Tooltip(
"Each environment created within a project is an enclave of cloud resources that is logically separated from each other for increased security.",
title="learn more"
)}}
</header>
<ul>
<li v-for="(_, i) in environments" class="block-list__item">
{{ TextInput(form.environment_name) }}
<span class="icon-link icon-link--danger icon-link--vertical" v-on:click="removeEnvironment(i)">{{ Icon('x') }} Remove</span>
</li>
</ul>
<div class="block-list__footer">
<a v-on:click="addEnvironment" class="icon-link">Add another environment</a>
</div>
</div>
<div class="action-group">
<input type="submit" value="Create Project" class="usa-button usa-button-primary">
<a href="{{ url_for('workspaces.workspace_projects', workspace_id=workspace.id) }}" class="action-group__action">Cancel</a>
</div>
</div>
</form>
</new-project>
{% endblock %}

View File

@ -1,24 +1,34 @@
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/alert.html" import Alert %}
{% extends "base_workspace.html" %} {% extends "base_workspace.html" %}
{% block workspace_content %} {% block workspace_content %}
{% for project in projects %} {% if request.args.get("newWorkspace") %}
{{ Alert('Workspace created!',
message="\
<p>You are now ready to create projects and environments within the JEDI Cloud.</p>
",
level='success'
) }}
{% endif %}
{% for project in workspace.projects %}
<div class='block-list project-list-item'> <div class='block-list project-list-item'>
<header class='block-list__header'> <header class='block-list__header'>
<h2 class='block-list__title'>{{ project['name'] }} ({{ project['environments']|length }} environments)</h2> <h2 class='block-list__title'>{{ project.name }} ({{ project.environments|length }} environments)</h2>
<a class='icon-link' href=''> <a class='icon-link' href='/workspaces/123456/projects/789/edit'>
{{ Icon('edit') }} {{ Icon('edit') }}
<span>edit</span> <span>edit</span>
</a> </a>
</header> </header>
<ul> <ul>
{% for environment in project['environments'] %} {% for environment in project.environments %}
<li class='block-list__item project-list-item__environment'> <li class='block-list__item project-list-item__environment'>
<a href='/' target='_blank' rel='noopener noreferrer' class='project-list-item__environment__link'> <a href='/' target='_blank' rel='noopener noreferrer' class='project-list-item__environment__link'>
{{ Icon('link') }} {{ Icon('link') }}
<span>{{ environment["name"]}}</span> <span>{{ environment.name }}</span>
</a> </a>
<div class='project-list-item__environment__members'> <div class='project-list-item__environment__members'>

View File

@ -11,16 +11,16 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for w in workspaces %} {% for workspace in workspaces %}
<tr> <tr>
<td> <td>
<a class='icon-link icon-link--large' href="/workspaces/{{w['task_order']['number']}}/projects">{{ w['name'] }}</a><br> <a class='icon-link icon-link--large' href="/workspaces/{{ workspace.id }}/projects">{{ workspace.name }}</a><br>
</td> </td>
<td> <td>
#{{ w['task_order']['number'] }} #{{ workspace.task_order.number }}
</td> </td>
<td> <td>
<span class="label">{{ w['user_count'] }}</span><span class='h6'>Users</span> <span class="label">{{ workspace.user_count }}</span><span class='h6'>Users</span>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -1,12 +0,0 @@
{% extends "base.html.to" %}
{% block content %}
<div class="usa-width-one-whole empty-state">
<p>There are currently no JEDI workspaces</p>
<a href="" class="usa-button">New Workspace</a>
</div>
{% end %}

View File

@ -1,32 +1,30 @@
from uuid import uuid4
from atst.domain.workspace_users import WorkspaceUsers from atst.domain.workspace_users import WorkspaceUsers
from atst.domain.users import Users from atst.domain.users import Users
from tests.factories import WorkspaceFactory
def test_can_create_new_workspace_user(): def test_can_create_new_workspace_user():
workspace_id = uuid4() workspace = WorkspaceFactory.create()
user = Users.create("developer") new_user = Users.create("developer")
workspace_user_dicts = [{"id": user.id, "workspace_role": "owner"}] workspace_user_dicts = [{"id": new_user.id, "workspace_role": "owner"}]
workspace_users = WorkspaceUsers.add_many(workspace.id, workspace_user_dicts)
workspace_users = WorkspaceUsers.add_many(workspace_id, workspace_user_dicts) assert workspace_users[0].user.id == new_user.id
assert workspace_users[0].user.id == user.id
assert workspace_users[0].user.atat_role.name == "developer" assert workspace_users[0].user.atat_role.name == "developer"
assert workspace_users[0].workspace_role.role.name == "owner" assert workspace_users[0].workspace_role.role.name == "owner"
def test_can_update_existing_workspace_user(): def test_can_update_existing_workspace_user():
workspace_id = uuid4() workspace = WorkspaceFactory.create()
user = Users.create("developer") new_user = Users.create("developer")
WorkspaceUsers.add_many( WorkspaceUsers.add_many(
workspace_id, [{"id": user.id, "workspace_role": "owner"}] workspace.id, [{"id": new_user.id, "workspace_role": "owner"}]
) )
workspace_users = WorkspaceUsers.add_many( workspace_users = WorkspaceUsers.add_many(
workspace_id, [{"id": user.id, "workspace_role": "developer"}] workspace.id, [{"id": new_user.id, "workspace_role": "developer"}]
) )
assert workspace_users[0].user.id == user.id assert workspace_users[0].user.id == new_user.id
assert workspace_users[0].workspace_role.role.name == "developer" assert workspace_users[0].workspace_role.role.name == "developer"

View File

@ -0,0 +1,89 @@
import pytest
from uuid import uuid4
from atst.domain.exceptions import NotFoundError, UnauthorizedError
from atst.domain.workspaces import Workspaces
from atst.domain.workspace_users import WorkspaceUsers
from tests.factories import WorkspaceFactory, RequestFactory, UserFactory
def test_can_create_workspace():
request = RequestFactory.create()
workspace = Workspaces.create(request, name="frugal-whale")
assert workspace.name == "frugal-whale"
assert workspace.request == request
def test_default_workspace_name_is_request_id():
request = RequestFactory.create()
workspace = Workspaces.create(request)
assert workspace.name == str(request.id)
def test_get_nonexistent_workspace_raises():
with pytest.raises(NotFoundError):
Workspaces.get(UserFactory.build(), uuid4())
def test_can_get_workspace_by_request():
workspace = WorkspaceFactory.create()
found = Workspaces.get_by_request(workspace.request)
assert workspace == found
def test_creating_workspace_adds_owner():
user = UserFactory.create()
request = RequestFactory.create(creator=user)
workspace = Workspaces.create(request)
assert workspace.roles[0].user == user
def test_workspace_has_timestamps():
request = RequestFactory.create()
workspace = Workspaces.create(request)
assert workspace.time_created == workspace.time_updated
def test_workspaces_get_ensures_user_is_in_workspace():
owner = UserFactory.create()
outside_user = UserFactory.create()
workspace = Workspaces.create(RequestFactory.create(creator=owner))
workspace_ = Workspaces.get(owner, workspace.id)
assert workspace_ == workspace
with pytest.raises(UnauthorizedError):
Workspaces.get(outside_user, workspace.id)
def test_workspaces_get_many_with_no_workspaces():
workspaces = Workspaces.get_many(UserFactory.build())
assert workspaces == []
def test_workspaces_get_many_returns_a_users_workspaces():
user = UserFactory.create()
users_workspace = Workspaces.create(RequestFactory.create(creator=user))
# random workspace
Workspaces.create(RequestFactory.create())
assert Workspaces.get_many(user) == [users_workspace]
def test_get_for_update_allows_owner():
owner = UserFactory.create()
workspace = Workspaces.create(RequestFactory.create(creator=owner))
Workspaces.get_for_update(owner, workspace.id)
def test_get_for_update_blocks_developer():
owner = UserFactory.create()
developer = UserFactory.create()
workspace = Workspaces.create(RequestFactory.create(creator=owner))
WorkspaceUsers.add(developer, workspace.id, "developer")
with pytest.raises(UnauthorizedError):
Workspaces.get_for_update(developer, workspace.id)

View File

@ -10,6 +10,7 @@ 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.workspace import Workspace
from atst.models.request_status_event import RequestStatusEvent from atst.models.request_status_event import RequestStatusEvent
from atst.domain.roles import Roles from atst.domain.roles import Roles
@ -102,3 +103,12 @@ class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory):
class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory): class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta: class Meta:
model = TaskOrder model = TaskOrder
class WorkspaceFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Workspace
request = factory.SubFactory(RequestFactory)
# name it the same as the request ID by default
name = factory.LazyAttribute(lambda w: w.request.id)

View File

@ -1,12 +1,10 @@
import re
import pytest
import urllib import urllib
from flask import url_for from flask import url_for
from atst.eda_client import MockEDAClient from atst.eda_client import MockEDAClient
from tests.mocks import MOCK_REQUEST, MOCK_USER from tests.mocks import MOCK_REQUEST, MOCK_USER
from tests.factories import PENumberFactory from tests.factories import PENumberFactory, RequestFactory
class TestPENumberInForm: class TestPENumberInForm:
@ -39,14 +37,13 @@ class TestPENumberInForm:
def _set_monkeypatches(self, monkeypatch): def _set_monkeypatches(self, monkeypatch):
monkeypatch.setattr("atst.forms.financial.FinancialForm.validate", lambda s: True) monkeypatch.setattr("atst.forms.financial.FinancialForm.validate", lambda s: True)
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST)
monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: MOCK_USER) monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: MOCK_USER)
def submit_data(self, client, data, extended=False): def submit_data(self, client, data, extended=False):
url_kwargs = {"request_id": MOCK_REQUEST.id} request = RequestFactory.create(body=MOCK_REQUEST.body)
url_kwargs = {"request_id": request.id}
if extended: if extended:
url_kwargs["extended"] = True url_kwargs["extended"] = True
response = client.post( response = client.post(
url_for("requests.financial_verification", **url_kwargs), url_for("requests.financial_verification", **url_kwargs),
headers={"Content-Type": "application/x-www-form-urlencoded"}, headers={"Content-Type": "application/x-www-form-urlencoded"},
@ -72,7 +69,7 @@ class TestPENumberInForm:
response = self.submit_data(client, data) response = self.submit_data(client, data)
assert response.status_code == 302 assert response.status_code == 302
assert "/requests/financial_verification_submitted" in response.headers.get("Location") assert "/workspaces" in response.headers.get("Location")
def test_submit_request_form_with_new_valid_pe_id(self, monkeypatch, client): def test_submit_request_form_with_new_valid_pe_id(self, monkeypatch, client):
self._set_monkeypatches(monkeypatch) self._set_monkeypatches(monkeypatch)
@ -84,7 +81,7 @@ class TestPENumberInForm:
response = self.submit_data(client, data) response = self.submit_data(client, data)
assert response.status_code == 302 assert response.status_code == 302
assert "/requests/financial_verification_submitted" in response.headers.get("Location") assert "/workspaces" in response.headers.get("Location")
def test_submit_request_form_with_missing_pe_id(self, monkeypatch, client): def test_submit_request_form_with_missing_pe_id(self, monkeypatch, client):
self._set_monkeypatches(monkeypatch) self._set_monkeypatches(monkeypatch)
@ -132,4 +129,4 @@ class TestPENumberInForm:
response = self.submit_data(client, data, extended=True) response = self.submit_data(client, data, extended=True)
assert response.status_code == 302 assert response.status_code == 302
assert "/requests/financial_verification_submitted" in response.headers.get("Location") assert "/workspaces" in response.headers.get("Location")