From ef153f5226faf084aac8351b6345d6b8990b4a88 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 16 Aug 2018 09:51:35 -0400 Subject: [PATCH 01/45] basic workspace model and repository implementation --- .../4be312655ceb_add_workspaces_table.py | 37 +++++++++++++++ atst/domain/workspaces.py | 46 ++++++++++++++----- atst/models/__init__.py | 1 + atst/models/workspace.py | 19 ++++++++ tests/domain/test_workspaces.py | 36 +++++++++++++++ tests/factories.py | 11 +++++ 6 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 alembic/versions/4be312655ceb_add_workspaces_table.py create mode 100644 atst/models/workspace.py create mode 100644 tests/domain/test_workspaces.py diff --git a/alembic/versions/4be312655ceb_add_workspaces_table.py b/alembic/versions/4be312655ceb_add_workspaces_table.py new file mode 100644 index 00000000..6d0ba7d9 --- /dev/null +++ b/alembic/versions/4be312655ceb_add_workspaces_table.py @@ -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 ### diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 64d7ce50..f062431e 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -1,23 +1,44 @@ +from sqlalchemy.orm.exc import NoResultFound + +from atst.database import db +from atst.domain.exceptions import NotFoundError +from atst.models.workspace import Workspace + + class Workspaces(object): - MOCK_WORKSPACES = [ - { - "name": "Unclassified IaaS and PaaS for Defense Digital Service (DDS)", - "id": "5966187a-eff9-44c3-aa15-4de7a65ac7ff", - "task_order": {"number": 123456}, - "user_count": 23, - } - ] + # will a request have a TO association? + # do we automatically create an entry for the request.creator in the + # workspace_roles table? + + @classmethod + def create(cls, request, task_order, name=None): + name = name or request.id + return Workspace(request=request, task_order=task_order, name=name) @classmethod def get(cls, workspace_id): - return cls.MOCK_WORKSPACES[0] + try: + workspace = db.session.query(Workspace).filter_by(id=workspace_id).one() + except NoResultFound: + raise NotFoundError("workspace") + + return workspace @classmethod - def get_many(cls): - return cls.MOCK_WORKSPACES + def get_by_request(cls, request): + try: + workspace = db.session.query(Workspace).filter_by(request=request).one() + except NoResultFound: + raise NotFoundError("workspace") + + return workspace + class Projects(object): + def __init__(self): + pass + @classmethod def create(cls, creator_id, body): pass @@ -67,6 +88,9 @@ class Projects(object): class Members(object): + def __init__(self): + pass + @classmethod def create(cls, creator_id, body): pass diff --git a/atst/models/__init__.py b/atst/models/__init__.py index 1d6daae1..129064eb 100644 --- a/atst/models/__init__.py +++ b/atst/models/__init__.py @@ -10,3 +10,4 @@ from .user import User from .workspace_role import WorkspaceRole from .pe_number import PENumber from .task_order import TaskOrder +from .workspace import Workspace diff --git a/atst/models/workspace.py b/atst/models/workspace.py new file mode 100644 index 00000000..4fd11951 --- /dev/null +++ b/atst/models/workspace.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, ForeignKey, String +from sqlalchemy.orm import relationship + +from atst.models import Base +from atst.models.types import Id + + +class Workspace(Base): + __tablename__ = "workspaces" + + id = Id() + + request_id = Column(ForeignKey("requests.id"), nullable=False) + request = relationship("Request") + + task_order_id = Column(ForeignKey("task_order.id"), nullable=False) + task_order = relationship("TaskOrder") + + name = Column(String, unique=True) diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py new file mode 100644 index 00000000..48940d07 --- /dev/null +++ b/tests/domain/test_workspaces.py @@ -0,0 +1,36 @@ +import pytest +from uuid import uuid4 + +from atst.domain.exceptions import NotFoundError +from atst.domain.workspaces import Workspaces + +from tests.factories import WorkspaceFactory, RequestFactory, TaskOrderFactory + + +def test_can_create_workspace(): + request = RequestFactory.create() + to = TaskOrderFactory.create() + workspace = Workspaces.create(request, to) + assert workspace.request == request + assert workspace.task_order == to + assert workspace.name == request.id + + workspace = Workspaces.create(request, to, name="frugal-whale") + assert workspace.name == "frugal-whale" + + +def test_can_get_workspace(): + workspace = WorkspaceFactory.create() + found = Workspaces.get(workspace.id) + assert workspace == found + + +def test_nonexistent_workspace_raises(): + with pytest.raises(NotFoundError): + Workspaces.get(uuid4()) + + +def test_can_get_workspace_by_request(): + workspace = WorkspaceFactory.create() + found = Workspaces.get_by_request(workspace.request) + assert workspace == found diff --git a/tests/factories.py b/tests/factories.py index a6370060..1691ec61 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -10,6 +10,7 @@ from atst.models.pe_number import PENumber from atst.models.task_order import TaskOrder from atst.models.user import User from atst.models.role import Role +from atst.models.workspace import Workspace from atst.models.request_status_event import RequestStatusEvent from atst.domain.roles import Roles @@ -102,3 +103,13 @@ class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory): class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = TaskOrder + + +class WorkspaceFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = Workspace + + request = factory.SubFactory(RequestFactory) + task_order = factory.SubFactory(TaskOrderFactory) + # name it the same as the request ID by default + name = factory.LazyAttribute(lambda w: w.request.id) From 75f41d4d2ba2cb95f01b519efcef169553659309 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 16 Aug 2018 16:46:00 -0400 Subject: [PATCH 02/45] remove workspaces task order association for now --- ...emove_workspaces_task_order_association.py | 30 +++++++++++++++++++ atst/domain/workspaces.py | 4 +-- atst/models/workspace.py | 3 -- tests/domain/test_workspaces.py | 6 ++-- tests/factories.py | 1 - 5 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 alembic/versions/f549c7cee17c_remove_workspaces_task_order_association.py diff --git a/alembic/versions/f549c7cee17c_remove_workspaces_task_order_association.py b/alembic/versions/f549c7cee17c_remove_workspaces_task_order_association.py new file mode 100644 index 00000000..7b57cfb9 --- /dev/null +++ b/alembic/versions/f549c7cee17c_remove_workspaces_task_order_association.py @@ -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, nullable=False)) + op.create_foreign_key('workspaces_task_order_id_fkey', 'workspaces', 'task_order', ['task_order_id'], ['id']) + # ### end Alembic commands ### diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index f062431e..7e675ec1 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -11,9 +11,9 @@ class Workspaces(object): # workspace_roles table? @classmethod - def create(cls, request, task_order, name=None): + def create(cls, request, name=None): name = name or request.id - return Workspace(request=request, task_order=task_order, name=name) + return Workspace(request=request, name=name) @classmethod def get(cls, workspace_id): diff --git a/atst/models/workspace.py b/atst/models/workspace.py index 4fd11951..56c62188 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -13,7 +13,4 @@ class Workspace(Base): request_id = Column(ForeignKey("requests.id"), nullable=False) request = relationship("Request") - task_order_id = Column(ForeignKey("task_order.id"), nullable=False) - task_order = relationship("TaskOrder") - name = Column(String, unique=True) diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index 48940d07..3e44848b 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -9,13 +9,11 @@ from tests.factories import WorkspaceFactory, RequestFactory, TaskOrderFactory def test_can_create_workspace(): request = RequestFactory.create() - to = TaskOrderFactory.create() - workspace = Workspaces.create(request, to) + workspace = Workspaces.create(request) assert workspace.request == request - assert workspace.task_order == to assert workspace.name == request.id - workspace = Workspaces.create(request, to, name="frugal-whale") + workspace = Workspaces.create(request, name="frugal-whale") assert workspace.name == "frugal-whale" diff --git a/tests/factories.py b/tests/factories.py index 1691ec61..51be428b 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -110,6 +110,5 @@ class WorkspaceFactory(factory.alchemy.SQLAlchemyModelFactory): model = Workspace request = factory.SubFactory(RequestFactory) - task_order = factory.SubFactory(TaskOrderFactory) # name it the same as the request ID by default name = factory.LazyAttribute(lambda w: w.request.id) From c723c9b326dc3d0e4b33cc1d5b55adeaf22e32db Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 16 Aug 2018 17:03:10 -0400 Subject: [PATCH 03/45] WIP: creating a workspace creates a workspace_role --- atst/domain/workspace_users.py | 3 ++- atst/domain/workspaces.py | 7 ++++++- atst/models/workspace.py | 4 ++++ tests/domain/test_workspaces.py | 12 +++++++++++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/atst/domain/workspace_users.py b/atst/domain/workspace_users.py index 7e217c38..a67c0cbb 100644 --- a/atst/domain/workspace_users.py +++ b/atst/domain/workspace_users.py @@ -21,7 +21,8 @@ class WorkspaceUsers(object): try: workspace_role = ( - WorkspaceRole.query.join(User) + db.session.query(WorkspaceRole) + .join(User) .filter(User.id == user_id, WorkspaceRole.workspace_id == workspace_id) .one() ) diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 7e675ec1..4ba9c591 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -3,6 +3,8 @@ from sqlalchemy.orm.exc import NoResultFound from atst.database import db from atst.domain.exceptions import NotFoundError from atst.models.workspace import Workspace +from atst.models.workspace_role import WorkspaceRole +from atst.domain.roles import Roles class Workspaces(object): @@ -13,7 +15,10 @@ class Workspaces(object): @classmethod def create(cls, request, name=None): name = name or request.id - return Workspace(request=request, name=name) + workspace = Workspace(request=request, name=name) + role = Roles.get("owner") + wr = WorkspaceRole(user_id=request.creator.id, role=role, workspace_id=workspace.id) + return workspace @classmethod def get(cls, workspace_id): diff --git a/atst/models/workspace.py b/atst/models/workspace.py index 56c62188..03370c34 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -14,3 +14,7 @@ class Workspace(Base): request = relationship("Request") name = Column(String, unique=True) + + @property + def owner(self): + return self.request.creator diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index 3e44848b..fc52a719 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -3,8 +3,9 @@ from uuid import uuid4 from atst.domain.exceptions import NotFoundError from atst.domain.workspaces import Workspaces +from atst.domain.workspace_users import WorkspaceUsers -from tests.factories import WorkspaceFactory, RequestFactory, TaskOrderFactory +from tests.factories import WorkspaceFactory, RequestFactory, UserFactory def test_can_create_workspace(): @@ -32,3 +33,12 @@ 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) + workspace_user = WorkspaceUsers.get(workspace.id, user.id) + assert workspace_user.workspace_role + From 01778ada05c4dfe265730fb7f060350234dcaa0e Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 17 Aug 2018 11:02:59 -0400 Subject: [PATCH 04/45] Add timestamps to workspace --- .../a2b499a1dd62_workspace_timestamps.py | 30 +++++++++++++++++++ atst/models/mixins.py | 7 +++++ atst/models/workspace.py | 3 +- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/a2b499a1dd62_workspace_timestamps.py create mode 100644 atst/models/mixins.py diff --git a/alembic/versions/a2b499a1dd62_workspace_timestamps.py b/alembic/versions/a2b499a1dd62_workspace_timestamps.py new file mode 100644 index 00000000..f7c3e05a --- /dev/null +++ b/alembic/versions/a2b499a1dd62_workspace_timestamps.py @@ -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 ### diff --git a/atst/models/mixins.py b/atst/models/mixins.py new file mode 100644 index 00000000..467ee172 --- /dev/null +++ b/atst/models/mixins.py @@ -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()) + diff --git a/atst/models/workspace.py b/atst/models/workspace.py index 03370c34..b0051093 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -3,9 +3,10 @@ from sqlalchemy.orm import relationship from atst.models import Base from atst.models.types import Id +from atst.models.mixins import TimestampsMixin -class Workspace(Base): +class Workspace(Base, TimestampsMixin): __tablename__ = "workspaces" id = Id() From 8d58b2a7a051b422bdcf619892d95f7be22be182 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 17 Aug 2018 11:35:21 -0400 Subject: [PATCH 05/45] WIP: created Project and Environment models --- .../f064247f2988_projects_and_environments.py | 47 +++++++++++++++++++ atst/models/__init__.py | 2 + atst/models/environment.py | 16 +++++++ atst/models/project.py | 18 +++++++ atst/models/workspace.py | 4 +- tests/domain/test_workspaces.py | 7 +++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 alembic/versions/f064247f2988_projects_and_environments.py create mode 100644 atst/models/environment.py create mode 100644 atst/models/project.py diff --git a/alembic/versions/f064247f2988_projects_and_environments.py b/alembic/versions/f064247f2988_projects_and_environments.py new file mode 100644 index 00000000..5efa590b --- /dev/null +++ b/alembic/versions/f064247f2988_projects_and_environments.py @@ -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 ### diff --git a/atst/models/__init__.py b/atst/models/__init__.py index 129064eb..9accc290 100644 --- a/atst/models/__init__.py +++ b/atst/models/__init__.py @@ -11,3 +11,5 @@ from .workspace_role import WorkspaceRole from .pe_number import PENumber from .task_order import TaskOrder from .workspace import Workspace +from .project import Project +from .environment import Environment diff --git a/atst/models/environment.py b/atst/models/environment.py new file mode 100644 index 00000000..8e812635 --- /dev/null +++ b/atst/models/environment.py @@ -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") diff --git a/atst/models/project.py b/atst/models/project.py new file mode 100644 index 00000000..05ee853e --- /dev/null +++ b/atst/models/project.py @@ -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") + projects = relationship("Environment", back_populates="project") diff --git a/atst/models/workspace.py b/atst/models/workspace.py index b0051093..31476254 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -10,11 +10,11 @@ class Workspace(Base, TimestampsMixin): __tablename__ = "workspaces" id = Id() + name = Column(String, unique=True) request_id = Column(ForeignKey("requests.id"), nullable=False) request = relationship("Request") - - name = Column(String, unique=True) + projects = relationship("Project", back_populates="workspace") @property def owner(self): diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index fc52a719..d72be57e 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -42,3 +42,10 @@ def test_creating_workspace_adds_owner(): workspace_user = WorkspaceUsers.get(workspace.id, user.id) assert workspace_user.workspace_role + +def test_workspace_has_timestamps(): + request = RequestFactory.create() + workspace = Workspaces.create(request) + assert workspace.request == request + assert workspace.name == request.id + assert workspace.time_created == workspace.time_updated From eea769e63793591db3bbe34c68c2e2e6f2275f6c Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 17 Aug 2018 11:41:33 -0400 Subject: [PATCH 06/45] Add workspace and workspace role to db --- atst/domain/workspaces.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 4ba9c591..20b77794 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -16,8 +16,12 @@ class Workspaces(object): def create(cls, request, name=None): name = name or request.id workspace = Workspace(request=request, name=name) + role = Roles.get("owner") - wr = WorkspaceRole(user_id=request.creator.id, role=role, workspace_id=workspace.id) + workspace_role = WorkspaceRole(user_id=request.creator.id, role=role, workspace_id=workspace.id) + + db.session.add(workspace) + db.session.add(workspace_role) return workspace @classmethod From 59e1b2fe6914809323be4a3cbf5fda7fd5837fa8 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 17 Aug 2018 13:20:27 -0400 Subject: [PATCH 07/45] Commit workspace --- atst/domain/workspaces.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 20b77794..c910671f 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -22,6 +22,8 @@ class Workspaces(object): db.session.add(workspace) db.session.add(workspace_role) + db.session.commit() + return workspace @classmethod From a438e409bae14c3e1e57175c8ea4aa4b16a19a9b Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 20 Aug 2018 10:22:01 -0400 Subject: [PATCH 08/45] Refactor workspace tests --- tests/domain/test_workspaces.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index d72be57e..5bac8b6c 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -10,12 +10,16 @@ from tests.factories import WorkspaceFactory, RequestFactory, UserFactory def test_can_create_workspace(): request = RequestFactory.create() - workspace = Workspaces.create(request) - assert workspace.request == request - assert workspace.name == request.id - 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.request == request + assert workspace.name == str(request.id) def test_can_get_workspace(): @@ -46,6 +50,4 @@ def test_creating_workspace_adds_owner(): def test_workspace_has_timestamps(): request = RequestFactory.create() workspace = Workspaces.create(request) - assert workspace.request == request - assert workspace.name == request.id assert workspace.time_created == workspace.time_updated From 43263f35cc7a3a35f12d3207d8ad523af025d398 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 20 Aug 2018 10:52:04 -0400 Subject: [PATCH 09/45] Fix workspace_role workspace relationship --- ...22b9_add_workspace_role_workspace_id_fk.py | 28 +++++++++++++++++++ atst/domain/workspaces.py | 2 +- atst/models/workspace_role.py | 7 +++-- tests/domain/test_workspace_users.py | 24 ++++++++-------- 4 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 alembic/versions/f36f130622b9_add_workspace_role_workspace_id_fk.py diff --git a/alembic/versions/f36f130622b9_add_workspace_role_workspace_id_fk.py b/alembic/versions/f36f130622b9_add_workspace_role_workspace_id_fk.py new file mode 100644 index 00000000..aef41c19 --- /dev/null +++ b/alembic/versions/f36f130622b9_add_workspace_role_workspace_id_fk.py @@ -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 ### diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index c910671f..3997ab9f 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -18,7 +18,7 @@ class Workspaces(object): workspace = Workspace(request=request, name=name) role = Roles.get("owner") - workspace_role = WorkspaceRole(user_id=request.creator.id, role=role, workspace_id=workspace.id) + workspace_role = WorkspaceRole(user=request.creator, role=role, workspace=workspace) db.session.add(workspace) db.session.add(workspace_role) diff --git a/atst/models/workspace_role.py b/atst/models/workspace_role.py index 86970e0b..cb008bd3 100644 --- a/atst/models/workspace_role.py +++ b/atst/models/workspace_role.py @@ -10,11 +10,14 @@ class WorkspaceRole(Base): __tablename__ = "workspace_role" 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") + 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") + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) + Index( "workspace_role_user_workspace", diff --git a/tests/domain/test_workspace_users.py b/tests/domain/test_workspace_users.py index 2c651235..be24796b 100644 --- a/tests/domain/test_workspace_users.py +++ b/tests/domain/test_workspace_users.py @@ -1,32 +1,30 @@ -from uuid import uuid4 - from atst.domain.workspace_users import WorkspaceUsers from atst.domain.users import Users +from tests.factories import WorkspaceFactory def test_can_create_new_workspace_user(): - workspace_id = uuid4() - user = Users.create("developer") + workspace = WorkspaceFactory.create() + 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 == user.id + assert workspace_users[0].user.id == new_user.id assert workspace_users[0].user.atat_role.name == "developer" assert workspace_users[0].workspace_role.role.name == "owner" def test_can_update_existing_workspace_user(): - workspace_id = uuid4() - user = Users.create("developer") + workspace = WorkspaceFactory.create() + new_user = Users.create("developer") 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_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" From ee17ca6633a688cdba1c7a1d744adfdc24c518a0 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 10:26:12 -0400 Subject: [PATCH 10/45] Add Workspace.roles --- atst/models/workspace.py | 2 +- atst/models/workspace_role.py | 2 +- tests/domain/test_workspaces.py | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/atst/models/workspace.py b/atst/models/workspace.py index 31476254..151889a6 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -11,10 +11,10 @@ class Workspace(Base, TimestampsMixin): 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): diff --git a/atst/models/workspace_role.py b/atst/models/workspace_role.py index cb008bd3..be238866 100644 --- a/atst/models/workspace_role.py +++ b/atst/models/workspace_role.py @@ -11,7 +11,7 @@ class WorkspaceRole(Base): id = Id() workspace_id = Column(UUID(as_uuid=True), ForeignKey("workspaces.id"), index=True) - workspace = relationship("Workspace") + workspace = relationship("Workspace", back_populates="roles") role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id")) role = relationship("Role") diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index 5bac8b6c..29dc231e 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -18,7 +18,6 @@ def test_can_create_workspace(): def test_default_workspace_name_is_request_id(): request = RequestFactory.create() workspace = Workspaces.create(request) - assert workspace.request == request assert workspace.name == str(request.id) @@ -51,3 +50,9 @@ def test_workspace_has_timestamps(): request = RequestFactory.create() workspace = Workspaces.create(request) assert workspace.time_created == workspace.time_updated + + +def test_workspace_has_roles(): + request = RequestFactory.create() + workspace = Workspaces.create(request) + assert workspace.roles[0].user == request.creator From 7d165e45d37d8986d68e85099bc070e5fb651ead Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 10:36:22 -0400 Subject: [PATCH 11/45] Authorize user in Workspaces.get --- atst/domain/workspaces.py | 7 +++++-- atst/models/workspace.py | 4 ++++ tests/domain/test_workspaces.py | 30 ++++++++++++++---------------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 3997ab9f..cd161267 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -1,7 +1,7 @@ from sqlalchemy.orm.exc import NoResultFound from atst.database import db -from atst.domain.exceptions import NotFoundError +from atst.domain.exceptions import NotFoundError, UnauthorizedError from atst.models.workspace import Workspace from atst.models.workspace_role import WorkspaceRole from atst.domain.roles import Roles @@ -27,12 +27,15 @@ class Workspaces(object): return workspace @classmethod - def get(cls, workspace_id): + def get(cls, user, workspace_id): try: workspace = db.session.query(Workspace).filter_by(id=workspace_id).one() except NoResultFound: raise NotFoundError("workspace") + if user not in workspace.users: + raise UnauthorizedError(user, "get workspace") + return workspace @classmethod diff --git a/atst/models/workspace.py b/atst/models/workspace.py index 151889a6..1f82a594 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -19,3 +19,7 @@ class Workspace(Base, TimestampsMixin): @property def owner(self): return self.request.creator + + @property + def users(self): + return set(role.user for role in self.roles) diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index 29dc231e..abdd2cca 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -1,9 +1,8 @@ import pytest from uuid import uuid4 -from atst.domain.exceptions import NotFoundError +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 @@ -21,15 +20,9 @@ def test_default_workspace_name_is_request_id(): assert workspace.name == str(request.id) -def test_can_get_workspace(): - workspace = WorkspaceFactory.create() - found = Workspaces.get(workspace.id) - assert workspace == found - - -def test_nonexistent_workspace_raises(): +def test_get_nonexistent_workspace_raises(): with pytest.raises(NotFoundError): - Workspaces.get(uuid4()) + Workspaces.get(UserFactory.build(), uuid4()) def test_can_get_workspace_by_request(): @@ -42,8 +35,7 @@ def test_creating_workspace_adds_owner(): user = UserFactory.create() request = RequestFactory.create(creator=user) workspace = Workspaces.create(request) - workspace_user = WorkspaceUsers.get(workspace.id, user.id) - assert workspace_user.workspace_role + assert workspace.roles[0].user == user def test_workspace_has_timestamps(): @@ -52,7 +44,13 @@ def test_workspace_has_timestamps(): assert workspace.time_created == workspace.time_updated -def test_workspace_has_roles(): - request = RequestFactory.create() - workspace = Workspaces.create(request) - assert workspace.roles[0].user == request.creator +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) From 92553f3b39dd811f7ea34b8ebfd1a9592d7b6347 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 10:44:36 -0400 Subject: [PATCH 12/45] Add Workspaces.get_many --- atst/domain/workspaces.py | 5 +++++ atst/routes/workspaces.py | 7 ++++--- tests/domain/test_workspaces.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index cd161267..b0f8819c 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -47,6 +47,11 @@ class Workspaces(object): return workspace + @classmethod + def get_many(cls, user): + workspaces = db.session.query(Workspace).join(WorkspaceRole).filter(WorkspaceRole.user == user).all() + return workspaces + class Projects(object): diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 40be4aee..d721eb58 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -15,13 +15,14 @@ def workspace(): @bp.route("/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//projects") def workspace_projects(workspace_id): - projects = Projects.get_many(workspace_id) - return render_template("workspace_projects.html", projects=projects) + workspace = Workspaces.get(g.current_user, workspace_id) + return render_template("workspace_projects.html", workspace=workspace) @bp.route("/workspaces//members") diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index abdd2cca..2a406f39 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -54,3 +54,18 @@ def test_workspaces_get_ensures_user_is_in_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] From 95f7fb202639021eb4f66b360339919d7ad6770b Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 10:48:24 -0400 Subject: [PATCH 13/45] Add workspace in seed.py --- script/seed.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/script/seed.py b/script/seed.py index d865b9d7..7ddc7cf7 100644 --- a/script/seed.py +++ b/script/seed.py @@ -8,6 +8,7 @@ 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.workspaces import Workspaces from atst.domain.exceptions import AlreadyExistsError from tests.factories import RequestFactory from atst.routes.dev import _DEV_USERS as DEV_USERS @@ -23,11 +24,15 @@ def seed_db(): pass for user in users: + requests = [] for dollar_value in [1, 200, 3000, 40000, 500000, 1000000]: request = Requests.create( user, RequestFactory.build_request_body(user, dollar_value) ) Requests.submit(request) + requests.append(request) + + Workspaces.create(request[0]) if __name__ == "__main__": From dcd69f6b9fb245ec13659ae80097cd9d4edf4555 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 10:51:29 -0400 Subject: [PATCH 14/45] Workspaces page working --- atst/models/workspace.py | 8 ++++++++ atst/routes/workspaces.py | 5 ++--- templates/workspaces.html | 8 ++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/atst/models/workspace.py b/atst/models/workspace.py index 1f82a594..f88ea7a0 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -23,3 +23,11 @@ class Workspace(Base, TimestampsMixin): @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"} diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index d721eb58..a94a6224 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -1,7 +1,6 @@ -from flask import Blueprint, render_template, request as http_request - -from atst.domain.workspaces import Members, Projects, Workspaces +from flask import Blueprint, render_template, request as http_request, g +from atst.domain.workspaces import Workspaces bp = Blueprint("workspaces", __name__) diff --git a/templates/workspaces.html b/templates/workspaces.html index 5f407766..14d4600c 100644 --- a/templates/workspaces.html +++ b/templates/workspaces.html @@ -11,16 +11,16 @@ - {% for w in workspaces %} + {% for workspace in workspaces %} - {{ w['name'] }}
+ {{ workspace.name }}
- #{{ w['task_order']['number'] }} + #{{ workspace.task_order.number }} - {{ w['user_count'] }}Users + {{ workspace.user_count }}Users {% endfor %} From 0de8866919dbee7f22a190c548ad601d78882439 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 11:25:44 -0400 Subject: [PATCH 15/45] workspace_projects route working --- atst/domain/workspaces.py | 53 ++++--------------------------- atst/models/project.py | 2 +- atst/routes/workspaces.py | 10 ++++-- templates/workspace_projects.html | 10 +++--- 4 files changed, 20 insertions(+), 55 deletions(-) diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index b0f8819c..8f5da292 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -1,9 +1,10 @@ from sqlalchemy.orm.exc import NoResultFound from atst.database import db -from atst.domain.exceptions import NotFoundError, UnauthorizedError from atst.models.workspace import Workspace from atst.models.workspace_role import WorkspaceRole +from atst.models.project import Project +from atst.domain.exceptions import NotFoundError, UnauthorizedError from atst.domain.roles import Roles @@ -55,54 +56,14 @@ class Workspaces(object): class Projects(object): - def __init__(self): - pass - @classmethod - def create(cls, creator_id, body): - pass + def create(cls, workspace, name, description): + project = Project(workspace=workspace, name=name, description=description) - @classmethod - def get(cls, project_id): - pass + db.session.add(project) + db.session.commit() - @classmethod - def get_many(cls, workspace_id): - return [ - { - "id": "187c9bea-9541-45d7-801f-cf8e7a642e93", - "name": "Code.mil", - "environments": [ - { - "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 + return project class Members(object): diff --git a/atst/models/project.py b/atst/models/project.py index 05ee853e..46562e82 100644 --- a/atst/models/project.py +++ b/atst/models/project.py @@ -15,4 +15,4 @@ class Project(Base, TimestampsMixin): workspace_id = Column(ForeignKey("workspaces.id"), nullable=False) workspace = relationship("Workspace") - projects = relationship("Environment", back_populates="project") + environments = relationship("Environment", back_populates="project") diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index a94a6224..3dd193de 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -1,6 +1,6 @@ from flask import Blueprint, render_template, request as http_request, g -from atst.domain.workspaces import Workspaces +from atst.domain.workspaces import Workspaces, Members bp = Blueprint("workspaces", __name__) @@ -26,8 +26,12 @@ def workspace_projects(workspace_id): @bp.route("/workspaces//members") def workspace_members(workspace_id): - members = Members.get_many(workspace_id) - return render_template("workspace_members.html", members=members) + workspace = Workspaces.get(g.current_user, workspace_id) + members_repo = Members() + members = members_repo.get_many(workspace_id) + return render_template( + "workspace_members.html", workspace=workspace, members=members + ) @bp.route("/workspaces//reports") diff --git a/templates/workspace_projects.html b/templates/workspace_projects.html index ccc99759..d299897a 100644 --- a/templates/workspace_projects.html +++ b/templates/workspace_projects.html @@ -4,21 +4,21 @@ {% block workspace_content %} -{% for project in projects %} +{% for project in workspace.projects %}
-

{{ project['name'] }} ({{ project['environments']|length }} environments)

- +

{{ project.name }} ({{ project.environments|length }} environments)

+
{{ Icon('edit') }} edit
    - {% for environment in project['environments'] %} + {% for environment in project.environments %}
  • {{ Icon('link') }} - {{ environment["name"]}} + {{ environment.name }}
    From 99d0f9a66708b42b7e965a14935060f4773a95fd Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 11:25:58 -0400 Subject: [PATCH 16/45] Create workspace and project in seed.py --- script/seed.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/script/seed.py b/script/seed.py index 7ddc7cf7..b36e73c5 100644 --- a/script/seed.py +++ b/script/seed.py @@ -8,7 +8,7 @@ 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.workspaces import Workspaces +from atst.domain.workspaces import Workspaces, Projects from atst.domain.exceptions import AlreadyExistsError from tests.factories import RequestFactory from atst.routes.dev import _DEV_USERS as DEV_USERS @@ -32,7 +32,12 @@ def seed_db(): Requests.submit(request) requests.append(request) - Workspaces.create(request[0]) + 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__": From 2b7798d03bef3c499cf7638c03ade20deb0da8f6 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 11:28:22 -0400 Subject: [PATCH 17/45] Formatting --- atst/domain/workspaces.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 8f5da292..0fedcc05 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -19,7 +19,9 @@ class Workspaces(object): workspace = Workspace(request=request, name=name) role = Roles.get("owner") - workspace_role = WorkspaceRole(user=request.creator, role=role, workspace=workspace) + workspace_role = WorkspaceRole( + user=request.creator, role=role, workspace=workspace + ) db.session.add(workspace) db.session.add(workspace_role) @@ -50,12 +52,16 @@ class Workspaces(object): @classmethod def get_many(cls, user): - workspaces = db.session.query(Workspace).join(WorkspaceRole).filter(WorkspaceRole.user == user).all() + workspaces = ( + db.session.query(Workspace) + .join(WorkspaceRole) + .filter(WorkspaceRole.user == user) + .all() + ) return workspaces class Projects(object): - @classmethod def create(cls, workspace, name, description): project = Project(workspace=workspace, name=name, description=description) @@ -67,7 +73,6 @@ class Projects(object): class Members(object): - def __init__(self): pass From 4d4e90f482155973c49fb7c7c88831b83fdc058e Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 11:31:07 -0400 Subject: [PATCH 18/45] Remove eralchemy and print_schema --- Pipfile | 1 - Pipfile.lock | 19 ++++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/Pipfile b/Pipfile index fef290df..1312960b 100644 --- a/Pipfile +++ b/Pipfile @@ -29,7 +29,6 @@ black = "*" pytest-watch = "*" factory-boy = "*" pytest-flask = "*" -pytest-env = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index 2dfe651e..707a6ec2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "41ad134816dae388385cfb15105e0eca436b25791ec4fbf67a2b36c4ae8056bd" + "sha256": "5fc8273838354406366b401529a6f512a73ac6a8ecea6699afa4ab7b4996bf13" }, "pipfile-spec": 6, "requires": { @@ -271,7 +271,7 @@ "sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622", "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.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==2018.5" }, "redis": { @@ -317,7 +317,7 @@ "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "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.1.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version < '4'", "version": "==1.23" }, "webassets": { @@ -501,7 +501,7 @@ "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "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 != '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" }, "itsdangerous": { @@ -619,7 +619,7 @@ "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "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 != '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" }, "prompt-toolkit": { @@ -642,7 +642,7 @@ "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", "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 != '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" }, "pygments": { @@ -668,13 +668,6 @@ "index": "pypi", "version": "==3.7.2" }, - "pytest-env": { - "hashes": [ - "sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2" - ], - "index": "pypi", - "version": "==0.6.2" - }, "pytest-flask": { "hashes": [ "sha256:2c5a36f9033ef8b6f85ddbefaebdd4f89197fc283f94b20dfe1a1beba4b77f03", From 81619e07ebbd38df2a1a5e38e12c003a3f6333ff Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 12:00:46 -0400 Subject: [PATCH 19/45] Try to decouple financial verification tests from Reqeusts internals --- tests/routes/test_financial_verification.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/routes/test_financial_verification.py b/tests/routes/test_financial_verification.py index 675b15e3..242cf575 100644 --- a/tests/routes/test_financial_verification.py +++ b/tests/routes/test_financial_verification.py @@ -1,12 +1,10 @@ -import re -import pytest import urllib from flask import url_for from atst.eda_client import MockEDAClient from tests.mocks import MOCK_REQUEST, MOCK_USER -from tests.factories import PENumberFactory +from tests.factories import PENumberFactory, RequestFactory class TestPENumberInForm: @@ -39,14 +37,13 @@ class TestPENumberInForm: def _set_monkeypatches(self, monkeypatch): 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) 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: url_kwargs["extended"] = True - response = client.post( url_for("requests.financial_verification", **url_kwargs), headers={"Content-Type": "application/x-www-form-urlencoded"}, From 18cd1b447366fa6c5406a67b5ccda3579d8a2ed3 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 13:06:12 -0400 Subject: [PATCH 20/45] Approve request, create workspace after fin. verification --- atst/domain/requests.py | 10 ++++++++++ atst/routes/requests/financial_verification.py | 16 +++++++++++----- tests/routes/test_financial_verification.py | 4 ++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/atst/domain/requests.py b/atst/domain/requests.py index a1102f92..c86e2adc 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -6,6 +6,7 @@ from sqlalchemy.orm.attributes import flag_modified from atst.models.request import Request from atst.models.request_status_event import RequestStatusEvent, RequestStatus +from atst.domain.workspaces import Workspaces from atst.database import db from .exceptions import NotFoundError @@ -114,6 +115,15 @@ class Requests(object): db.session.add(request) db.session.commit() + return request + + @classmethod + def update_financial_verification(cls, request_id, data): + updated_request = Requests.update(request_id, {"financial_verification": data}) + approved_request = Requests.set_status(updated_request, RequestStatus.APPROVED) + workspace = Workspaces.create(approved_request) + return workspace + @classmethod def set_status(cls, request: Request, status: RequestStatus): status_event = RequestStatusEvent(new_status=status) diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py index b7fc4993..2216238f 100644 --- a/atst/routes/requests/financial_verification.py +++ b/atst/routes/requests/financial_verification.py @@ -31,17 +31,23 @@ def update_financial_verification(request_id): existing_request = Requests.get(request_id) 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(): - request_data = {"financial_verification": form.data} valid = form.perform_extra_validation( existing_request.body.get("financial_verification") ) - Requests.update(request_id, request_data) + new_workspace = Requests.update_financial_verification(request_id, post_data) if valid: - return redirect(url_for("requests.financial_verification_submitted")) - + return redirect( + url_for( + "workspaces.workspace_projects", + workspace_id=new_workspace.id, + modal=True, + ) + ) else: form.reset() return render_template( diff --git a/tests/routes/test_financial_verification.py b/tests/routes/test_financial_verification.py index 242cf575..b63a6f99 100644 --- a/tests/routes/test_financial_verification.py +++ b/tests/routes/test_financial_verification.py @@ -69,7 +69,7 @@ class TestPENumberInForm: response = self.submit_data(client, data) 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): self._set_monkeypatches(monkeypatch) @@ -81,7 +81,7 @@ class TestPENumberInForm: response = self.submit_data(client, data) 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): self._set_monkeypatches(monkeypatch) From 7c21e64c51b3dcc824b33340e9de40eac3bd74bf Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 13:46:54 -0400 Subject: [PATCH 21/45] WIP projects templates --- templates/navigation/workspace_navigation.html | 4 ++-- templates/workspace_project_new.html | 8 ++++++++ templates/workspace_projects.html | 11 +++++++++++ templates/workspaces_blank.html.to | 12 ------------ 4 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 templates/workspace_project_new.html delete mode 100644 templates/workspaces_blank.html.to diff --git a/templates/navigation/workspace_navigation.html b/templates/navigation/workspace_navigation.html index 3ed02ea9..6dfa4f09 100644 --- a/templates/navigation/workspace_navigation.html +++ b/templates/navigation/workspace_navigation.html @@ -9,8 +9,8 @@ subnav=[ { "label": "Add New Project", - "href":"/", - "active": g.matchesPath('workspaces/projects/new'), + "href": url_for('workspaces.new_project', workspace_id=workspace.id), + "active": g.matchesPath('\/workspaces\/[A-Za-z0-9-]*\/projects'), "icon": "plus" } ] diff --git a/templates/workspace_project_new.html b/templates/workspace_project_new.html new file mode 100644 index 00000000..0996946d --- /dev/null +++ b/templates/workspace_project_new.html @@ -0,0 +1,8 @@ +{% from "components/icon.html" import Icon %} + +{% extends "base_workspace.html" %} + +{% block workspace_content %} +

    Add a new project

    + +{% endblock %} diff --git a/templates/workspace_projects.html b/templates/workspace_projects.html index d299897a..18f24396 100644 --- a/templates/workspace_projects.html +++ b/templates/workspace_projects.html @@ -1,9 +1,20 @@ {% from "components/icon.html" import Icon %} +{% from "components/alert.html" import Alert %} {% extends "base_workspace.html" %} {% block workspace_content %} +{% if True %} + {{ Alert('Workspace created!', + message="\ +

    You are now ready to create projects and environments within the JEDI Cloud.

    + ", + actions='', + level='success' + ) }} +{% endif %} + {% for project in workspace.projects %}
    diff --git a/templates/workspaces_blank.html.to b/templates/workspaces_blank.html.to deleted file mode 100644 index 0e5f4a0a..00000000 --- a/templates/workspaces_blank.html.to +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "base.html.to" %} - -{% block content %} - -
    -

    There are currently no JEDI workspaces

    - New Workspace -
    - - -{% end %} - From 5203690748b808d1d4d34e8cd30bc99101153bf0 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 14:21:03 -0400 Subject: [PATCH 22/45] Create new project --- atst/domain/workspaces.py | 10 ++++++++ atst/forms/new_project.py | 9 +++++++ atst/routes/workspaces.py | 30 ++++++++++++++++++++--- templates/workspace_project_new.html | 36 +++++++++++++++++++++++++++- 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 atst/forms/new_project.py diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 0fedcc05..985447f0 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -4,6 +4,7 @@ from atst.database import db from atst.models.workspace import Workspace from atst.models.workspace_role import WorkspaceRole from atst.models.project import Project +from atst.models.environment import Environment from atst.domain.exceptions import NotFoundError, UnauthorizedError from atst.domain.roles import Roles @@ -72,6 +73,15 @@ class Projects(object): return project +class Environments(object): + @classmethod + def create(cls, project, name): + environment = Environment(project=project, name=name) + db.session.add(environment) + db.session.commit() + return environment + + class Members(object): def __init__(self): pass diff --git a/atst/forms/new_project.py b/atst/forms/new_project.py new file mode 100644 index 00000000..1552a1ab --- /dev/null +++ b/atst/forms/new_project.py @@ -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") diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 3dd193de..3cee0fab 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -1,6 +1,7 @@ -from flask import Blueprint, render_template, request as http_request, g +from flask import Blueprint, render_template, request as http_request, g, redirect, url_for -from atst.domain.workspaces import Workspaces, Members +from atst.domain.workspaces import Workspaces, Members, Projects, Environments +from atst.forms.new_project import NewProjectForm bp = Blueprint("workspaces", __name__) @@ -36,4 +37,27 @@ def workspace_members(workspace_id): @bp.route("/workspaces//reports") def workspace_reports(workspace_id): - return render_template("workspace_reports.html") + return render_template("workspace_reports.html", workspace_id=workspace_id) + + +@bp.route("/workspaces//projects/new") +def new_project(workspace_id): + workspace = Workspaces.get(g.current_user, workspace_id) + form = NewProjectForm() + return render_template("workspace_project_new.html", workspace=workspace, form=form) + + +@bp.route("/workspaces//projects", methods=["POST"]) +def update_project(workspace_id): + workspace = Workspaces.get(g.current_user, workspace_id) + form = NewProjectForm(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) + ) diff --git a/templates/workspace_project_new.html b/templates/workspace_project_new.html index 0996946d..5fdfe042 100644 --- a/templates/workspace_project_new.html +++ b/templates/workspace_project_new.html @@ -1,8 +1,42 @@ {% from "components/icon.html" import Icon %} +{% from "components/text_input.html" import TextInput %} {% extends "base_workspace.html" %} {% block workspace_content %} -

    Add a new project

    +
    + {{ form.csrf_token }} +
    +
    +

    Add a new project

    +
    + +
    + {{ TextInput(form.name) }} + {{ TextInput(form.description, paragraph=True) }} +
    +
    + +
    +
    +
    +

    Project Environments

    +
    + +
      +
    • + + {{ TextInput(form.environment_name) }} + +
    • +
    +
    +
    + +
    + + +
    +
    {% endblock %} From 67700e13ba2560a6cfa41261d8e93a22c6922711 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 15:11:05 -0400 Subject: [PATCH 23/45] Show alert when workspace is new --- atst/routes/requests/financial_verification.py | 8 +------- templates/workspace_projects.html | 3 +-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py index 2216238f..63a3c449 100644 --- a/atst/routes/requests/financial_verification.py +++ b/atst/routes/requests/financial_verification.py @@ -41,13 +41,7 @@ def update_financial_verification(request_id): ) new_workspace = Requests.update_financial_verification(request_id, post_data) if valid: - return redirect( - url_for( - "workspaces.workspace_projects", - workspace_id=new_workspace.id, - modal=True, - ) - ) + return redirect(url_for("workspaces.workspace_projects", workspace_id=new_workspace.id, newWorkspace=True)) else: form.reset() return render_template( diff --git a/templates/workspace_projects.html b/templates/workspace_projects.html index 18f24396..cd2a1308 100644 --- a/templates/workspace_projects.html +++ b/templates/workspace_projects.html @@ -5,12 +5,11 @@ {% block workspace_content %} -{% if True %} +{% if request.args.get("newWorkspace") %} {{ Alert('Workspace created!', message="\

    You are now ready to create projects and environments within the JEDI Cloud.

    ", - actions='', level='success' ) }} {% endif %} From 020e1b9cb0438b56865ce076d4c32a132c6a13b9 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 15:12:58 -0400 Subject: [PATCH 24/45] Allow user to create new fields for environment names Currently only one of them is being created. --- atst/forms/new_project.py | 2 +- js/components/forms/new_project.js | 46 ++++++++++++++++++++++++++++ js/index.js | 2 ++ templates/workspace_project_new.html | 9 ++++-- 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 js/components/forms/new_project.js diff --git a/atst/forms/new_project.py b/atst/forms/new_project.py index 1552a1ab..033312c5 100644 --- a/atst/forms/new_project.py +++ b/atst/forms/new_project.py @@ -1,5 +1,5 @@ from flask_wtf import Form -from wtforms.fields import StringField, TextAreaField +from wtforms.fields import StringField, TextAreaField, FieldList class NewProjectForm(Form): diff --git a/js/components/forms/new_project.js b/js/components/forms/new_project.js new file mode 100644 index 00000000..cbea6161 --- /dev/null +++ b/js/components/forms/new_project.js @@ -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) + } + } + } +} diff --git a/js/index.js b/js/index.js index 4ecd6aa7..3881a313 100644 --- a/js/index.js +++ b/js/index.js @@ -8,6 +8,7 @@ import checkboxinput from './components/checkbox_input' import DetailsOfUse from './components/forms/details_of_use' import poc from './components/forms/poc' import financial from './components/forms/financial' +import NewProject from './components/forms/new_project' Vue.use(VTooltip) @@ -21,6 +22,7 @@ const app = new Vue({ DetailsOfUse, poc, financial, + NewProject }, methods: { closeModal: function(name) { diff --git a/templates/workspace_project_new.html b/templates/workspace_project_new.html index 5fdfe042..c01c8d4e 100644 --- a/templates/workspace_project_new.html +++ b/templates/workspace_project_new.html @@ -4,6 +4,7 @@ {% extends "base_workspace.html" %} {% block workspace_content %} +
    {{ form.csrf_token }}
    @@ -24,19 +25,21 @@
      -
    • +
    • {{ TextInput(form.environment_name) }} + {{ Icon('x') }}
    + Add another environment
    - + Cancel
    - + {% endblock %} From be47b2dea096ded9c3dee015e14b7f291d2bec46 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 15:20:58 -0400 Subject: [PATCH 25/45] Add some badly-placed tooltips to new project form --- templates/workspace_project_new.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/templates/workspace_project_new.html b/templates/workspace_project_new.html index c01c8d4e..d9e797f2 100644 --- a/templates/workspace_project_new.html +++ b/templates/workspace_project_new.html @@ -1,5 +1,6 @@ {% from "components/icon.html" import Icon %} {% from "components/text_input.html" import TextInput %} +{% from "components/tooltip.html" import Tooltip %} {% extends "base_workspace.html" %} @@ -10,8 +11,13 @@

    Add a new project

    + {{ Tooltip( + "AT-AT allows you to organize your workspace into multiple projects, each of which may have environments.", + title="learn more" + )}}
    +
    {{ TextInput(form.name) }} {{ TextInput(form.description, paragraph=True) }} @@ -22,6 +28,10 @@

    Project Environments

    + {{ 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" + )}}
      From c5013f90285f95af3c60e8c0750f61129dee0efb Mon Sep 17 00:00:00 2001 From: luis cielak Date: Tue, 21 Aug 2018 16:09:22 -0400 Subject: [PATCH 26/45] Resolve conflcits --- styles/elements/_inputs.scss | 2 +- styles/sections/_projects_list.scss | 4 ++++ templates/workspace_project_new.html | 7 +++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index 14b508ab..53fb9d9f 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -58,7 +58,7 @@ } .usa-input { - margin: ($gap * 4) ($gap * 2) ($gap * 4) 0; + margin: ($gap * 4) ($gap * 2) ($gap * 4) 0; @include media($medium-screen) { margin: ($gap * 4) 0; diff --git a/styles/sections/_projects_list.scss b/styles/sections/_projects_list.scss index fe62b7a6..e48bb7af 100644 --- a/styles/sections/_projects_list.scss +++ b/styles/sections/_projects_list.scss @@ -4,6 +4,10 @@ flex-direction: row; justify-content: space-between; + .usa-input { + margin: 0; + } + .project-list-item__environment__link { @include icon-link; @include icon-link-large; diff --git a/templates/workspace_project_new.html b/templates/workspace_project_new.html index d9e797f2..e42a2e6f 100644 --- a/templates/workspace_project_new.html +++ b/templates/workspace_project_new.html @@ -24,10 +24,9 @@
    -
    -

    Project Environments

    +

    Project Environments

    {{ 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" @@ -35,7 +34,7 @@
      -
    • +
    • {{ TextInput(form.environment_name) }} {{ Icon('x') }} @@ -47,7 +46,7 @@
    - + Cancel
    From 3c5c2b8b5e1925ffcd15a2a93f43be4d29444636 Mon Sep 17 00:00:00 2001 From: luis cielak Date: Tue, 21 Aug 2018 16:28:30 -0400 Subject: [PATCH 27/45] Add action footer and some styles --- styles/elements/_panels.scss | 1 - templates/workspace_project_new.html | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/styles/elements/_panels.scss b/styles/elements/_panels.scss index 27fdc144..303351fe 100644 --- a/styles/elements/_panels.scss +++ b/styles/elements/_panels.scss @@ -63,7 +63,6 @@ .panel__heading { margin: $gap * 2; - @include media($medium-screen) { margin: $gap * 4; } diff --git a/templates/workspace_project_new.html b/templates/workspace_project_new.html index e42a2e6f..98242bed 100644 --- a/templates/workspace_project_new.html +++ b/templates/workspace_project_new.html @@ -27,10 +27,12 @@

    Project Environments

    + {{ 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" )}} +
      @@ -41,14 +43,20 @@
    + +
    - Add another environment + + +
    + + Cancel +
    +
    -
    - - Cancel -
    {% endblock %} From 6236999cafad0b2619bbad8a4214772e6e1ba6e9 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 16:38:23 -0400 Subject: [PATCH 28/45] Fix migration so that we can downgrade --- .../f549c7cee17c_remove_workspaces_task_order_association.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alembic/versions/f549c7cee17c_remove_workspaces_task_order_association.py b/alembic/versions/f549c7cee17c_remove_workspaces_task_order_association.py index 7b57cfb9..7bf2069e 100644 --- a/alembic/versions/f549c7cee17c_remove_workspaces_task_order_association.py +++ b/alembic/versions/f549c7cee17c_remove_workspaces_task_order_association.py @@ -25,6 +25,6 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('workspaces', sa.Column('task_order_id', sa.INTEGER(), autoincrement=False, nullable=False)) + 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 ### From bfe901d6d8eb61ed7a02f31c2384c3ca3abe5647 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 16:47:19 -0400 Subject: [PATCH 29/45] Fix test --- tests/routes/test_financial_verification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/routes/test_financial_verification.py b/tests/routes/test_financial_verification.py index b63a6f99..d6552846 100644 --- a/tests/routes/test_financial_verification.py +++ b/tests/routes/test_financial_verification.py @@ -129,4 +129,4 @@ class TestPENumberInForm: response = self.submit_data(client, data, extended=True) assert response.status_code == 302 - assert "/requests/financial_verification_submitted" in response.headers.get("Location") + assert "/workspaces" in response.headers.get("Location") From 9669a42b9aa4a67263876da844236c79897c17a6 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 16:57:56 -0400 Subject: [PATCH 30/45] Workspaces.get requires user --- atst/routes/workspaces.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 3cee0fab..0b5c9773 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -9,7 +9,7 @@ bp = Blueprint("workspaces", __name__) def workspace(): workspace = None if "workspace_id" in http_request.view_args: - workspace = Workspaces.get(http_request.view_args["workspace_id"]) + workspace = Workspaces.get(g.current_user, http_request.view_args["workspace_id"]) return { "workspace": workspace } @@ -50,7 +50,7 @@ def new_project(workspace_id): @bp.route("/workspaces//projects", methods=["POST"]) def update_project(workspace_id): workspace = Workspaces.get(g.current_user, workspace_id) - form = NewProjectForm(request.form) + form = NewProjectForm(http_request.form) if form.validate(): project_data = form.data From ee94784ac9647ff40bda73baae9f265a82794cfe Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 20:17:48 -0400 Subject: [PATCH 31/45] Fix bug that attempted to create duplicate workspaces --- atst/domain/requests.py | 9 ++++++--- atst/routes/requests/financial_verification.py | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/atst/domain/requests.py b/atst/domain/requests.py index c86e2adc..4836fd96 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -118,10 +118,13 @@ class Requests(object): return request @classmethod - def update_financial_verification(cls, request_id, data): - updated_request = Requests.update(request_id, {"financial_verification": data}) - approved_request = Requests.set_status(updated_request, RequestStatus.APPROVED) + 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 diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py index 63a3c449..c16c6e10 100644 --- a/atst/routes/requests/financial_verification.py +++ b/atst/routes/requests/financial_verification.py @@ -36,11 +36,13 @@ def update_financial_verification(request_id): ) if form.validate(): + request_data = {"financial_verification": form.data} valid = form.perform_extra_validation( existing_request.body.get("financial_verification") ) - new_workspace = Requests.update_financial_verification(request_id, post_data) + updated_request = Requests.update(request_id, request_data) if valid: + new_workspace = Requests.approve_and_create_workspace(updated_request) return redirect(url_for("workspaces.workspace_projects", workspace_id=new_workspace.id, newWorkspace=True)) else: form.reset() From e0ebce1448d390605717b90c51018212e2c97adb Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 20:21:58 -0400 Subject: [PATCH 32/45] Remove unused import --- atst/forms/new_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atst/forms/new_project.py b/atst/forms/new_project.py index 033312c5..1552a1ab 100644 --- a/atst/forms/new_project.py +++ b/atst/forms/new_project.py @@ -1,5 +1,5 @@ from flask_wtf import Form -from wtforms.fields import StringField, TextAreaField, FieldList +from wtforms.fields import StringField, TextAreaField class NewProjectForm(Form): From 9dd1a417e09314d90a3b624d6a32cdb66a392312 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 20:25:10 -0400 Subject: [PATCH 33/45] Re-introduce pytest-env --- Pipfile | 1 + Pipfile.lock | 65 +++++++++++----------------------------------------- 2 files changed, 15 insertions(+), 51 deletions(-) diff --git a/Pipfile b/Pipfile index 1312960b..fef290df 100644 --- a/Pipfile +++ b/Pipfile @@ -29,6 +29,7 @@ black = "*" pytest-watch = "*" factory-boy = "*" pytest-flask = "*" +pytest-env = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index 707a6ec2..8a19a1a2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5fc8273838354406366b401529a6f512a73ac6a8ecea6699afa4ab7b4996bf13" + "sha256": "41ad134816dae388385cfb15105e0eca436b25791ec4fbf67a2b36c4ae8056bd" }, "pipfile-spec": 6, "requires": { @@ -271,7 +271,7 @@ "sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622", "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.*'", + "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" }, "redis": { @@ -317,7 +317,7 @@ "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" ], - "markers": "python_version >= '2.6' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version < '4'", + "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" }, "webassets": { @@ -350,14 +350,6 @@ ], "version": "==1.4.3" }, - "appnope": { - "hashes": [ - "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", - "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.0" - }, "argh": { "hashes": [ "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", @@ -449,6 +441,7 @@ "sha256:ea7cfd3aeb1544732d08bd9cfba40c5b78e3a91e17b1a0698ab81bfc5554c628", "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" }, "flask": { @@ -501,7 +494,7 @@ "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "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.*'", + "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" }, "itsdangerous": { @@ -619,7 +612,7 @@ "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "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.*'", + "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" }, "prompt-toolkit": { @@ -642,7 +635,7 @@ "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", "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.*'", + "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" }, "pygments": { @@ -668,6 +661,13 @@ "index": "pypi", "version": "==3.7.2" }, + "pytest-env": { + "hashes": [ + "sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2" + ], + "index": "pypi", + "version": "==0.6.2" + }, "pytest-flask": { "hashes": [ "sha256:2c5a36f9033ef8b6f85ddbefaebdd4f89197fc283f94b20dfe1a1beba4b77f03", @@ -747,43 +747,6 @@ ], "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": { "hashes": [ "sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162" From 0469e061dad553b64c890b5d61a42479a041b753 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 21 Aug 2018 20:47:22 -0400 Subject: [PATCH 34/45] Check permissions when attempting to create a project --- atst/domain/authz.py | 8 ++++++++ atst/domain/workspace_users.py | 23 +++++++++++++++++++++++ atst/domain/workspaces.py | 11 +++++++++++ atst/routes/workspaces.py | 4 ++-- tests/domain/test_workspaces.py | 18 ++++++++++++++++++ 5 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 atst/domain/authz.py diff --git a/atst/domain/authz.py b/atst/domain/authz.py new file mode 100644 index 00000000..08f7ac21 --- /dev/null +++ b/atst/domain/authz.py @@ -0,0 +1,8 @@ +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() diff --git a/atst/domain/workspace_users.py b/atst/domain/workspace_users.py index a67c0cbb..e7e51077 100644 --- a/atst/domain/workspace_users.py +++ b/atst/domain/workspace_users.py @@ -31,6 +31,29 @@ class WorkspaceUsers(object): 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 def add_many(cls, workspace_id, workspace_user_dicts): workspace_users = [] diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 985447f0..ebede1f1 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -7,6 +7,8 @@ from atst.models.project import Project from atst.models.environment import Environment 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): @@ -42,6 +44,15 @@ class Workspaces(object): return workspace + @classmethod + def get_for_update(cls, user, workspace_id): + workspace = Workspaces.get(user, workspace_id) + if not Authorization.has_workspace_permission( + user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE + ): + raise UnauthorizedError(user, "add project") + return workspace + @classmethod def get_by_request(cls, request): try: diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 0b5c9773..34950e45 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -42,14 +42,14 @@ def workspace_reports(workspace_id): @bp.route("/workspaces//projects/new") def new_project(workspace_id): - workspace = Workspaces.get(g.current_user, 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//projects", methods=["POST"]) def update_project(workspace_id): - workspace = Workspaces.get(g.current_user, workspace_id) + workspace = Workspaces.get_for_update(g.current_user, workspace_id) form = NewProjectForm(http_request.form) if form.validate(): diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index 2a406f39..468536a8 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -3,6 +3,7 @@ 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 @@ -69,3 +70,20 @@ def test_workspaces_get_many_returns_a_users_workspaces(): 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) From e2e6e6da4d4d472df123311813bc580f10e4630d Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 22 Aug 2018 10:10:24 -0400 Subject: [PATCH 35/45] Redirect /workspaces/ to canonical URL --- atst/routes/workspaces.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 34950e45..2bced4d7 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -1,16 +1,26 @@ -from flask import Blueprint, render_template, request as http_request, g, redirect, url_for +from flask import ( + Blueprint, + render_template, + request as http_request, + g, + redirect, + url_for, +) from atst.domain.workspaces import Workspaces, Members, Projects, Environments from atst.forms.new_project import NewProjectForm bp = Blueprint("workspaces", __name__) + @bp.context_processor def workspace(): workspace = None if "workspace_id" in http_request.view_args: - workspace = Workspaces.get(g.current_user, http_request.view_args["workspace_id"]) - return { "workspace": workspace } + workspace = Workspaces.get( + g.current_user, http_request.view_args["workspace_id"] + ) + return {"workspace": workspace} @bp.route("/workspaces") @@ -25,6 +35,11 @@ def workspace_projects(workspace_id): return render_template("workspace_projects.html", workspace=workspace) +@bp.route("/workspaces/") +def show_workspace(workspace_id): + return redirect(url_for("workspaces.workspace_projects", workspace_id=workspace_id)) + + @bp.route("/workspaces//members") def workspace_members(workspace_id): workspace = Workspaces.get(g.current_user, workspace_id) From 3eb9076b03dbbc5eb986de9101b7d81a526486be Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 22 Aug 2018 11:05:51 -0400 Subject: [PATCH 36/45] Add Authorization.is_in_workspace --- atst/domain/authz.py | 4 ++++ atst/domain/workspaces.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/atst/domain/authz.py b/atst/domain/authz.py index 08f7ac21..506970b8 100644 --- a/atst/domain/authz.py +++ b/atst/domain/authz.py @@ -6,3 +6,7 @@ class Authorization(object): 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 diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index ebede1f1..83f729f6 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -39,7 +39,7 @@ class Workspaces(object): except NoResultFound: raise NotFoundError("workspace") - if user not in workspace.users: + if not Authorization.is_in_workspace(user, workspace): raise UnauthorizedError(user, "get workspace") return workspace From 1b7b024bd7bbc8111b740083d9138597a29e515c Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 22 Aug 2018 11:10:29 -0400 Subject: [PATCH 37/45] Remove outdated comment --- atst/domain/workspaces.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 83f729f6..afb6bb80 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -12,10 +12,6 @@ from atst.models.permissions import Permissions class Workspaces(object): - # will a request have a TO association? - # do we automatically create an entry for the request.creator in the - # workspace_roles table? - @classmethod def create(cls, request, name=None): name = name or request.id From 50e2666c3ce7919ced433da3347aaa6dd561684d Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 22 Aug 2018 11:21:29 -0400 Subject: [PATCH 38/45] Move Projects and Environments into their own files --- atst/domain/environments.py | 12 ++++++++++++ atst/domain/projects.py | 13 +++++++++++++ atst/domain/workspaces.py | 20 -------------------- atst/routes/workspaces.py | 4 +++- script/seed.py | 3 ++- 5 files changed, 30 insertions(+), 22 deletions(-) create mode 100644 atst/domain/environments.py create mode 100644 atst/domain/projects.py diff --git a/atst/domain/environments.py b/atst/domain/environments.py new file mode 100644 index 00000000..90755052 --- /dev/null +++ b/atst/domain/environments.py @@ -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 + diff --git a/atst/domain/projects.py b/atst/domain/projects.py new file mode 100644 index 00000000..16e0fc4c --- /dev/null +++ b/atst/domain/projects.py @@ -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 diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index afb6bb80..c69aa3ca 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -3,8 +3,6 @@ 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.models.project import Project -from atst.models.environment import Environment from atst.domain.exceptions import NotFoundError, UnauthorizedError from atst.domain.roles import Roles from atst.domain.authz import Authorization @@ -68,25 +66,7 @@ class Workspaces(object): ) return workspaces - -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 - - -class Environments(object): - @classmethod - def create(cls, project, name): - environment = Environment(project=project, name=name) - db.session.add(environment) - db.session.commit() - return environment class Members(object): diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 2bced4d7..23ec4f7a 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -7,7 +7,9 @@ from flask import ( url_for, ) -from atst.domain.workspaces import Workspaces, Members, Projects, Environments +from atst.domain.workspaces import Workspaces, Members +from atst.domain.projects import Projects +from atst.domain.environments import Environments from atst.forms.new_project import NewProjectForm bp = Blueprint("workspaces", __name__) diff --git a/script/seed.py b/script/seed.py index b36e73c5..68df7b04 100644 --- a/script/seed.py +++ b/script/seed.py @@ -8,7 +8,8 @@ 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.workspaces import Workspaces, Projects +from atst.domain.workspaces import Workspaces +from atst.domain.projects import Projects from atst.domain.exceptions import AlreadyExistsError from tests.factories import RequestFactory from atst.routes.dev import _DEV_USERS as DEV_USERS From 287fcf7e3e40e163e40a089422f934dba08407bf Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 22 Aug 2018 11:24:55 -0400 Subject: [PATCH 39/45] Factor out _create_workspace_role --- atst/domain/workspaces.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index c69aa3ca..29fe6d8f 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -14,14 +14,9 @@ class Workspaces(object): def create(cls, request, name=None): name = name or request.id workspace = Workspace(request=request, name=name) - - role = Roles.get("owner") - workspace_role = WorkspaceRole( - user=request.creator, role=role, workspace=workspace - ) + Workspaces._create_workspace_role(request.creator, workspace, "owner") db.session.add(workspace) - db.session.add(workspace_role) db.session.commit() return workspace @@ -67,6 +62,13 @@ class Workspaces(object): return workspaces @classmethod + def _create_workspace_role(cls, user, workspace, role_name): + role = Roles.get(role_name) + workspace_role = WorkspaceRole( + user=user, role=role, workspace=workspace + ) + db.session.add(workspace_role) + return workspace_role class Members(object): From 379461e6fba5f8fbf3ab2001eba5b58814a845d1 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 22 Aug 2018 11:38:10 -0400 Subject: [PATCH 40/45] Get Workspace.owner from role, not from request creator --- atst/models/workspace.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/atst/models/workspace.py b/atst/models/workspace.py index f88ea7a0..0b36a7b6 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -18,7 +18,14 @@ class Workspace(Base, TimestampsMixin): @property def owner(self): - return self.request.creator + return next( + ( + workspace_role.user + for workspace_role in self.roles + if workspace_role.role.name == "owner" + ), + None, + ) @property def users(self): From 9875a1186071ffe453e0ab7346cf2be1c9dfd915 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 22 Aug 2018 11:49:05 -0400 Subject: [PATCH 41/45] Workspace now response to .members --- atst/domain/workspaces.py | 49 -------------------------------- atst/models/workspace.py | 35 +++++++++++++++++++++++ atst/routes/workspaces.py | 8 ++---- templates/workspace_members.html | 4 +-- 4 files changed, 39 insertions(+), 57 deletions(-) diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 29fe6d8f..dab03302 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -69,52 +69,3 @@ class Workspaces(object): ) db.session.add(workspace_role) return workspace_role - - -class Members(object): - def __init__(self): - pass - - @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 diff --git a/atst/models/workspace.py b/atst/models/workspace.py index 0b36a7b6..fd8f981e 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -6,6 +6,37 @@ 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" @@ -38,3 +69,7 @@ class Workspace(Base, TimestampsMixin): @property def task_order(self): return {"number": "task-order-number"} + + @property + def members(self): + return MOCK_MEMBERS diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 23ec4f7a..6b25a04d 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -7,7 +7,7 @@ from flask import ( url_for, ) -from atst.domain.workspaces import Workspaces, Members +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 @@ -45,11 +45,7 @@ def show_workspace(workspace_id): @bp.route("/workspaces//members") def workspace_members(workspace_id): workspace = Workspaces.get(g.current_user, workspace_id) - members_repo = Members() - members = members_repo.get_many(workspace_id) - return render_template( - "workspace_members.html", workspace=workspace, members=members - ) + return render_template("workspace_members.html", workspace=workspace) @bp.route("/workspaces//reports") diff --git a/templates/workspace_members.html b/templates/workspace_members.html index 19cb340f..099e53a0 100644 --- a/templates/workspace_members.html +++ b/templates/workspace_members.html @@ -4,7 +4,7 @@ {% block workspace_content %} -{% if not members %} +{% if not workspace.members %} {{ EmptyState( 'There are currently no members in this Workspace.', @@ -58,7 +58,7 @@ - {% for m in members %} + {% for m in workspace.members %} {{ m['first_name'] }} {{ m['last_name'] }} {% if m['num_projects'] == '0' %} No Project Access {% endif %} From 5616fa4f11099718ff0f898604c211004648d40a Mon Sep 17 00:00:00 2001 From: luis cielak Date: Wed, 22 Aug 2018 10:59:32 -0400 Subject: [PATCH 42/45] Adjust block list title alignmnet when there is a tooltip --- styles/elements/_block_lists.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/styles/elements/_block_lists.scss b/styles/elements/_block_lists.scss index 6cb24d11..ade36741 100644 --- a/styles/elements/_block_lists.scss +++ b/styles/elements/_block_lists.scss @@ -15,11 +15,16 @@ display: flex; flex-direction: row; justify-content: space-between; + + .icon-tooltip { + padding: 0.25rem 0.5rem; + } } @mixin block-list__title { @include h4; margin: 0; + line-height: 3rem; } @mixin block-list__footer { From 6747ef2659be035477f9c1d969f3a70ecc4451fa Mon Sep 17 00:00:00 2001 From: luis cielak Date: Wed, 22 Aug 2018 11:40:22 -0400 Subject: [PATCH 43/45] Updates to tooltip heading and add text to remove icon --- styles/elements/_icon_link.scss | 1 + styles/elements/_panels.scss | 5 +++++ templates/project_edit.html.to | 2 +- templates/workspace_project_new.html | 4 ++-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/styles/elements/_icon_link.scss b/styles/elements/_icon_link.scss index 00daae3f..d9db1bd3 100644 --- a/styles/elements/_icon_link.scss +++ b/styles/elements/_icon_link.scss @@ -22,6 +22,7 @@ background: none; transition: background-color $hover-transition-time; border-radius: $gap / 2; + cursor: pointer; .icon { @include icon-color($color-primary); diff --git a/styles/elements/_panels.scss b/styles/elements/_panels.scss index 303351fe..61b0acd6 100644 --- a/styles/elements/_panels.scss +++ b/styles/elements/_panels.scss @@ -69,6 +69,11 @@ h1, h2, h3, h4, h5, h6 { margin: 0; + display: inline-block; + } + + .icon-tooltip { + margin-left: $gap*2; } } } diff --git a/templates/project_edit.html.to b/templates/project_edit.html.to index 93768977..58c18bed 100644 --- a/templates/project_edit.html.to +++ b/templates/project_edit.html.to @@ -88,7 +88,7 @@ diff --git a/templates/workspace_project_new.html b/templates/workspace_project_new.html index 98242bed..68a5805f 100644 --- a/templates/workspace_project_new.html +++ b/templates/workspace_project_new.html @@ -39,13 +39,13 @@
  • {{ TextInput(form.environment_name) }} - {{ Icon('x') }} + {{ Icon('x') }} Remove
From 3daf7023021d27184e369542bd3a8790c461751b Mon Sep 17 00:00:00 2001 From: luis cielak Date: Wed, 22 Aug 2018 11:48:15 -0400 Subject: [PATCH 44/45] Begin styling environmnet item --- styles/elements/_icons.scss | 4 ++++ templates/workspace_project_new.html | 9 +++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/styles/elements/_icons.scss b/styles/elements/_icons.scss index 34b35a8c..b0f435fe 100644 --- a/styles/elements/_icons.scss +++ b/styles/elements/_icons.scss @@ -49,4 +49,8 @@ &.icon--large { @include icon-size(24); } + + &.icon--remove { + @include icon-color($color-red); + } } diff --git a/templates/workspace_project_new.html b/templates/workspace_project_new.html index 68a5805f..aac5c7a9 100644 --- a/templates/workspace_project_new.html +++ b/templates/workspace_project_new.html @@ -27,20 +27,17 @@

Project Environments

- {{ 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" )}} -
    -
  • - +
  • {{ TextInput(form.environment_name) }} - - {{ Icon('x') }} Remove + {{ Icon('x') }} Remove +
From 7fc31f64753da2d412c5a2d1dc5afd328cf01b6d Mon Sep 17 00:00:00 2001 From: luis cielak Date: Wed, 22 Aug 2018 13:55:04 -0400 Subject: [PATCH 45/45] Add header grow class for distributed layout --- styles/elements/_panels.scss | 6 ++++++ templates/workspace_project_new.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/styles/elements/_panels.scss b/styles/elements/_panels.scss index 61b0acd6..c6b9a510 100644 --- a/styles/elements/_panels.scss +++ b/styles/elements/_panels.scss @@ -75,6 +75,12 @@ .icon-tooltip { margin-left: $gap*2; } + + &--grow { + display: flex; + flex-direction: row; + justify-content: space-between; + } } } diff --git a/templates/workspace_project_new.html b/templates/workspace_project_new.html index aac5c7a9..85cdf673 100644 --- a/templates/workspace_project_new.html +++ b/templates/workspace_project_new.html @@ -9,7 +9,7 @@
{{ form.csrf_token }}
-
+

Add a new project

{{ Tooltip( "AT-AT allows you to organize your workspace into multiple projects, each of which may have environments.",