workspace -> portfolio everywhere

This commit is contained in:
dandds 2019-01-11 09:58:00 -05:00
parent 3fc323d785
commit d3d36822df
122 changed files with 2156 additions and 2129 deletions

View File

@ -12,7 +12,7 @@ from atst.database import db
from atst.assets import environment as assets_environment from atst.assets import environment as assets_environment
from atst.filters import register_filters from atst.filters import register_filters
from atst.routes import bp from atst.routes import bp
from atst.routes.workspaces import workspaces_bp as workspace_routes from atst.routes.portfolios import portfolios_bp as portfolio_routes
from atst.routes.requests import requests_bp from atst.routes.requests import requests_bp
from atst.routes.task_orders import task_orders_bp from atst.routes.task_orders import task_orders_bp
from atst.routes.dev import bp as dev_routes from atst.routes.dev import bp as dev_routes
@ -63,7 +63,7 @@ def make_app(config):
make_error_pages(app) make_error_pages(app)
app.register_blueprint(bp) app.register_blueprint(bp)
app.register_blueprint(workspace_routes) app.register_blueprint(portfolio_routes)
app.register_blueprint(task_orders_bp) app.register_blueprint(task_orders_bp)
app.register_blueprint(user_routes) app.register_blueprint(user_routes)
app.register_blueprint(requests_bp) app.register_blueprint(requests_bp)

View File

@ -10,9 +10,9 @@ from atst.models.environment_role import EnvironmentRole
class Applications(object): class Applications(object):
@classmethod @classmethod
def create(cls, user, workspace, name, description, environment_names): def create(cls, user, portfolio, name, description, environment_names):
application = Application( application = Application(
workspace=workspace, name=name, description=description portfolio=portfolio, name=name, description=description
) )
db.session.add(application) db.session.add(application)
@ -22,13 +22,13 @@ class Applications(object):
return application return application
@classmethod @classmethod
def get(cls, user, workspace, application_id): def get(cls, user, portfolio, application_id):
# TODO: this should check permission for this particular application # TODO: this should check permission for this particular application
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
user, user,
workspace, portfolio,
Permissions.VIEW_APPLICATION_IN_WORKSPACE, Permissions.VIEW_APPLICATION_IN_WORKSPACE,
"view application in workspace", "view application in portfolio",
) )
try: try:
@ -41,28 +41,28 @@ class Applications(object):
return application return application
@classmethod @classmethod
def for_user(self, user, workspace): def for_user(self, user, portfolio):
return ( return (
db.session.query(Application) db.session.query(Application)
.join(Environment) .join(Environment)
.join(EnvironmentRole) .join(EnvironmentRole)
.filter(Application.workspace_id == workspace.id) .filter(Application.workspace_id == portfolio.id)
.filter(EnvironmentRole.user_id == user.id) .filter(EnvironmentRole.user_id == user.id)
.all() .all()
) )
@classmethod @classmethod
def get_all(cls, user, workspace_role, workspace): def get_all(cls, user, portfolio_role, portfolio):
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
user, user,
workspace, portfolio,
Permissions.VIEW_APPLICATION_IN_WORKSPACE, Permissions.VIEW_APPLICATION_IN_WORKSPACE,
"view application in workspace", "view application in portfolio",
) )
try: try:
applications = ( applications = (
db.session.query(Application).filter_by(workspace_id=workspace.id).all() db.session.query(Application).filter_by(portfolio_id=portfolio.id).all()
) )
except NoResultFound: except NoResultFound:
raise NotFoundError("applications") raise NotFoundError("applications")
@ -70,7 +70,7 @@ class Applications(object):
return applications return applications
@classmethod @classmethod
def update(cls, user, workspace, application, new_data): def update(cls, user, portfolio, application, new_data):
if "name" in new_data: if "name" in new_data:
application.name = new_data["name"] application.name = new_data["name"]
if "description" in new_data: if "description" in new_data:

View File

@ -15,13 +15,13 @@ class AuditEventQuery(Query):
return cls.paginate(query, pagination_opts) return cls.paginate(query, pagination_opts)
@classmethod @classmethod
def get_ws_events(cls, workspace_id, pagination_opts): def get_ws_events(cls, portfolio_id, pagination_opts):
query = ( query = (
db.session.query(cls.model) db.session.query(cls.model)
.filter( .filter(
or_( or_(
cls.model.workspace_id == workspace_id, cls.model.portfolio_id == portfolio_id,
cls.model.resource_id == workspace_id, cls.model.resource_id == portfolio_id,
) )
) )
.order_by(cls.model.time_created.desc()) .order_by(cls.model.time_created.desc())
@ -31,8 +31,8 @@ class AuditEventQuery(Query):
class AuditLog(object): class AuditLog(object):
@classmethod @classmethod
def log_system_event(cls, resource, action, workspace=None): def log_system_event(cls, resource, action, portfolio=None):
return cls._log(resource=resource, action=action, workspace=workspace) return cls._log(resource=resource, action=action, portfolio=portfolio)
@classmethod @classmethod
def get_all_events(cls, user, pagination_opts=None): def get_all_events(cls, user, pagination_opts=None):
@ -42,14 +42,14 @@ class AuditLog(object):
return AuditEventQuery.get_all(pagination_opts) return AuditEventQuery.get_all(pagination_opts)
@classmethod @classmethod
def get_workspace_events(cls, user, workspace, pagination_opts=None): def get_portfolio_events(cls, user, portfolio, pagination_opts=None):
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
user, user,
workspace, portfolio,
Permissions.VIEW_WORKSPACE_AUDIT_LOG, Permissions.VIEW_WORKSPACE_AUDIT_LOG,
"view workspace audit log", "view portfolio audit log",
) )
return AuditEventQuery.get_ws_events(workspace.id, pagination_opts) return AuditEventQuery.get_ws_events(portfolio.id, pagination_opts)
@classmethod @classmethod
def get_by_resource(cls, resource_id): def get_by_resource(cls, resource_id):
@ -65,14 +65,14 @@ class AuditLog(object):
return type(resource).__name__.lower() return type(resource).__name__.lower()
@classmethod @classmethod
def _log(cls, user=None, workspace=None, resource=None, action=None): def _log(cls, user=None, portfolio=None, resource=None, action=None):
resource_id = resource.id if resource else None resource_id = resource.id if resource else None
resource_type = cls._resource_type(resource) if resource else None resource_type = cls._resource_type(resource) if resource else None
workspace_id = workspace.id if workspace else None portfolio_id = portfolio.id if portfolio else None
audit_event = AuditEventQuery.create( audit_event = AuditEventQuery.create(
user=user, user=user,
workspace_id=workspace_id, portfolio_id=portfolio_id,
resource_id=resource_id, resource_id=resource_id,
resource_type=resource_type, resource_type=resource_type,
action=action, action=action,

View File

@ -1,24 +1,24 @@
from atst.domain.workspace_roles import WorkspaceRoles from atst.domain.portfolio_roles import PortfolioRoles
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
class Authorization(object): class Authorization(object):
@classmethod @classmethod
def has_workspace_permission(cls, user, workspace, permission): def has_portfolio_permission(cls, user, portfolio, permission):
return permission in WorkspaceRoles.workspace_role_permissions(workspace, user) return permission in PortfolioRoles.portfolio_role_permissions(portfolio, user)
@classmethod @classmethod
def has_atat_permission(cls, user, permission): def has_atat_permission(cls, user, permission):
return permission in user.atat_role.permissions return permission in user.atat_role.permissions
@classmethod @classmethod
def is_in_workspace(cls, user, workspace): def is_in_portfolio(cls, user, portfolio):
return user in workspace.users return user in portfolio.users
@classmethod @classmethod
def check_workspace_permission(cls, user, workspace, permission, message): def check_portfolio_permission(cls, user, portfolio, permission, message):
if not Authorization.has_workspace_permission(user, workspace, permission): if not Authorization.has_portfolio_permission(user, portfolio, permission):
raise UnauthorizedError(user, message) raise UnauthorizedError(user, message)
@classmethod @classmethod
@ -39,8 +39,8 @@ class Authorization(object):
if Authorization._check_is_task_order_officer(task_order, user): if Authorization._check_is_task_order_officer(task_order, user):
return True return True
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
user, task_order.workspace, permission, message user, task_order.portfolio, permission, message
) )
@classmethod @classmethod

View File

@ -200,17 +200,17 @@ class MockReportingProvider(ReportingInterface):
] ]
) )
def get_budget(self, workspace): def get_budget(self, portfolio):
if workspace.name in self.REPORT_FIXTURE_MAP: if portfolio.name in self.REPORT_FIXTURE_MAP:
return self.REPORT_FIXTURE_MAP[workspace.name]["budget"] return self.REPORT_FIXTURE_MAP[portfolio.name]["budget"]
elif workspace.request and workspace.legacy_task_order: elif portfolio.request and portfolio.legacy_task_order:
return workspace.legacy_task_order.budget return portfolio.legacy_task_order.budget
return 0 return 0
def get_total_spending(self, workspace): def get_total_spending(self, portfolio):
if workspace.name in self.REPORT_FIXTURE_MAP: if portfolio.name in self.REPORT_FIXTURE_MAP:
return self._sum_monthly_spend( return self._sum_monthly_spend(
self.REPORT_FIXTURE_MAP[workspace.name]["applications"] self.REPORT_FIXTURE_MAP[portfolio.name]["applications"]
) )
return 0 return 0
@ -230,17 +230,17 @@ class MockReportingProvider(ReportingInterface):
return application_totals return application_totals
def _rollup_workspace_totals(self, application_totals): def _rollup_portfolio_totals(self, application_totals):
monthly_spend = [ monthly_spend = [
(month, spend) (month, spend)
for application in application_totals.values() for application in application_totals.values()
for month, spend in application.items() for month, spend in application.items()
] ]
workspace_totals = {} portfolio_totals = {}
for month, spends in groupby(sorted(monthly_spend), lambda m: m[0]): for month, spends in groupby(sorted(monthly_spend), lambda m: m[0]):
workspace_totals[month] = sum([spend[1] for spend in spends]) portfolio_totals[month] = sum([spend[1] for spend in spends])
return workspace_totals return portfolio_totals
def monthly_totals_for_environment(self, environment_id): def monthly_totals_for_environment(self, environment_id):
"""Return the monthly totals for the specified environment. """Return the monthly totals for the specified environment.
@ -253,26 +253,26 @@ class MockReportingProvider(ReportingInterface):
""" """
return self.MONTHLY_SPEND_BY_ENVIRONMENT.get(environment_id, {}) return self.MONTHLY_SPEND_BY_ENVIRONMENT.get(environment_id, {})
def monthly_totals(self, workspace): def monthly_totals(self, portfolio):
"""Return month totals rolled up by environment, application, and workspace. """Return month totals rolled up by environment, application, and portfolio.
Data should returned with three top level keys, "workspace", "applications", Data should returned with three top level keys, "portfolio", "applications",
and "environments". and "environments".
The "applications" key will have budget data per month for each application, The "applications" key will have budget data per month for each application,
The "environments" key will have budget data for each environment. The "environments" key will have budget data for each environment.
The "workspace" key will be total monthly spending for the workspace. The "portfolio" key will be total monthly spending for the portfolio.
For example: For example:
{ {
"environments": { "X-Wing": { "Prod": { "01/2018": 75.42 } } }, "environments": { "X-Wing": { "Prod": { "01/2018": 75.42 } } },
"applications": { "X-Wing": { "01/2018": 75.42 } }, "applications": { "X-Wing": { "01/2018": 75.42 } },
"workspace": { "01/2018": 75.42 }, "portfolio": { "01/2018": 75.42 },
} }
""" """
applications = workspace.applications applications = portfolio.applications
if workspace.name in self.REPORT_FIXTURE_MAP: if portfolio.name in self.REPORT_FIXTURE_MAP:
applications = self.REPORT_FIXTURE_MAP[workspace.name]["applications"] applications = self.REPORT_FIXTURE_MAP[portfolio.name]["applications"]
environments = { environments = {
application.name: { application.name: {
env.name: self.monthly_totals_for_environment(env.id) env.name: self.monthly_totals_for_environment(env.id)
@ -282,17 +282,17 @@ class MockReportingProvider(ReportingInterface):
} }
application_totals = self._rollup_application_totals(environments) application_totals = self._rollup_application_totals(environments)
workspace_totals = self._rollup_workspace_totals(application_totals) portfolio_totals = self._rollup_portfolio_totals(application_totals)
return { return {
"environments": environments, "environments": environments,
"applications": application_totals, "applications": application_totals,
"workspace": workspace_totals, "portfolio": portfolio_totals,
} }
def cumulative_budget(self, workspace): def cumulative_budget(self, portfolio):
if workspace.name in self.REPORT_FIXTURE_MAP: if portfolio.name in self.REPORT_FIXTURE_MAP:
budget_months = self.REPORT_FIXTURE_MAP[workspace.name]["cumulative"] budget_months = self.REPORT_FIXTURE_MAP[portfolio.name]["cumulative"]
else: else:
budget_months = {} budget_months = {}

View File

@ -60,10 +60,10 @@ class Environments(object):
return env return env
@classmethod @classmethod
def update_environment_roles(cls, user, workspace, workspace_role, ids_and_roles): def update_environment_roles(cls, user, portfolio, portfolio_role, ids_and_roles):
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
user, user,
workspace, portfolio,
Permissions.ADD_AND_ASSIGN_CSP_ROLES, Permissions.ADD_AND_ASSIGN_CSP_ROLES,
"assign environment roles", "assign environment roles",
) )
@ -75,13 +75,13 @@ class Environments(object):
if new_role is None: if new_role is None:
role_deleted = EnvironmentRoles.delete( role_deleted = EnvironmentRoles.delete(
workspace_role.user.id, environment.id portfolio_role.user.id, environment.id
) )
if role_deleted: if role_deleted:
updated = True updated = True
else: else:
env_role = EnvironmentRoles.get( env_role = EnvironmentRoles.get(
workspace_role.user.id, id_and_role["id"] portfolio_role.user.id, id_and_role["id"]
) )
if env_role and env_role.role != new_role: if env_role and env_role.role != new_role:
env_role.role = new_role env_role.role = new_role
@ -89,7 +89,7 @@ class Environments(object):
db.session.add(env_role) db.session.add(env_role)
elif not env_role: elif not env_role:
env_role = EnvironmentRoles.create( env_role = EnvironmentRoles.create(
user=workspace_role.user, environment=environment, role=new_role user=portfolio_role.user, environment=environment, role=new_role
) )
updated = True updated = True
db.session.add(env_role) db.session.add(env_role)
@ -101,9 +101,9 @@ class Environments(object):
@classmethod @classmethod
def revoke_access(cls, user, environment, target_user): def revoke_access(cls, user, environment, target_user):
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
user, user,
environment.workspace, environment.portfolio,
Permissions.REMOVE_CSP_ROLES, Permissions.REMOVE_CSP_ROLES,
"revoke environment access", "revoke environment access",
) )

View File

@ -3,9 +3,9 @@ from sqlalchemy.orm.exc import NoResultFound
from atst.database import db from atst.database import db
from atst.models.invitation import Invitation, Status as InvitationStatus from atst.models.invitation import Invitation, Status as InvitationStatus
from atst.domain.workspace_roles import WorkspaceRoles from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.authz import Authorization, Permissions from atst.domain.authz import Authorization, Permissions
from atst.domain.workspaces import Workspaces from atst.domain.portfolios import Portfolios
from .exceptions import NotFoundError from .exceptions import NotFoundError
@ -54,11 +54,11 @@ class Invitations(object):
return invite return invite
@classmethod @classmethod
def create(cls, inviter, workspace_role, email): def create(cls, inviter, portfolio_role, email):
invite = Invitation( invite = Invitation(
workspace_role=workspace_role, portfolio_role=portfolio_role,
inviter=inviter, inviter=inviter,
user=workspace_role.user, user=portfolio_role.user,
status=InvitationStatus.PENDING, status=InvitationStatus.PENDING,
expiration_time=Invitations.current_expiration_time(), expiration_time=Invitations.current_expiration_time(),
email=email, email=email,
@ -86,7 +86,7 @@ class Invitations(object):
elif invite.is_pending: # pragma: no branch elif invite.is_pending: # pragma: no branch
Invitations._update_status(invite, InvitationStatus.ACCEPTED) Invitations._update_status(invite, InvitationStatus.ACCEPTED)
WorkspaceRoles.enable(invite.workspace_role) PortfolioRoles.enable(invite.portfolio_role)
return invite return invite
@classmethod @classmethod
@ -109,18 +109,18 @@ class Invitations(object):
return Invitations._update_status(invite, InvitationStatus.REVOKED) return Invitations._update_status(invite, InvitationStatus.REVOKED)
@classmethod @classmethod
def resend(cls, user, workspace_id, token): def resend(cls, user, portfolio_id, token):
workspace = Workspaces.get(user, workspace_id) portfolio = Portfolios.get(user, portfolio_id)
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
user, user,
workspace, portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"resend a workspace invitation", "resend a portfolio invitation",
) )
previous_invitation = Invitations._get(token) previous_invitation = Invitations._get(token)
Invitations._update_status(previous_invitation, InvitationStatus.REVOKED) Invitations._update_status(previous_invitation, InvitationStatus.REVOKED)
return Invitations.create( return Invitations.create(
user, previous_invitation.workspace_role, previous_invitation.email user, previous_invitation.portfolio_role, previous_invitation.email
) )

View File

@ -0,0 +1,167 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models.portfolio_role import (
PortfolioRole,
Status as PortfolioRoleStatus,
MEMBER_STATUSES,
)
from atst.models.user import User
from .roles import Roles
from .users import Users
from .exceptions import NotFoundError
MEMBER_STATUS_CHOICES = [
dict(name=key, display_name=value) for key, value in MEMBER_STATUSES.items()
]
class PortfolioRoles(object):
@classmethod
def get(cls, portfolio_id, user_id):
try:
portfolio_role = (
db.session.query(PortfolioRole)
.join(User)
.filter(User.id == user_id, PortfolioRole.portfolio_id == portfolio_id)
.one()
)
except NoResultFound:
raise NotFoundError("portfolio_role")
return portfolio_role
@classmethod
def get_by_id(cls, id_):
try:
return db.session.query(PortfolioRole).filter(PortfolioRole.id == id_).one()
except NoResultFound:
raise NotFoundError("portfolio_role")
@classmethod
def _get_active_portfolio_role(cls, portfolio_id, user_id):
try:
return (
db.session.query(PortfolioRole)
.join(User)
.filter(User.id == user_id, PortfolioRole.workspace_id == portfolio_id)
.filter(PortfolioRole.status == PortfolioRoleStatus.ACTIVE)
.one()
)
except NoResultFound:
return None
@classmethod
def portfolio_role_permissions(cls, portfolio, user):
portfolio_role = PortfolioRoles._get_active_portfolio_role(
portfolio.id, user.id
)
atat_permissions = set(user.atat_role.permissions)
portfolio_permissions = (
[] if portfolio_role is None else portfolio_role.role.permissions
)
return set(portfolio_permissions).union(atat_permissions)
@classmethod
def _get_portfolio_role(cls, user, portfolio_id):
try:
existing_portfolio_role = (
db.session.query(PortfolioRole)
.filter(
PortfolioRole.user == user,
PortfolioRole.portfolio_id == portfolio_id,
)
.one()
)
return existing_portfolio_role
except NoResultFound:
raise NotFoundError("portfolio role")
@classmethod
def add(cls, user, portfolio_id, role_name):
role = Roles.get(role_name)
new_portfolio_role = None
try:
existing_portfolio_role = (
db.session.query(PortfolioRole)
.filter(
PortfolioRole.user == user,
PortfolioRole.portfolio_id == portfolio_id,
)
.one()
)
new_portfolio_role = existing_portfolio_role
new_portfolio_role.role = role
except NoResultFound:
new_portfolio_role = PortfolioRole(
user=user,
role_id=role.id,
portfolio_id=portfolio_id,
status=PortfolioRoleStatus.PENDING,
)
user.portfolio_roles.append(new_portfolio_role)
db.session.add(user)
db.session.commit()
return new_portfolio_role
@classmethod
def update_role(cls, portfolio_role, role_name):
new_role = Roles.get(role_name)
portfolio_role.role = new_role
db.session.add(portfolio_role)
db.session.commit()
return portfolio_role
@classmethod
def add_many(cls, portfolio_id, portfolio_role_dicts):
portfolio_roles = []
for user_dict in portfolio_role_dicts:
try:
user = Users.get(user_dict["id"])
except NoResultFound:
default_role = Roles.get("developer")
user = User(id=user_dict["id"], atat_role=default_role)
try:
role = Roles.get(user_dict["portfolio_role"])
except NoResultFound:
raise NotFoundError("role")
try:
existing_portfolio_role = (
db.session.query(PortfolioRole)
.filter(
PortfolioRole.user == user,
PortfolioRole.portfolio_id == portfolio_id,
)
.one()
)
new_portfolio_role = existing_portfolio_role
new_portfolio_role.role = role
except NoResultFound:
new_portfolio_role = PortfolioRole(
user=user, role_id=role.id, portfolio_id=portfolio_id
)
user.portfolio_roles.append(new_portfolio_role)
portfolio_roles.append(new_portfolio_role)
db.session.add(user)
db.session.commit()
return portfolio_roles
@classmethod
def enable(cls, portfolio_role):
portfolio_role.status = PortfolioRoleStatus.ACTIVE
db.session.add(portfolio_role)
db.session.commit()

View File

@ -0,0 +1 @@
from .portfolios import Portfolios, PortfolioError

View File

@ -0,0 +1,182 @@
from atst.domain.roles import Roles
from atst.domain.authz import Authorization
from atst.models.permissions import Permissions
from atst.domain.users import Users
from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.environments import Environments
from atst.models.portfolio_role import Status as PortfolioRoleStatus
from .query import PortfoliosQuery
from .scopes import ScopedPortfolio
class PortfolioError(Exception):
pass
class Portfolios(object):
@classmethod
def create(cls, user, name):
portfolio = PortfoliosQuery.create(name=name)
Portfolios._create_portfolio_role(
user, portfolio, "owner", status=PortfolioRoleStatus.ACTIVE
)
PortfoliosQuery.add_and_commit(portfolio)
return portfolio
@classmethod
def create_from_request(cls, request, name=None):
name = name or request.displayname
portfolio = PortfoliosQuery.create(request=request, name=name)
Portfolios._create_portfolio_role(
request.creator, portfolio, "owner", status=PortfolioRoleStatus.ACTIVE
)
PortfoliosQuery.add_and_commit(portfolio)
return portfolio
@classmethod
def get(cls, user, portfolio_id):
portfolio = PortfoliosQuery.get(portfolio_id)
Authorization.check_portfolio_permission(
user, portfolio, Permissions.VIEW_WORKSPACE, "get portfolio"
)
return ScopedPortfolio(user, portfolio)
@classmethod
def get_for_update_applications(cls, user, portfolio_id):
portfolio = PortfoliosQuery.get(portfolio_id)
Authorization.check_portfolio_permission(
user, portfolio, Permissions.ADD_APPLICATION_IN_WORKSPACE, "add application"
)
return portfolio
@classmethod
def get_for_update_information(cls, user, portfolio_id):
portfolio = PortfoliosQuery.get(portfolio_id)
Authorization.check_portfolio_permission(
user,
portfolio,
Permissions.EDIT_WORKSPACE_INFORMATION,
"update portfolio information",
)
return portfolio
@classmethod
def get_for_update_member(cls, user, portfolio_id):
portfolio = PortfoliosQuery.get(portfolio_id)
Authorization.check_portfolio_permission(
user,
portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"update a portfolio member",
)
return portfolio
@classmethod
def get_by_request(cls, request):
return PortfoliosQuery.get_by_request(request)
@classmethod
def get_with_members(cls, user, portfolio_id):
portfolio = PortfoliosQuery.get(portfolio_id)
Authorization.check_portfolio_permission(
user,
portfolio,
Permissions.VIEW_WORKSPACE_MEMBERS,
"view portfolio members",
)
return portfolio
@classmethod
def for_user(cls, user):
if Authorization.has_atat_permission(user, Permissions.VIEW_WORKSPACE):
portfolios = PortfoliosQuery.get_all()
else:
portfolios = PortfoliosQuery.get_for_user(user)
return portfolios
@classmethod
def create_member(cls, user, portfolio, data):
Authorization.check_portfolio_permission(
user,
portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"create portfolio member",
)
new_user = Users.get_or_create_by_dod_id(
data["dod_id"],
first_name=data["first_name"],
last_name=data["last_name"],
email=data["email"],
atat_role_name="default",
provisional=True,
)
return Portfolios.add_member(portfolio, new_user, data["portfolio_role"])
@classmethod
def add_member(cls, portfolio, member, role_name):
portfolio_role = PortfolioRoles.add(member, portfolio.id, role_name)
return portfolio_role
@classmethod
def update_member(cls, user, portfolio, member, role_name):
Authorization.check_portfolio_permission(
user,
portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"edit portfolio member",
)
return PortfolioRoles.update_role(member, role_name)
@classmethod
def _create_portfolio_role(
cls, user, portfolio, role_name, status=PortfolioRoleStatus.PENDING
):
role = Roles.get(role_name)
portfolio_role = PortfoliosQuery.create_portfolio_role(
user, role, portfolio, status=status
)
PortfoliosQuery.add_and_commit(portfolio_role)
return portfolio_role
@classmethod
def update(cls, portfolio, new_data):
if "name" in new_data:
portfolio.name = new_data["name"]
PortfoliosQuery.add_and_commit(portfolio)
@classmethod
def can_revoke_access_for(cls, portfolio, portfolio_role):
return (
portfolio_role.user != portfolio.owner
and portfolio_role.status == PortfolioRoleStatus.ACTIVE
)
@classmethod
def revoke_access(cls, user, portfolio_id, portfolio_role_id):
portfolio = PortfoliosQuery.get(portfolio_id)
Authorization.check_portfolio_permission(
user,
portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"revoke portfolio access",
)
portfolio_role = PortfolioRoles.get_by_id(portfolio_role_id)
if not Portfolios.can_revoke_access_for(portfolio, portfolio_role):
raise PortfolioError("cannot revoke portfolio access for this user")
portfolio_role.status = PortfolioRoleStatus.DISABLED
for environment in portfolio.all_environments:
Environments.revoke_access(user, environment, portfolio_role.user)
PortfoliosQuery.add_and_commit(portfolio_role)
return portfolio_role

View File

@ -0,0 +1,34 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.domain.common import Query
from atst.domain.exceptions import NotFoundError
from atst.models.portfolio import Portfolio
from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
class PortfoliosQuery(Query):
model = Portfolio
@classmethod
def get_by_request(cls, request):
try:
portfolio = db.session.query(Portfolio).filter_by(request=request).one()
except NoResultFound:
raise NotFoundError("portfolio")
return portfolio
@classmethod
def get_for_user(cls, user):
return (
db.session.query(Portfolio)
.join(PortfolioRole)
.filter(PortfolioRole.user == user)
.filter(PortfolioRole.status == PortfolioRoleStatus.ACTIVE)
.all()
)
@classmethod
def create_portfolio_role(cls, user, role, portfolio, **kwargs):
return PortfolioRole(user=user, role=role, portfolio=portfolio, **kwargs)

View File

@ -21,16 +21,16 @@ class ScopedResource(object):
return self.resource == other return self.resource == other
class ScopedWorkspace(ScopedResource): class ScopedPortfolio(ScopedResource):
""" """
An object that obeys the same API as a Workspace, but with the added An object that obeys the same API as a Portfolio, but with the added
functionality that it only returns sub-resources (applications and environments) functionality that it only returns sub-resources (applications and environments)
that the given user is allowed to see. that the given user is allowed to see.
""" """
@property @property
def applications(self): def applications(self):
can_view_all_applications = Authorization.has_workspace_permission( can_view_all_applications = Authorization.has_portfolio_permission(
self.user, self.resource, Permissions.VIEW_APPLICATION_IN_WORKSPACE self.user, self.resource, Permissions.VIEW_APPLICATION_IN_WORKSPACE
) )
@ -46,16 +46,16 @@ class ScopedWorkspace(ScopedResource):
class ScopedApplication(ScopedResource): class ScopedApplication(ScopedResource):
""" """
An object that obeys the same API as a Workspace, but with the added An object that obeys the same API as a Portfolio, but with the added
functionality that it only returns sub-resources (environments) functionality that it only returns sub-resources (environments)
that the given user is allowed to see. that the given user is allowed to see.
""" """
@property @property
def environments(self): def environments(self):
can_view_all_environments = Authorization.has_workspace_permission( can_view_all_environments = Authorization.has_portfolio_permission(
self.user, self.user,
self.resource.workspace, self.resource.portfolio,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION, Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
) )

View File

@ -3,15 +3,15 @@ from flask import current_app
class Reports: class Reports:
@classmethod @classmethod
def workspace_totals(cls, workspace): def portfolio_totals(cls, portfolio):
budget = current_app.csp.reports.get_budget(workspace) budget = current_app.csp.reports.get_budget(portfolio)
spent = current_app.csp.reports.get_total_spending(workspace) spent = current_app.csp.reports.get_total_spending(portfolio)
return {"budget": budget, "spent": spent} return {"budget": budget, "spent": spent}
@classmethod @classmethod
def monthly_totals(cls, workspace): def monthly_totals(cls, portfolio):
return current_app.csp.reports.monthly_totals(workspace) return current_app.csp.reports.monthly_totals(portfolio)
@classmethod @classmethod
def cumulative_budget(cls, workspace): def cumulative_budget(cls, portfolio):
return current_app.csp.reports.cumulative_budget(workspace) return current_app.csp.reports.cumulative_budget(portfolio)

View File

@ -1,6 +1,6 @@
import dateutil import dateutil
from atst.domain.workspaces import Workspaces from atst.domain.portfolios import Portfolios
from atst.models.request_revision import RequestRevision from atst.models.request_revision import RequestRevision
from atst.models.request_status_event import RequestStatusEvent, RequestStatus from atst.models.request_status_event import RequestStatusEvent, RequestStatus
from atst.models.request_review import RequestReview from atst.models.request_review import RequestReview
@ -99,25 +99,25 @@ class Requests(object):
return RequestsQuery.add_and_commit(request) return RequestsQuery.add_and_commit(request)
@classmethod @classmethod
def approve_and_create_workspace(cls, request): def approve_and_create_portfolio(cls, request):
approved_request = Requests.set_status(request, RequestStatus.APPROVED) approved_request = Requests.set_status(request, RequestStatus.APPROVED)
workspace = Workspaces.create_from_request(approved_request) portfolio = Portfolios.create_from_request(approved_request)
RequestsQuery.add_and_commit(approved_request) RequestsQuery.add_and_commit(approved_request)
return workspace return portfolio
@classmethod @classmethod
def auto_approve_and_create_workspace( def auto_approve_and_create_portfolio(
cls, cls,
request, request,
reason="Financial verification information found in Electronic Document Access API", reason="Financial verification information found in Electronic Document Access API",
): ):
workspace = Requests.approve_and_create_workspace(request) portfolio = Requests.approve_and_create_portfolio(request)
Requests._add_review( Requests._add_review(
user=None, request=request, review_data={"comment": reason} user=None, request=request, review_data={"comment": reason}
) )
return workspace return portfolio
@classmethod @classmethod
def set_status(cls, request, status: RequestStatus): def set_status(cls, request, status: RequestStatus):
@ -214,7 +214,7 @@ class Requests(object):
if request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE: if request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE:
Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION) Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION)
elif request.status == RequestStatus.PENDING_CCPO_APPROVAL: elif request.status == RequestStatus.PENDING_CCPO_APPROVAL:
Requests.approve_and_create_workspace(request) Requests.approve_and_create_portfolio(request)
return Requests._add_review(user=user, request=request, review_data=review_data) return Requests._add_review(user=user, request=request, review_data=review_data)

View File

@ -57,7 +57,7 @@ ATAT_ROLES = [
WORKSPACE_ROLES = [ WORKSPACE_ROLES = [
{ {
"name": "owner", "name": "owner",
"display_name": "Workspace Owner", "display_name": "Portfolio Owner",
"description": "Adds, edits, deactivates access to all applications, environments, and members. Views budget reports. Initiates and edits JEDI Cloud requests.", "description": "Adds, edits, deactivates access to all applications, environments, and members. Views budget reports. Initiates and edits JEDI Cloud requests.",
"permissions": [ "permissions": [
Permissions.REQUEST_JEDI_WORKSPACE, Permissions.REQUEST_JEDI_WORKSPACE,
@ -131,7 +131,7 @@ WORKSPACE_ROLES = [
{ {
"name": "billing_auditor", "name": "billing_auditor",
"display_name": "Billing Auditor", "display_name": "Billing Auditor",
"description": "Views only the applications and environments they are granted access to. Can also view budgets and reports associated with the workspace.", "description": "Views only the applications and environments they are granted access to. Can also view budgets and reports associated with the portfolio.",
"permissions": [ "permissions": [
Permissions.VIEW_USAGE_REPORT, Permissions.VIEW_USAGE_REPORT,
Permissions.VIEW_USAGE_DOLLARS, Permissions.VIEW_USAGE_DOLLARS,

View File

@ -3,7 +3,7 @@ from sqlalchemy.orm.exc import NoResultFound
from atst.database import db from atst.database import db
from atst.models.task_order import TaskOrder from atst.models.task_order import TaskOrder
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.domain.workspaces import Workspaces from atst.domain.portfolios import Portfolios
from atst.domain.authz import Authorization from atst.domain.authz import Authorization
from .exceptions import NotFoundError from .exceptions import NotFoundError
@ -63,11 +63,11 @@ class TaskOrders(object):
raise NotFoundError("task_order") raise NotFoundError("task_order")
@classmethod @classmethod
def create(cls, creator, workspace): def create(cls, creator, portfolio):
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
creator, workspace, Permissions.UPDATE_TASK_ORDER, "add task order" creator, portfolio, Permissions.UPDATE_TASK_ORDER, "add task order"
) )
task_order = TaskOrder(workspace=workspace, creator=creator) task_order = TaskOrder(portfolio=portfolio, creator=creator)
db.session.add(task_order) db.session.add(task_order)
db.session.commit() db.session.commit()
@ -116,39 +116,39 @@ class TaskOrders(object):
@classmethod @classmethod
def add_officer(cls, user, task_order, officer_type, officer_data): def add_officer(cls, user, task_order, officer_type, officer_data):
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
user, user,
task_order.workspace, task_order.portfolio,
Permissions.ADD_TASK_ORDER_OFFICER, Permissions.ADD_TASK_ORDER_OFFICER,
"add task order officer", "add task order officer",
) )
if officer_type in TaskOrders.OFFICERS: if officer_type in TaskOrders.OFFICERS:
workspace = task_order.workspace portfolio = task_order.portfolio
existing_member = next( existing_member = next(
( (
member member
for member in workspace.members for member in portfolio.members
if member.user.dod_id == officer_data["dod_id"] if member.user.dod_id == officer_data["dod_id"]
), ),
None, None,
) )
if existing_member: if existing_member:
workspace_user = existing_member.user portfolio_user = existing_member.user
else: else:
member = Workspaces.create_member( member = Portfolios.create_member(
user, workspace, {**officer_data, "workspace_role": "officer"} user, portfolio, {**officer_data, "portfolio_role": "officer"}
) )
workspace_user = member.user portfolio_user = member.user
setattr(task_order, officer_type, workspace_user) setattr(task_order, officer_type, portfolio_user)
db.session.add(task_order) db.session.add(task_order)
db.session.commit() db.session.commit()
return workspace_user return portfolio_user
else: else:
raise TaskOrderError( raise TaskOrderError(
"{} is not an officer role on task orders".format(officer_type) "{} is not an officer role on task orders".format(officer_type)

View File

@ -1,167 +0,0 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models.workspace_role import (
WorkspaceRole,
Status as WorkspaceRoleStatus,
MEMBER_STATUSES,
)
from atst.models.user import User
from .roles import Roles
from .users import Users
from .exceptions import NotFoundError
MEMBER_STATUS_CHOICES = [
dict(name=key, display_name=value) for key, value in MEMBER_STATUSES.items()
]
class WorkspaceRoles(object):
@classmethod
def get(cls, workspace_id, user_id):
try:
workspace_role = (
db.session.query(WorkspaceRole)
.join(User)
.filter(User.id == user_id, WorkspaceRole.workspace_id == workspace_id)
.one()
)
except NoResultFound:
raise NotFoundError("workspace_role")
return workspace_role
@classmethod
def get_by_id(cls, id_):
try:
return db.session.query(WorkspaceRole).filter(WorkspaceRole.id == id_).one()
except NoResultFound:
raise NotFoundError("workspace_role")
@classmethod
def _get_active_workspace_role(cls, workspace_id, user_id):
try:
return (
db.session.query(WorkspaceRole)
.join(User)
.filter(User.id == user_id, WorkspaceRole.workspace_id == workspace_id)
.filter(WorkspaceRole.status == WorkspaceRoleStatus.ACTIVE)
.one()
)
except NoResultFound:
return None
@classmethod
def workspace_role_permissions(cls, workspace, user):
workspace_role = WorkspaceRoles._get_active_workspace_role(
workspace.id, user.id
)
atat_permissions = set(user.atat_role.permissions)
workspace_permissions = (
[] if workspace_role is None else workspace_role.role.permissions
)
return set(workspace_permissions).union(atat_permissions)
@classmethod
def _get_workspace_role(cls, user, workspace_id):
try:
existing_workspace_role = (
db.session.query(WorkspaceRole)
.filter(
WorkspaceRole.user == user,
WorkspaceRole.workspace_id == workspace_id,
)
.one()
)
return existing_workspace_role
except NoResultFound:
raise NotFoundError("workspace role")
@classmethod
def add(cls, user, workspace_id, role_name):
role = Roles.get(role_name)
new_workspace_role = None
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,
status=WorkspaceRoleStatus.PENDING,
)
user.workspace_roles.append(new_workspace_role)
db.session.add(user)
db.session.commit()
return new_workspace_role
@classmethod
def update_role(cls, workspace_role, role_name):
new_role = Roles.get(role_name)
workspace_role.role = new_role
db.session.add(workspace_role)
db.session.commit()
return workspace_role
@classmethod
def add_many(cls, workspace_id, workspace_role_dicts):
workspace_roles = []
for user_dict in workspace_role_dicts:
try:
user = Users.get(user_dict["id"])
except NoResultFound:
default_role = Roles.get("developer")
user = User(id=user_dict["id"], atat_role=default_role)
try:
role = Roles.get(user_dict["workspace_role"])
except NoResultFound:
raise NotFoundError("role")
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)
workspace_roles.append(new_workspace_role)
db.session.add(user)
db.session.commit()
return workspace_roles
@classmethod
def enable(cls, workspace_role):
workspace_role.status = WorkspaceRoleStatus.ACTIVE
db.session.add(workspace_role)
db.session.commit()

View File

@ -1 +0,0 @@
from .workspaces import Workspaces, WorkspaceError

View File

@ -1,34 +0,0 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.domain.common import Query
from atst.domain.exceptions import NotFoundError
from atst.models.workspace import Workspace
from atst.models.workspace_role import WorkspaceRole, Status as WorkspaceRoleStatus
class WorkspacesQuery(Query):
model = Workspace
@classmethod
def get_by_request(cls, request):
try:
workspace = db.session.query(Workspace).filter_by(request=request).one()
except NoResultFound:
raise NotFoundError("workspace")
return workspace
@classmethod
def get_for_user(cls, user):
return (
db.session.query(Workspace)
.join(WorkspaceRole)
.filter(WorkspaceRole.user == user)
.filter(WorkspaceRole.status == WorkspaceRoleStatus.ACTIVE)
.all()
)
@classmethod
def create_workspace_role(cls, user, role, workspace, **kwargs):
return WorkspaceRole(user=user, role=role, workspace=workspace, **kwargs)

View File

@ -1,182 +0,0 @@
from atst.domain.roles import Roles
from atst.domain.authz import Authorization
from atst.models.permissions import Permissions
from atst.domain.users import Users
from atst.domain.workspace_roles import WorkspaceRoles
from atst.domain.environments import Environments
from atst.models.workspace_role import Status as WorkspaceRoleStatus
from .query import WorkspacesQuery
from .scopes import ScopedWorkspace
class WorkspaceError(Exception):
pass
class Workspaces(object):
@classmethod
def create(cls, user, name):
workspace = WorkspacesQuery.create(name=name)
Workspaces._create_workspace_role(
user, workspace, "owner", status=WorkspaceRoleStatus.ACTIVE
)
WorkspacesQuery.add_and_commit(workspace)
return workspace
@classmethod
def create_from_request(cls, request, name=None):
name = name or request.displayname
workspace = WorkspacesQuery.create(request=request, name=name)
Workspaces._create_workspace_role(
request.creator, workspace, "owner", status=WorkspaceRoleStatus.ACTIVE
)
WorkspacesQuery.add_and_commit(workspace)
return workspace
@classmethod
def get(cls, user, workspace_id):
workspace = WorkspacesQuery.get(workspace_id)
Authorization.check_workspace_permission(
user, workspace, Permissions.VIEW_WORKSPACE, "get workspace"
)
return ScopedWorkspace(user, workspace)
@classmethod
def get_for_update_applications(cls, user, workspace_id):
workspace = WorkspacesQuery.get(workspace_id)
Authorization.check_workspace_permission(
user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE, "add application"
)
return workspace
@classmethod
def get_for_update_information(cls, user, workspace_id):
workspace = WorkspacesQuery.get(workspace_id)
Authorization.check_workspace_permission(
user,
workspace,
Permissions.EDIT_WORKSPACE_INFORMATION,
"update workspace information",
)
return workspace
@classmethod
def get_for_update_member(cls, user, workspace_id):
workspace = WorkspacesQuery.get(workspace_id)
Authorization.check_workspace_permission(
user,
workspace,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"update a workspace member",
)
return workspace
@classmethod
def get_by_request(cls, request):
return WorkspacesQuery.get_by_request(request)
@classmethod
def get_with_members(cls, user, workspace_id):
workspace = WorkspacesQuery.get(workspace_id)
Authorization.check_workspace_permission(
user,
workspace,
Permissions.VIEW_WORKSPACE_MEMBERS,
"view workspace members",
)
return workspace
@classmethod
def for_user(cls, user):
if Authorization.has_atat_permission(user, Permissions.VIEW_WORKSPACE):
workspaces = WorkspacesQuery.get_all()
else:
workspaces = WorkspacesQuery.get_for_user(user)
return workspaces
@classmethod
def create_member(cls, user, workspace, data):
Authorization.check_workspace_permission(
user,
workspace,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"create workspace member",
)
new_user = Users.get_or_create_by_dod_id(
data["dod_id"],
first_name=data["first_name"],
last_name=data["last_name"],
email=data["email"],
atat_role_name="default",
provisional=True,
)
return Workspaces.add_member(workspace, new_user, data["workspace_role"])
@classmethod
def add_member(cls, workspace, member, role_name):
workspace_role = WorkspaceRoles.add(member, workspace.id, role_name)
return workspace_role
@classmethod
def update_member(cls, user, workspace, member, role_name):
Authorization.check_workspace_permission(
user,
workspace,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"edit workspace member",
)
return WorkspaceRoles.update_role(member, role_name)
@classmethod
def _create_workspace_role(
cls, user, workspace, role_name, status=WorkspaceRoleStatus.PENDING
):
role = Roles.get(role_name)
workspace_role = WorkspacesQuery.create_workspace_role(
user, role, workspace, status=status
)
WorkspacesQuery.add_and_commit(workspace_role)
return workspace_role
@classmethod
def update(cls, workspace, new_data):
if "name" in new_data:
workspace.name = new_data["name"]
WorkspacesQuery.add_and_commit(workspace)
@classmethod
def can_revoke_access_for(cls, workspace, workspace_role):
return (
workspace_role.user != workspace.owner
and workspace_role.status == WorkspaceRoleStatus.ACTIVE
)
@classmethod
def revoke_access(cls, user, workspace_id, workspace_role_id):
workspace = WorkspacesQuery.get(workspace_id)
Authorization.check_workspace_permission(
user,
workspace,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"revoke workspace access",
)
workspace_role = WorkspaceRoles.get_by_id(workspace_role_id)
if not Workspaces.can_revoke_access_for(workspace, workspace_role):
raise WorkspaceError("cannot revoke workspace access for this user")
workspace_role.status = WorkspaceRoleStatus.DISABLED
for environment in workspace.all_environments:
Environments.revoke_access(user, environment, workspace_role.user)
WorkspacesQuery.add_and_commit(workspace_role)
return workspace_role

View File

@ -162,7 +162,7 @@ ENVIRONMENT_ROLES = [
ENV_ROLE_MODAL_DESCRIPTION = { ENV_ROLE_MODAL_DESCRIPTION = {
"header": "Assign Environment Role", "header": "Assign Environment Role",
"body": "An environment role determines the permissions a member of the workspace assumes when using the JEDI Cloud.<br/><br/>A member may have different environment roles across different applications. A member can only have one assigned environment role in a given environment.", "body": "An environment role determines the permissions a member of the portfolio assumes when using the JEDI Cloud.<br/><br/>A member may have different environment roles across different applications. A member can only have one assigned environment role in a given environment.",
} }
FUNDING_TYPES = [ FUNDING_TYPES = [

View File

@ -11,8 +11,8 @@ class EditMemberForm(FlaskForm):
# This form also accepts a field for each environment in each application # This form also accepts a field for each environment in each application
# that the user is a member of # that the user is a member of
workspace_role = SelectField( portfolio_role = SelectField(
translate("forms.edit_member.workspace_role_label"), translate("forms.edit_member.portfolio_role_label"),
choices=WORKSPACE_ROLES, choices=WORKSPACE_ROLES,
validators=[Required()], validators=[Required()],
) )

View File

@ -25,10 +25,10 @@ class NewMemberForm(FlaskForm):
translate("forms.new_member.dod_id_label"), translate("forms.new_member.dod_id_label"),
validators=[Required(), Length(min=10), IsNumber()], validators=[Required(), Length(min=10), IsNumber()],
) )
workspace_role = SelectField( portfolio_role = SelectField(
translate("forms.new_member.workspace_role_label"), translate("forms.new_member.portfolio_role_label"),
choices=WORKSPACE_ROLES, choices=WORKSPACE_ROLES,
validators=[Required()], validators=[Required()],
default="", default="",
description=translate("forms.new_member.workspace_role_description"), description=translate("forms.new_member.portfolio_role_description"),
) )

View File

@ -181,7 +181,7 @@ class InformationAboutYouForm(CacheableForm):
date_latest_training = inherit_field(USER_FIELDS["date_latest_training"]) date_latest_training = inherit_field(USER_FIELDS["date_latest_training"])
class WorkspaceOwnerForm(CacheableForm): class PortfolioOwnerForm(CacheableForm):
def validate(self, *args, **kwargs): def validate(self, *args, **kwargs):
if self.am_poc.data: if self.am_poc.data:
# Prepend Optional validators so that the validation chain # Prepend Optional validators so that the validation chain

View File

@ -5,14 +5,14 @@ from .forms import CacheableForm
from atst.utils.localization import translate from atst.utils.localization import translate
class WorkspaceForm(CacheableForm): class PortfolioForm(CacheableForm):
name = StringField( name = StringField(
translate("forms.workspace.name_label"), translate("forms.portfolio.name_label"),
validators=[ validators=[
Length( Length(
min=4, min=4,
max=100, max=100,
message=translate("forms.workspace.name_length_validation_message"), message=translate("forms.portfolio.name_length_validation_message"),
) )
], ],
) )

View File

@ -7,10 +7,10 @@ from .request_status_event import RequestStatusEvent
from .permissions import Permissions from .permissions import Permissions
from .role import Role from .role import Role
from .user import User from .user import User
from .workspace_role import WorkspaceRole from .portfolio_role import PortfolioRole
from .pe_number import PENumber from .pe_number import PENumber
from .legacy_task_order import LegacyTaskOrder from .legacy_task_order import LegacyTaskOrder
from .workspace import Workspace from .portfolio import Portfolio
from .application import Application from .application import Application
from .environment import Environment from .environment import Environment
from .attachment import Attachment from .attachment import Attachment

View File

@ -14,7 +14,7 @@ class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
description = Column(String, nullable=False) description = Column(String, nullable=False)
workspace_id = Column(ForeignKey("workspaces.id"), nullable=False) workspace_id = Column(ForeignKey("workspaces.id"), nullable=False)
workspace = relationship("Workspace") portfolio = relationship("Portfolio")
environments = relationship("Environment", back_populates="application") environments = relationship("Environment", back_populates="application")
@property @property
@ -22,6 +22,6 @@ class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
return self.name return self.name
def __repr__(self): # pragma: no cover def __repr__(self): # pragma: no cover
return "<Application(name='{}', description='{}', workspace='{}', id='{}')>".format( return "<Application(name='{}', description='{}', portfolio='{}', id='{}')>".format(
self.name, self.description, self.workspace.name, self.id self.name, self.description, self.portfolio.name, self.id
) )

View File

@ -15,7 +15,7 @@ class AuditEvent(Base, TimestampsMixin):
user = relationship("User", backref="audit_events") user = relationship("User", backref="audit_events")
workspace_id = Column(UUID(as_uuid=True), ForeignKey("workspaces.id"), index=True) workspace_id = Column(UUID(as_uuid=True), ForeignKey("workspaces.id"), index=True)
workspace = relationship("Workspace", backref="audit_events") portfolio = relationship("Portfolio", backref="audit_events")
request_id = Column(UUID(as_uuid=True), ForeignKey("requests.id"), index=True) request_id = Column(UUID(as_uuid=True), ForeignKey("requests.id"), index=True)
request = relationship("Request", backref="audit_events") request = relationship("Request", backref="audit_events")

View File

@ -30,17 +30,17 @@ class Environment(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
return self.name return self.name
@property @property
def workspace(self): def portfolio(self):
return self.application.workspace return self.application.portfolio
def auditable_workspace_id(self): def auditable_portfolio_id(self):
return self.application.workspace_id return self.application.workspace_id
def __repr__(self): def __repr__(self):
return "<Environment(name='{}', num_users='{}', application='{}', workspace='{}', id='{}')>".format( return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
self.name, self.name,
self.num_users, self.num_users,
self.application.name, self.application.name,
self.application.workspace.name, self.application.portfolio.name,
self.id, self.id,
) )

View File

@ -47,8 +47,8 @@ class EnvironmentRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
"environment_id": str(self.environment_id), "environment_id": str(self.environment_id),
"application": self.environment.application.name, "application": self.environment.application.name,
"application_id": str(self.environment.project_id), "application_id": str(self.environment.project_id),
"workspace": self.environment.application.workspace.name, "portfolio": self.environment.application.portfolio.name,
"workspace_id": str(self.environment.application.workspace.id), "portfolio_id": str(self.environment.application.portfolio.id),
} }

View File

@ -30,8 +30,8 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
workspace_role_id = Column( workspace_role_id = Column(
UUID(as_uuid=True), ForeignKey("workspace_roles.id"), index=True UUID(as_uuid=True), ForeignKey("workspace_roles.id"), index=True
) )
workspace_role = relationship( portfolio_role = relationship(
"WorkspaceRole", "PortfolioRole",
backref=backref("invitations", order_by="Invitation.time_created"), backref=backref("invitations", order_by="Invitation.time_created"),
) )
@ -47,8 +47,8 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
email = Column(String, nullable=False) email = Column(String, nullable=False)
def __repr__(self): def __repr__(self):
return "<Invitation(user='{}', workspace_role='{}', id='{}', email='{}')>".format( return "<Invitation(user='{}', portfolio_role='{}', id='{}', email='{}')>".format(
self.user_id, self.workspace_role_id, self.id, self.email self.user_id, self.portfolio_role_id, self.id, self.email
) )
@property @property
@ -91,13 +91,13 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
] ]
@property @property
def workspace(self): def portfolio(self):
if self.workspace_role: # pragma: no branch if self.portfolio_role: # pragma: no branch
return self.workspace_role.workspace return self.portfolio_role.portfolio
@property @property
def user_name(self): def user_name(self):
return self.workspace_role.user.full_name return self.portfolio_role.user.full_name
@property @property
def is_revokable(self): def is_revokable(self):
@ -122,5 +122,5 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
return change_set return change_set
@property @property
def workspace_id(self): def portfolio_id(self):
return self.workspace_role.workspace_id return self.portfolio_role.workspace_id

View File

@ -13,7 +13,7 @@ class AuditableMixin(object):
@staticmethod @staticmethod
def create_audit_event(connection, resource, action): def create_audit_event(connection, resource, action):
user_id = getattr_path(g, "current_user.id") user_id = getattr_path(g, "current_user.id")
workspace_id = resource.workspace_id portfolio_id = resource.workspace_id
request_id = resource.request_id request_id = resource.request_id
resource_type = resource.resource_type resource_type = resource.resource_type
display_name = resource.displayname display_name = resource.displayname
@ -23,7 +23,7 @@ class AuditableMixin(object):
audit_event = AuditEvent( audit_event = AuditEvent(
user_id=user_id, user_id=user_id,
workspace_id=workspace_id, workspace_id=portfolio_id,
request_id=request_id, request_id=request_id,
resource_type=resource_type, resource_type=resource_type,
resource_id=resource.id, resource_id=resource.id,

View File

@ -1,10 +1,10 @@
class Permissions(object): class Permissions(object):
VIEW_AUDIT_LOG = "view_audit_log" VIEW_AUDIT_LOG = "view_audit_log"
VIEW_WORKSPACE_AUDIT_LOG = "view_workspace_audit_log" VIEW_WORKSPACE_AUDIT_LOG = "view_portfolio_audit_log"
REQUEST_JEDI_WORKSPACE = "request_jedi_workspace" REQUEST_JEDI_WORKSPACE = "request_jedi_portfolio"
VIEW_ORIGINAL_JEDI_REQEUST = "view_original_jedi_request" VIEW_ORIGINAL_JEDI_REQEUST = "view_original_jedi_request"
REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST = ( REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST = (
"review_and_approve_jedi_workspace_request" "review_and_approve_jedi_portfolio_request"
) )
MODIFY_ATAT_ROLE_PERMISSIONS = "modify_atat_role_permissions" MODIFY_ATAT_ROLE_PERMISSIONS = "modify_atat_role_permissions"
CREATE_CSP_ROLE = "create_csp_role" CREATE_CSP_ROLE = "create_csp_role"
@ -22,18 +22,18 @@ class Permissions(object):
VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS = "view_assigned_atat_role_configurations" VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS = "view_assigned_atat_role_configurations"
VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS = "view_assigned_csp_role_configurations" VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS = "view_assigned_csp_role_configurations"
EDIT_WORKSPACE_INFORMATION = "edit_workspace_information" EDIT_WORKSPACE_INFORMATION = "edit_portfolio_information"
DEACTIVATE_WORKSPACE = "deactivate_workspace" DEACTIVATE_WORKSPACE = "deactivate_portfolio"
VIEW_ATAT_PERMISSIONS = "view_atat_permissions" VIEW_ATAT_PERMISSIONS = "view_atat_permissions"
TRANSFER_OWNERSHIP_OF_WORKSPACE = "transfer_ownership_of_workspace" TRANSFER_OWNERSHIP_OF_WORKSPACE = "transfer_ownership_of_portfolio"
VIEW_WORKSPACE_MEMBERS = "view_workspace_members" VIEW_WORKSPACE_MEMBERS = "view_portfolio_members"
VIEW_WORKSPACE = "view_workspace" VIEW_WORKSPACE = "view_portfolio"
ADD_APPLICATION_IN_WORKSPACE = "add_application_in_workspace" ADD_APPLICATION_IN_WORKSPACE = "add_application_in_portfolio"
DELETE_APPLICATION_IN_WORKSPACE = "delete_application_in_workspace" DELETE_APPLICATION_IN_WORKSPACE = "delete_application_in_portfolio"
DEACTIVATE_APPLICATION_IN_WORKSPACE = "deactivate_application_in_workspace" DEACTIVATE_APPLICATION_IN_WORKSPACE = "deactivate_application_in_portfolio"
VIEW_APPLICATION_IN_WORKSPACE = "view_application_in_workspace" VIEW_APPLICATION_IN_WORKSPACE = "view_application_in_portfolio"
RENAME_APPLICATION_IN_WORKSPACE = "rename_application_in_workspace" RENAME_APPLICATION_IN_WORKSPACE = "rename_application_in_portfolio"
ADD_ENVIRONMENT_IN_APPLICATION = "add_environment_in_application" ADD_ENVIRONMENT_IN_APPLICATION = "add_environment_in_application"
DELETE_ENVIRONMENT_IN_APPLICATION = "delete_environment_in_application" DELETE_ENVIRONMENT_IN_APPLICATION = "delete_environment_in_application"
@ -41,8 +41,8 @@ class Permissions(object):
VIEW_ENVIRONMENT_IN_APPLICATION = "view_environment_in_application" VIEW_ENVIRONMENT_IN_APPLICATION = "view_environment_in_application"
RENAME_ENVIRONMENT_IN_APPLICATION = "rename_environment_in_application" RENAME_ENVIRONMENT_IN_APPLICATION = "rename_environment_in_application"
ADD_TAG_TO_WORKSPACE = "add_tag_to_workspace" ADD_TAG_TO_WORKSPACE = "add_tag_to_portfolio"
REMOVE_TAG_FROM_WORKSPACE = "remove_tag_from_workspace" REMOVE_TAG_FROM_WORKSPACE = "remove_tag_from_portfolio"
VIEW_TASK_ORDER = "view_task_order" VIEW_TASK_ORDER = "view_task_order"
UPDATE_TASK_ORDER = "update_task_order" UPDATE_TASK_ORDER = "update_task_order"

View File

@ -3,28 +3,28 @@ from sqlalchemy.orm import relationship
from itertools import chain from itertools import chain
from atst.models import Base, mixins, types from atst.models import Base, mixins, types
from atst.models.workspace_role import WorkspaceRole, Status as WorkspaceRoleStatus from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
from atst.utils import first_or_none from atst.utils import first_or_none
from atst.database import db from atst.database import db
class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin): class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
__tablename__ = "workspaces" __tablename__ = "workspaces"
id = types.Id() id = types.Id()
name = Column(String) name = Column(String)
request_id = Column(ForeignKey("requests.id"), nullable=True) request_id = Column(ForeignKey("requests.id"), nullable=True)
applications = relationship("Application", back_populates="workspace") applications = relationship("Application", back_populates="portfolio")
roles = relationship("WorkspaceRole") roles = relationship("PortfolioRole")
task_orders = relationship("TaskOrder") task_orders = relationship("TaskOrder")
@property @property
def owner(self): def owner(self):
def _is_workspace_owner(workspace_role): def _is_portfolio_owner(portfolio_role):
return workspace_role.role.name == "owner" return portfolio_role.role.name == "owner"
owner = first_or_none(_is_workspace_owner, self.roles) owner = first_or_none(_is_portfolio_owner, self.roles)
return owner.user if owner else None return owner.user if owner else None
@property @property
@ -42,9 +42,9 @@ class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
@property @property
def members(self): def members(self):
return ( return (
db.session.query(WorkspaceRole) db.session.query(PortfolioRole)
.filter(WorkspaceRole.workspace_id == self.id) .filter(PortfolioRole.portfolio_id == self.id)
.filter(WorkspaceRole.status != WorkspaceRoleStatus.DISABLED) .filter(PortfolioRole.status != PortfolioRoleStatus.DISABLED)
.all() .all()
) )
@ -56,10 +56,10 @@ class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
def all_environments(self): def all_environments(self):
return list(chain.from_iterable(p.environments for p in self.applications)) return list(chain.from_iterable(p.environments for p in self.applications))
def auditable_workspace_id(self): def auditable_portfolio_id(self):
return self.id return self.id
def __repr__(self): def __repr__(self):
return "<Workspace(name='{}', request='{}', user_count='{}', id='{}')>".format( return "<Portfolio(name='{}', request='{}', user_count='{}', id='{}')>".format(
self.name, self.request_id, self.user_count, self.id self.name, self.request_id, self.user_count, self.id
) )

View File

@ -30,14 +30,14 @@ class Status(Enum):
PENDING = "pending" PENDING = "pending"
class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin): class PortfolioRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
__tablename__ = "workspace_roles" __tablename__ = "workspace_roles"
id = Id() id = Id()
workspace_id = Column( workspace_id = Column(
UUID(as_uuid=True), ForeignKey("workspaces.id"), index=True, nullable=False UUID(as_uuid=True), ForeignKey("workspaces.id"), index=True, nullable=False
) )
workspace = relationship("Workspace", back_populates="roles") portfolio = relationship("Portfolio", back_populates="roles")
role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"), nullable=False) role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"), nullable=False)
role = relationship("Role") role = relationship("Role")
@ -49,8 +49,8 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING)
def __repr__(self): def __repr__(self):
return "<WorkspaceRole(role='{}', workspace='{}', user_id='{}', id='{}')>".format( return "<PortfolioRole(role='{}', portfolio='{}', user_id='{}', id='{}')>".format(
self.role.name, self.workspace.name, self.user_id, self.id self.role.name, self.portfolio.name, self.user_id, self.id
) )
@property @property
@ -127,8 +127,8 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
db.session.query(EnvironmentRole) db.session.query(EnvironmentRole)
.join(EnvironmentRole.environment) .join(EnvironmentRole.environment)
.join(Environment.application) .join(Environment.application)
.join(Application.workspace) .join(Application.portfolio)
.filter(Application.workspace_id == self.workspace_id) .filter(Application.portfolio_id == self.portfolio_id)
.filter(EnvironmentRole.user_id == self.user_id) .filter(EnvironmentRole.user_id == self.user_id)
.count() .count()
) )
@ -139,8 +139,8 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
db.session.query(EnvironmentRole) db.session.query(EnvironmentRole)
.join(EnvironmentRole.environment) .join(EnvironmentRole.environment)
.join(Environment.application) .join(Environment.application)
.join(Application.workspace) .join(Application.portfolio)
.filter(Application.workspace_id == self.workspace_id) .filter(Application.portfolio_id == self.portfolio_id)
.filter(EnvironmentRole.user_id == self.user_id) .filter(EnvironmentRole.user_id == self.user_id)
.all() .all()
) )
@ -157,8 +157,8 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
Index( Index(
"workspace_role_user_workspace", "portfolio_role_user_portfolio",
WorkspaceRole.user_id, PortfolioRole.user_id,
WorkspaceRole.workspace_id, PortfolioRole.workspace_id,
unique=True, unique=True,
) )

View File

@ -34,7 +34,7 @@ class Request(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
"RequestStatusEvent", backref="request", order_by="RequestStatusEvent.sequence" "RequestStatusEvent", backref="request", order_by="RequestStatusEvent.sequence"
) )
workspace = relationship("Workspace", uselist=False, backref="request") portfolio = relationship("Portfolio", uselist=False, backref="request")
user_id = Column(ForeignKey("users.id"), nullable=False) user_id = Column(ForeignKey("users.id"), nullable=False)
creator = relationship("User", backref="owned_requests") creator = relationship("User", backref="owned_requests")

View File

@ -25,7 +25,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
id = types.Id() id = types.Id()
workspace_id = Column(ForeignKey("workspaces.id")) workspace_id = Column(ForeignKey("workspaces.id"))
workspace = relationship("Workspace") portfolio = relationship("Portfolio")
user_id = Column(ForeignKey("users.id")) user_id = Column(ForeignKey("users.id"))
creator = relationship("User", foreign_keys="TaskOrder.user_id") creator = relationship("User", foreign_keys="TaskOrder.user_id")
@ -92,7 +92,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
@property @property
def portfolio_name(self): def portfolio_name(self):
return self.workspace.name return self.portfolio.name
@property @property
def is_pending(self): def is_pending(self):

View File

@ -14,7 +14,7 @@ class User(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
atat_role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id")) atat_role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"))
atat_role = relationship("Role") atat_role = relationship("Role")
workspace_roles = relationship("WorkspaceRole", backref="user") portfolio_roles = relationship("PortfolioRole", backref="user")
email = Column(String, unique=True) email = Column(String, unique=True)
dod_id = Column(String, unique=True, nullable=False) dod_id = Column(String, unique=True, nullable=False)
@ -65,22 +65,22 @@ class User(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
return "{} {}".format(self.first_name, self.last_name) return "{} {}".format(self.first_name, self.last_name)
@property @property
def has_workspaces(self): def has_portfolios(self):
return ( return (
Permissions.VIEW_WORKSPACE in self.atat_role.permissions Permissions.VIEW_WORKSPACE in self.atat_role.permissions
) or self.workspace_roles ) or self.portfolio_roles
@property @property
def displayname(self): def displayname(self):
return self.full_name return self.full_name
def __repr__(self): def __repr__(self):
return "<User(name='{}', dod_id='{}', email='{}', role='{}', has_workspaces='{}', id='{}')>".format( return "<User(name='{}', dod_id='{}', email='{}', role='{}', has_portfolios='{}', id='{}')>".format(
self.full_name, self.full_name,
self.dod_id, self.dod_id,
self.email, self.email,
self.atat_role_name, self.atat_role_name,
self.has_workspaces, self.has_portfolios,
self.id, self.id,
) )

View File

@ -52,25 +52,25 @@ def home():
if user.atat_role_name == "ccpo": if user.atat_role_name == "ccpo":
return redirect(url_for("requests.requests_index")) return redirect(url_for("requests.requests_index"))
num_workspaces = len(user.workspace_roles) num_portfolios = len(user.portfolio_roles)
if num_workspaces == 0: if num_portfolios == 0:
return redirect(url_for("requests.requests_index")) return redirect(url_for("requests.requests_index"))
elif num_workspaces == 1: elif num_portfolios == 1:
workspace_role = user.workspace_roles[0] portfolio_role = user.portfolio_roles[0]
workspace_id = workspace_role.workspace.id portfolio_id = portfolio_role.portfolio.id
is_request_owner = workspace_role.role.name == "owner" is_request_owner = portfolio_role.role.name == "owner"
if is_request_owner: if is_request_owner:
return redirect( return redirect(
url_for("workspaces.workspace_reports", workspace_id=workspace_id) url_for("portfolios.portfolio_reports", portfolio_id=portfolio_id)
) )
else: else:
return redirect( return redirect(
url_for("workspaces.workspace_applications", workspace_id=workspace_id) url_for("portfolios.portfolio_applications", portfolio_id=portfolio_id)
) )
else: else:
return redirect(url_for("workspaces.workspaces")) return redirect(url_for("portfolios.portfolios"))
@bp.route("/styleguide") @bp.route("/styleguide")

View File

@ -8,7 +8,7 @@ from atst.domain.invitations import (
ExpiredError as InvitationExpiredError, ExpiredError as InvitationExpiredError,
WrongUserError as InvitationWrongUserError, WrongUserError as InvitationWrongUserError,
) )
from atst.domain.workspaces import WorkspaceError from atst.domain.portfolios import PortfolioError
from atst.utils.flash import formatted_flash as flash from atst.utils.flash import formatted_flash as flash
@ -26,7 +26,7 @@ def make_error_pages(app):
@app.errorhandler(werkzeug_exceptions.NotFound) @app.errorhandler(werkzeug_exceptions.NotFound)
@app.errorhandler(exceptions.NotFoundError) @app.errorhandler(exceptions.NotFoundError)
@app.errorhandler(exceptions.UnauthorizedError) @app.errorhandler(exceptions.UnauthorizedError)
@app.errorhandler(WorkspaceError) @app.errorhandler(PortfolioError)
# pylint: disable=unused-variable # pylint: disable=unused-variable
def not_found(e): def not_found(e):
return handle_error(e) return handle_error(e)

View File

@ -0,0 +1,41 @@
from flask import Blueprint, request as http_request, g, render_template
portfolios_bp = Blueprint("portfolios", __name__)
from . import index
from . import applications
from . import members
from . import invitations
from . import task_orders
from atst.domain.exceptions import UnauthorizedError
from atst.domain.portfolios import Portfolios
from atst.domain.authz import Authorization
from atst.models.permissions import Permissions
@portfolios_bp.context_processor
def portfolio():
portfolios = Portfolios.for_user(g.current_user)
portfolio = None
if "portfolio_id" in http_request.view_args:
try:
portfolio = Portfolios.get(
g.current_user, http_request.view_args["portfolio_id"]
)
portfolios = [ws for ws in portfolios if not ws.id == portfolio.id]
except UnauthorizedError:
pass
def user_can(permission):
if portfolio:
return Authorization.has_portfolio_permission(
g.current_user, portfolio, permission
)
return False
return {
"portfolio": portfolio,
"portfolios": portfolios,
"permissions": Permissions,
"user_can": user_can,
}

View File

@ -0,0 +1,102 @@
from flask import (
current_app as app,
g,
redirect,
render_template,
request as http_request,
url_for,
)
from . import portfolios_bp
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.exceptions import UnauthorizedError
from atst.domain.applications import Applications
from atst.domain.portfolios import Portfolios
from atst.forms.application import NewApplicationForm, ApplicationForm
@portfolios_bp.route("/portfolios/<portfolio_id>/applications")
def portfolio_applications(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id)
return render_template("portfolios/applications/index.html", portfolio=portfolio)
@portfolios_bp.route("/portfolios/<portfolio_id>/applications/new")
def new_application(portfolio_id):
portfolio = Portfolios.get_for_update_applications(g.current_user, portfolio_id)
form = NewApplicationForm()
return render_template(
"portfolios/applications/new.html", portfolio=portfolio, form=form
)
@portfolios_bp.route("/portfolios/<portfolio_id>/applications/new", methods=["POST"])
def create_application(portfolio_id):
portfolio = Portfolios.get_for_update_applications(g.current_user, portfolio_id)
form = NewApplicationForm(http_request.form)
if form.validate():
application_data = form.data
Applications.create(
g.current_user,
portfolio,
application_data["name"],
application_data["description"],
application_data["environment_names"],
)
return redirect(
url_for("portfolios.portfolio_applications", portfolio_id=portfolio.id)
)
else:
return render_template(
"portfolios/applications/new.html", portfolio=portfolio, form=form
)
@portfolios_bp.route("/portfolios/<portfolio_id>/applications/<application_id>/edit")
def edit_application(portfolio_id, application_id):
portfolio = Portfolios.get_for_update_applications(g.current_user, portfolio_id)
application = Applications.get(g.current_user, portfolio, application_id)
form = ApplicationForm(name=application.name, description=application.description)
return render_template(
"portfolios/applications/edit.html",
portfolio=portfolio,
application=application,
form=form,
)
@portfolios_bp.route(
"/portfolios/<portfolio_id>/applications/<application_id>/edit", methods=["POST"]
)
def update_application(portfolio_id, application_id):
portfolio = Portfolios.get_for_update_applications(g.current_user, portfolio_id)
application = Applications.get(g.current_user, portfolio, application_id)
form = ApplicationForm(http_request.form)
if form.validate():
application_data = form.data
Applications.update(g.current_user, portfolio, application, application_data)
return redirect(
url_for("portfolios.portfolio_applications", portfolio_id=portfolio.id)
)
else:
return render_template(
"portfolios/applications/edit.html",
portfolio=portfolio,
application=application,
form=form,
)
@portfolios_bp.route("/portfolios/<portfolio_id>/environments/<environment_id>/access")
def access_environment(portfolio_id, environment_id):
env_role = EnvironmentRoles.get(g.current_user.id, environment_id)
if not env_role:
raise UnauthorizedError(
g.current_user, "access environment {}".format(environment_id)
)
else:
token = app.csp.cloud.get_access_token(env_role)
return redirect(url_for("atst.csp_environment_access", token=token))

View File

@ -0,0 +1,102 @@
from datetime import date, timedelta
from flask import render_template, request as http_request, g, redirect, url_for
from . import portfolios_bp
from atst.domain.reports import Reports
from atst.domain.portfolios import Portfolios
from atst.domain.audit_log import AuditLog
from atst.domain.authz import Authorization
from atst.domain.common import Paginator
from atst.forms.portfolio import PortfolioForm
from atst.models.permissions import Permissions
@portfolios_bp.route("/portfolios")
def portfolios():
portfolios = Portfolios.for_user(g.current_user)
return render_template("portfolios/index.html", page=5, portfolios=portfolios)
@portfolios_bp.route("/portfolios/<portfolio_id>/edit")
def portfolio(portfolio_id):
portfolio = Portfolios.get_for_update_information(g.current_user, portfolio_id)
form = PortfolioForm(data={"name": portfolio.name})
return render_template("portfolios/edit.html", form=form, portfolio=portfolio)
@portfolios_bp.route("/portfolios/<portfolio_id>/edit", methods=["POST"])
def edit_portfolio(portfolio_id):
portfolio = Portfolios.get_for_update_information(g.current_user, portfolio_id)
form = PortfolioForm(http_request.form)
if form.validate():
Portfolios.update(portfolio, form.data)
return redirect(
url_for("portfolios.portfolio_applications", portfolio_id=portfolio.id)
)
else:
return render_template("portfolios/edit.html", form=form, portfolio=portfolio)
@portfolios_bp.route("/portfolios/<portfolio_id>")
def show_portfolio(portfolio_id):
return redirect(
url_for("portfolios.portfolio_applications", portfolio_id=portfolio_id)
)
@portfolios_bp.route("/portfolios/<portfolio_id>/reports")
def portfolio_reports(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id)
Authorization.check_portfolio_permission(
g.current_user,
portfolio,
Permissions.VIEW_USAGE_DOLLARS,
"view portfolio reports",
)
today = date.today()
month = http_request.args.get("month", today.month)
year = http_request.args.get("year", today.year)
current_month = date(int(year), int(month), 15)
prev_month = current_month - timedelta(days=28)
two_months_ago = prev_month - timedelta(days=28)
expiration_date = (
portfolio.legacy_task_order and portfolio.legacy_task_order.expiration_date
)
if expiration_date:
remaining_difference = expiration_date - today
remaining_days = remaining_difference.days
else:
remaining_days = None
return render_template(
"portfolios/reports/index.html",
cumulative_budget=Reports.cumulative_budget(portfolio),
portfolio_totals=Reports.portfolio_totals(portfolio),
monthly_totals=Reports.monthly_totals(portfolio),
jedi_request=portfolio.request,
legacy_task_order=portfolio.legacy_task_order,
current_month=current_month,
prev_month=prev_month,
two_months_ago=two_months_ago,
expiration_date=expiration_date,
remaining_days=remaining_days,
)
@portfolios_bp.route("/portfolios/<portfolio_id>/activity")
def portfolio_activity(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id)
pagination_opts = Paginator.get_pagination_opts(http_request)
audit_events = AuditLog.get_portfolio_events(
g.current_user, portfolio, pagination_opts
)
return render_template(
"portfolios/activity/index.html",
portfolio_name=portfolio.name,
portfolio_id=portfolio_id,
audit_events=audit_events,
)

View File

@ -1,7 +1,7 @@
from flask import g, redirect, url_for, render_template from flask import g, redirect, url_for, render_template
from . import workspaces_bp from . import portfolios_bp
from atst.domain.workspaces import Workspaces from atst.domain.portfolios import Portfolios
from atst.domain.invitations import Invitations from atst.domain.invitations import Invitations
from atst.queue import queue from atst.queue import queue
from atst.utils.flash import formatted_flash as flash from atst.utils.flash import formatted_flash as flash
@ -11,12 +11,12 @@ def send_invite_email(owner_name, token, new_member_email):
body = render_template("emails/invitation.txt", owner=owner_name, token=token) body = render_template("emails/invitation.txt", owner=owner_name, token=token)
queue.send_mail( queue.send_mail(
[new_member_email], [new_member_email],
"{} has invited you to a JEDI Cloud Workspace".format(owner_name), "{} has invited you to a JEDI Cloud Portfolio".format(owner_name),
body, body,
) )
@workspaces_bp.route("/workspaces/invitations/<token>", methods=["GET"]) @portfolios_bp.route("/portfolios/invitations/<token>", methods=["GET"])
def accept_invitation(token): def accept_invitation(token):
invite = Invitations.accept(g.current_user, token) invite = Invitations.accept(g.current_user, token)
@ -25,7 +25,7 @@ def accept_invitation(token):
# are. It will also have to manage cases like: # are. It will also have to manage cases like:
# - the logged-in user has multiple roles on the TO (e.g., KO and COR) # - the logged-in user has multiple roles on the TO (e.g., KO and COR)
# - the logged-in user has officer roles on multiple unsigned TOs # - the logged-in user has officer roles on multiple unsigned TOs
for task_order in invite.workspace.task_orders: for task_order in invite.portfolio.task_orders:
if g.current_user == task_order.contracting_officer: if g.current_user == task_order.contracting_officer:
return redirect( return redirect(
url_for("task_orders.new", screen=4, task_order_id=task_order.id) url_for("task_orders.new", screen=4, task_order_id=task_order.id)
@ -40,25 +40,25 @@ def accept_invitation(token):
) )
return redirect( return redirect(
url_for("workspaces.show_workspace", workspace_id=invite.workspace.id) url_for("portfolios.show_portfolio", portfolio_id=invite.portfolio.id)
) )
@workspaces_bp.route( @portfolios_bp.route(
"/workspaces/<workspace_id>/invitations/<token>/revoke", methods=["POST"] "/portfolios/<portfolio_id>/invitations/<token>/revoke", methods=["POST"]
) )
def revoke_invitation(workspace_id, token): def revoke_invitation(portfolio_id, token):
workspace = Workspaces.get_for_update_member(g.current_user, workspace_id) portfolio = Portfolios.get_for_update_member(g.current_user, portfolio_id)
Invitations.revoke(token) Invitations.revoke(token)
return redirect(url_for("workspaces.workspace_members", workspace_id=workspace.id)) return redirect(url_for("portfolios.portfolio_members", portfolio_id=portfolio.id))
@workspaces_bp.route( @portfolios_bp.route(
"/workspaces/<workspace_id>/invitations/<token>/resend", methods=["POST"] "/portfolios/<portfolio_id>/invitations/<token>/resend", methods=["POST"]
) )
def resend_invitation(workspace_id, token): def resend_invitation(portfolio_id, token):
invite = Invitations.resend(g.current_user, workspace_id, token) invite = Invitations.resend(g.current_user, portfolio_id, token)
send_invite_email(g.current_user.full_name, invite.token, invite.email) send_invite_email(g.current_user.full_name, invite.token, invite.email)
flash("resend_workspace_invitation", user_name=invite.user_name) flash("resend_portfolio_invitation", user_name=invite.user_name)
return redirect(url_for("workspaces.workspace_members", workspace_id=workspace_id)) return redirect(url_for("portfolios.portfolio_members", portfolio_id=portfolio_id))

View File

@ -2,11 +2,11 @@ import re
from flask import render_template, request as http_request, g, redirect, url_for from flask import render_template, request as http_request, g, redirect, url_for
from . import workspaces_bp from . import portfolios_bp
from atst.domain.exceptions import AlreadyExistsError from atst.domain.exceptions import AlreadyExistsError
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.workspaces import Workspaces from atst.domain.portfolios import Portfolios
from atst.domain.workspace_roles import WorkspaceRoles, MEMBER_STATUS_CHOICES from atst.domain.portfolio_roles import PortfolioRoles, MEMBER_STATUS_CHOICES
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.services.invitation import Invitation as InvitationService from atst.services.invitation import Invitation as InvitationService
@ -23,12 +23,12 @@ from atst.models.permissions import Permissions
from atst.utils.flash import formatted_flash as flash from atst.utils.flash import formatted_flash as flash
@workspaces_bp.route("/workspaces/<workspace_id>/members") @portfolios_bp.route("/portfolios/<portfolio_id>/members")
def workspace_members(workspace_id): def portfolio_members(portfolio_id):
workspace = Workspaces.get_with_members(g.current_user, workspace_id) portfolio = Portfolios.get_with_members(g.current_user, portfolio_id)
new_member_name = http_request.args.get("newMemberName") new_member_name = http_request.args.get("newMemberName")
new_member = next( new_member = next(
filter(lambda m: m.user_name == new_member_name, workspace.members), None filter(lambda m: m.user_name == new_member_name, portfolio.members), None
) )
members_list = [ members_list = [
{ {
@ -38,15 +38,15 @@ def workspace_members(workspace_id):
"role": k.role_displayname, "role": k.role_displayname,
"num_env": k.num_environment_roles, "num_env": k.num_environment_roles,
"edit_link": url_for( "edit_link": url_for(
"workspaces.view_member", workspace_id=workspace.id, member_id=k.user_id "portfolios.view_member", portfolio_id=portfolio.id, member_id=k.user_id
), ),
} }
for k in workspace.members for k in portfolio.members
] ]
return render_template( return render_template(
"workspaces/members/index.html", "portfolios/members/index.html",
workspace=workspace, portfolio=portfolio,
role_choices=WORKSPACE_ROLE_DEFINITIONS, role_choices=WORKSPACE_ROLE_DEFINITIONS,
status_choices=MEMBER_STATUS_CHOICES, status_choices=MEMBER_STATUS_CHOICES,
members=members_list, members=members_list,
@ -54,32 +54,32 @@ def workspace_members(workspace_id):
) )
@workspaces_bp.route("/workspaces/<workspace_id>/members/new") @portfolios_bp.route("/portfolios/<portfolio_id>/members/new")
def new_member(workspace_id): def new_member(portfolio_id):
workspace = Workspaces.get(g.current_user, workspace_id) portfolio = Portfolios.get(g.current_user, portfolio_id)
form = NewMemberForm() form = NewMemberForm()
return render_template( return render_template(
"workspaces/members/new.html", workspace=workspace, form=form "portfolios/members/new.html", portfolio=portfolio, form=form
) )
@workspaces_bp.route("/workspaces/<workspace_id>/members/new", methods=["POST"]) @portfolios_bp.route("/portfolios/<portfolio_id>/members/new", methods=["POST"])
def create_member(workspace_id): def create_member(portfolio_id):
workspace = Workspaces.get(g.current_user, workspace_id) portfolio = Portfolios.get(g.current_user, portfolio_id)
form = NewMemberForm(http_request.form) form = NewMemberForm(http_request.form)
if form.validate(): if form.validate():
try: try:
member = Workspaces.create_member(g.current_user, workspace, form.data) member = Portfolios.create_member(g.current_user, portfolio, form.data)
invite_service = InvitationService( invite_service = InvitationService(
g.current_user, member, form.data.get("email") g.current_user, member, form.data.get("email")
) )
invite_service.invite() invite_service.invite()
flash("new_workspace_member", new_member=new_member, workspace=workspace) flash("new_portfolio_member", new_member=new_member, portfolio=portfolio)
return redirect( return redirect(
url_for("workspaces.workspace_members", workspace_id=workspace.id) url_for("portfolios.portfolio_members", portfolio_id=portfolio.id)
) )
except AlreadyExistsError: except AlreadyExistsError:
return render_template( return render_template(
@ -87,31 +87,31 @@ def create_member(workspace_id):
) )
else: else:
return render_template( return render_template(
"workspaces/members/new.html", workspace=workspace, form=form "portfolios/members/new.html", portfolio=portfolio, form=form
) )
@workspaces_bp.route("/workspaces/<workspace_id>/members/<member_id>/member_edit") @portfolios_bp.route("/portfolios/<portfolio_id>/members/<member_id>/member_edit")
def view_member(workspace_id, member_id): def view_member(portfolio_id, member_id):
workspace = Workspaces.get(g.current_user, workspace_id) portfolio = Portfolios.get(g.current_user, portfolio_id)
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
g.current_user, g.current_user,
workspace, portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"edit this workspace user", "edit this portfolio user",
) )
member = WorkspaceRoles.get(workspace_id, member_id) member = PortfolioRoles.get(portfolio_id, member_id)
applications = Applications.get_all(g.current_user, member, workspace) applications = Applications.get_all(g.current_user, member, portfolio)
form = EditMemberForm(workspace_role=member.role_name) form = EditMemberForm(portfolio_role=member.role_name)
editable = g.current_user == member.user editable = g.current_user == member.user
can_revoke_access = Workspaces.can_revoke_access_for(workspace, member) can_revoke_access = Portfolios.can_revoke_access_for(portfolio, member)
if member.has_dod_id_error: if member.has_dod_id_error:
flash("workspace_member_dod_id_error") flash("portfolio_member_dod_id_error")
return render_template( return render_template(
"workspaces/members/edit.html", "portfolios/members/edit.html",
workspace=workspace, portfolio=portfolio,
member=member, member=member,
applications=applications, applications=applications,
form=form, form=form,
@ -123,18 +123,18 @@ def view_member(workspace_id, member_id):
) )
@workspaces_bp.route( @portfolios_bp.route(
"/workspaces/<workspace_id>/members/<member_id>/member_edit", methods=["POST"] "/portfolios/<portfolio_id>/members/<member_id>/member_edit", methods=["POST"]
) )
def update_member(workspace_id, member_id): def update_member(portfolio_id, member_id):
workspace = Workspaces.get(g.current_user, workspace_id) portfolio = Portfolios.get(g.current_user, portfolio_id)
Authorization.check_workspace_permission( Authorization.check_portfolio_permission(
g.current_user, g.current_user,
workspace, portfolio,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
"edit this workspace user", "edit this portfolio user",
) )
member = WorkspaceRoles.get(workspace_id, member_id) member = PortfolioRoles.get(portfolio_id, member_id)
ids_and_roles = [] ids_and_roles = []
form_dict = http_request.form.to_dict() form_dict = http_request.form.to_dict()
@ -147,39 +147,39 @@ def update_member(workspace_id, member_id):
form = EditMemberForm(http_request.form) form = EditMemberForm(http_request.form)
if form.validate(): if form.validate():
new_role_name = None new_role_name = None
if form.data["workspace_role"] != member.role.name: if form.data["portfolio_role"] != member.role.name:
member = Workspaces.update_member( member = Portfolios.update_member(
g.current_user, workspace, member, form.data["workspace_role"] g.current_user, portfolio, member, form.data["portfolio_role"]
) )
new_role_name = member.role_displayname new_role_name = member.role_displayname
flash( flash(
"workspace_role_updated", "portfolio_role_updated",
member_name=member.user_name, member_name=member.user_name,
updated_role=new_role_name, updated_role=new_role_name,
) )
updated_roles = Environments.update_environment_roles( updated_roles = Environments.update_environment_roles(
g.current_user, workspace, member, ids_and_roles g.current_user, portfolio, member, ids_and_roles
) )
if updated_roles: if updated_roles:
flash("environment_access_changed") flash("environment_access_changed")
return redirect( return redirect(
url_for("workspaces.workspace_members", workspace_id=workspace.id) url_for("portfolios.portfolio_members", portfolio_id=portfolio.id)
) )
else: else:
return render_template( return render_template(
"workspaces/members/edit.html", "portfolios/members/edit.html",
form=form, form=form,
workspace=workspace, portfolio=portfolio,
member=member, member=member,
) )
@workspaces_bp.route( @portfolios_bp.route(
"/workspaces/<workspace_id>/members/<member_id>/revoke_access", methods=["POST"] "/portfolios/<portfolio_id>/members/<member_id>/revoke_access", methods=["POST"]
) )
def revoke_access(workspace_id, member_id): def revoke_access(portfolio_id, member_id):
revoked_role = Workspaces.revoke_access(g.current_user, workspace_id, member_id) revoked_role = Portfolios.revoke_access(g.current_user, portfolio_id, member_id)
flash("revoked_workspace_access", member_name=revoked_role.user.full_name) flash("revoked_portfolio_access", member_name=revoked_role.user.full_name)
return redirect(url_for("workspaces.workspace_members", workspace_id=workspace_id)) return redirect(url_for("portfolios.portfolio_members", portfolio_id=portfolio_id))

View File

@ -0,0 +1,20 @@
from flask import g, render_template
from . import portfolios_bp
from atst.domain.task_orders import TaskOrders
from atst.domain.portfolios import Portfolios
@portfolios_bp.route("/portfolios/<portfolio_id>/task_orders")
def portfolio_task_orders(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id)
return render_template("portfolios/task_orders/index.html", portfolio=portfolio)
@portfolios_bp.route("/portfolios/<portfolio_id>/task_order/<task_order_id>")
def view_task_order(portfolio_id, task_order_id):
portfolio = Portfolios.get(g.current_user, portfolio_id)
task_order = TaskOrders.get(task_order_id)
return render_template(
"portfolios/task_orders/show.html", portfolio=portfolio, task_order=task_order
)

View File

@ -248,10 +248,10 @@ def update_financial_verification(request_id):
) )
if updated_request.legacy_task_order.verified: if updated_request.legacy_task_order.verified:
workspace = Requests.auto_approve_and_create_workspace(updated_request) portfolio = Requests.auto_approve_and_create_portfolio(updated_request)
flash("new_workspace") flash("new_portfolio")
return redirect( return redirect(
url_for("workspaces.new_application", workspace_id=workspace.id) url_for("portfolios.new_application", portfolio_id=portfolio.id)
) )
else: else:
return redirect(url_for("requests.requests_index", modal="pendingCCPOApproval")) return redirect(url_for("requests.requests_index", modal="pendingCCPOApproval"))

View File

@ -63,10 +63,10 @@ class RequestsIndex(object):
"extended_view": False, "extended_view": False,
} }
def _workspace_link_for_request(self, request): def _portfolio_link_for_request(self, request):
if request.is_approved: if request.is_approved:
return url_for( return url_for(
"workspaces.workspace_applications", workspace_id=request.workspace.id "portfolios.portfolio_applications", portfolio_id=request.portfolio.id
) )
else: else:
return None return None
@ -80,7 +80,7 @@ class RequestsIndex(object):
annual_usage = request.annual_spend annual_usage = request.annual_spend
return { return {
"workspace_id": request.workspace.id if request.workspace else None, "portfolio_id": request.portfolio.id if request.portfolio else None,
"name": request.displayname, "name": request.displayname,
"is_new": is_new, "is_new": is_new,
"is_approved": request.is_approved, "is_approved": request.is_approved,
@ -93,7 +93,7 @@ class RequestsIndex(object):
"edit_link": url_for("requests.edit", request_id=request.id), "edit_link": url_for("requests.edit", request_id=request.id),
"action_required": request.action_required_by == viewing_role, "action_required": request.action_required_by == viewing_role,
"dod_component": request.latest_revision.dod_component, "dod_component": request.latest_revision.dod_component,
"workspace_link": self._workspace_link_for_request(request), "portfolio_link": self._portfolio_link_for_request(request),
} }

View File

@ -113,9 +113,9 @@ class JEDIRequestFlow(object):
"form": request_forms.InformationAboutYouForm, "form": request_forms.InformationAboutYouForm,
}, },
{ {
"title": "Workspace Owner", "title": "Portfolio Owner",
"section": "primary_poc", "section": "primary_poc",
"form": request_forms.WorkspaceOwnerForm, "form": request_forms.PortfolioOwnerForm,
}, },
{ {
"title": "Review & Submit", "title": "Review & Submit",

View File

@ -10,5 +10,5 @@ def invite(task_order_id):
task_order = TaskOrders.get(g.current_user, task_order_id) task_order = TaskOrders.get(g.current_user, task_order_id)
flash("task_order_submitted", task_order=task_order) flash("task_order_submitted", task_order=task_order)
return redirect( return redirect(
url_for("workspaces.workspace_members", workspace_id=task_order.workspace.id) url_for("portfolios.portfolio_members", portfolio_id=task_order.portfolio.id)
) )

View File

@ -11,8 +11,8 @@ from flask import (
from . import task_orders_bp from . import task_orders_bp
from atst.domain.task_orders import TaskOrders from atst.domain.task_orders import TaskOrders
from atst.domain.workspaces import Workspaces from atst.domain.portfolios import Portfolios
from atst.domain.workspace_roles import WorkspaceRoles from atst.domain.portfolio_roles import PortfolioRoles
import atst.forms.task_order as task_order_form import atst.forms.task_order as task_order_form
from atst.services.invitation import Invitation as InvitationService from atst.services.invitation import Invitation as InvitationService
@ -114,9 +114,9 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
return self._form return self._form
@property @property
def workspace(self): def portfolio(self):
if self.task_order: if self.task_order:
return self.task_order.workspace return self.task_order.portfolio
def validate(self): def validate(self):
return self.form.validate() return self.form.validate()
@ -125,7 +125,7 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
if self.task_order: if self.task_order:
TaskOrders.update(self.user, self.task_order, **self.form.data) TaskOrders.update(self.user, self.task_order, **self.form.data)
else: else:
ws = Workspaces.create(self.user, self.form.portfolio_name.data) ws = Portfolios.create(self.user, self.form.portfolio_name.data)
to_data = self.form.data.copy() to_data = self.form.data.copy()
to_data.pop("portfolio_name") to_data.pop("portfolio_name")
self._task_order = TaskOrders.create(self.user, ws) self._task_order = TaskOrders.create(self.user, ws)
@ -177,7 +177,7 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
officer = TaskOrders.add_officer( officer = TaskOrders.add_officer(
self.user, self.task_order, officer_type["role"], officer_data self.user, self.task_order, officer_type["role"], officer_data
) )
ws_officer_member = WorkspaceRoles.get(self.workspace.id, officer.id) ws_officer_member = PortfolioRoles.get(self.portfolio.id, officer.id)
invite_service = InvitationService( invite_service = InvitationService(
self.user, self.user,
ws_officer_member, ws_officer_member,

View File

@ -1,41 +0,0 @@
from flask import Blueprint, request as http_request, g, render_template
workspaces_bp = Blueprint("workspaces", __name__)
from . import index
from . import applications
from . import members
from . import invitations
from . import task_orders
from atst.domain.exceptions import UnauthorizedError
from atst.domain.workspaces import Workspaces
from atst.domain.authz import Authorization
from atst.models.permissions import Permissions
@workspaces_bp.context_processor
def workspace():
workspaces = Workspaces.for_user(g.current_user)
workspace = None
if "workspace_id" in http_request.view_args:
try:
workspace = Workspaces.get(
g.current_user, http_request.view_args["workspace_id"]
)
workspaces = [ws for ws in workspaces if not ws.id == workspace.id]
except UnauthorizedError:
pass
def user_can(permission):
if workspace:
return Authorization.has_workspace_permission(
g.current_user, workspace, permission
)
return False
return {
"workspace": workspace,
"workspaces": workspaces,
"permissions": Permissions,
"user_can": user_can,
}

View File

@ -1,102 +0,0 @@
from flask import (
current_app as app,
g,
redirect,
render_template,
request as http_request,
url_for,
)
from . import workspaces_bp
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.exceptions import UnauthorizedError
from atst.domain.applications import Applications
from atst.domain.workspaces import Workspaces
from atst.forms.application import NewApplicationForm, ApplicationForm
@workspaces_bp.route("/workspaces/<workspace_id>/applications")
def workspace_applications(workspace_id):
workspace = Workspaces.get(g.current_user, workspace_id)
return render_template("workspaces/applications/index.html", workspace=workspace)
@workspaces_bp.route("/workspaces/<workspace_id>/applications/new")
def new_application(workspace_id):
workspace = Workspaces.get_for_update_applications(g.current_user, workspace_id)
form = NewApplicationForm()
return render_template(
"workspaces/applications/new.html", workspace=workspace, form=form
)
@workspaces_bp.route("/workspaces/<workspace_id>/applications/new", methods=["POST"])
def create_application(workspace_id):
workspace = Workspaces.get_for_update_applications(g.current_user, workspace_id)
form = NewApplicationForm(http_request.form)
if form.validate():
application_data = form.data
Applications.create(
g.current_user,
workspace,
application_data["name"],
application_data["description"],
application_data["environment_names"],
)
return redirect(
url_for("workspaces.workspace_applications", workspace_id=workspace.id)
)
else:
return render_template(
"workspaces/applications/new.html", workspace=workspace, form=form
)
@workspaces_bp.route("/workspaces/<workspace_id>/applications/<application_id>/edit")
def edit_application(workspace_id, application_id):
workspace = Workspaces.get_for_update_applications(g.current_user, workspace_id)
application = Applications.get(g.current_user, workspace, application_id)
form = ApplicationForm(name=application.name, description=application.description)
return render_template(
"workspaces/applications/edit.html",
workspace=workspace,
application=application,
form=form,
)
@workspaces_bp.route(
"/workspaces/<workspace_id>/applications/<application_id>/edit", methods=["POST"]
)
def update_application(workspace_id, application_id):
workspace = Workspaces.get_for_update_applications(g.current_user, workspace_id)
application = Applications.get(g.current_user, workspace, application_id)
form = ApplicationForm(http_request.form)
if form.validate():
application_data = form.data
Applications.update(g.current_user, workspace, application, application_data)
return redirect(
url_for("workspaces.workspace_applications", workspace_id=workspace.id)
)
else:
return render_template(
"workspaces/applications/edit.html",
workspace=workspace,
application=application,
form=form,
)
@workspaces_bp.route("/workspaces/<workspace_id>/environments/<environment_id>/access")
def access_environment(workspace_id, environment_id):
env_role = EnvironmentRoles.get(g.current_user.id, environment_id)
if not env_role:
raise UnauthorizedError(
g.current_user, "access environment {}".format(environment_id)
)
else:
token = app.csp.cloud.get_access_token(env_role)
return redirect(url_for("atst.csp_environment_access", token=token))

View File

@ -1,102 +0,0 @@
from datetime import date, timedelta
from flask import render_template, request as http_request, g, redirect, url_for
from . import workspaces_bp
from atst.domain.reports import Reports
from atst.domain.workspaces import Workspaces
from atst.domain.audit_log import AuditLog
from atst.domain.authz import Authorization
from atst.domain.common import Paginator
from atst.forms.workspace import WorkspaceForm
from atst.models.permissions import Permissions
@workspaces_bp.route("/workspaces")
def workspaces():
workspaces = Workspaces.for_user(g.current_user)
return render_template("workspaces/index.html", page=5, workspaces=workspaces)
@workspaces_bp.route("/workspaces/<workspace_id>/edit")
def workspace(workspace_id):
workspace = Workspaces.get_for_update_information(g.current_user, workspace_id)
form = WorkspaceForm(data={"name": workspace.name})
return render_template("workspaces/edit.html", form=form, workspace=workspace)
@workspaces_bp.route("/workspaces/<workspace_id>/edit", methods=["POST"])
def edit_workspace(workspace_id):
workspace = Workspaces.get_for_update_information(g.current_user, workspace_id)
form = WorkspaceForm(http_request.form)
if form.validate():
Workspaces.update(workspace, form.data)
return redirect(
url_for("workspaces.workspace_applications", workspace_id=workspace.id)
)
else:
return render_template("workspaces/edit.html", form=form, workspace=workspace)
@workspaces_bp.route("/workspaces/<workspace_id>")
def show_workspace(workspace_id):
return redirect(
url_for("workspaces.workspace_applications", workspace_id=workspace_id)
)
@workspaces_bp.route("/workspaces/<workspace_id>/reports")
def workspace_reports(workspace_id):
workspace = Workspaces.get(g.current_user, workspace_id)
Authorization.check_workspace_permission(
g.current_user,
workspace,
Permissions.VIEW_USAGE_DOLLARS,
"view workspace reports",
)
today = date.today()
month = http_request.args.get("month", today.month)
year = http_request.args.get("year", today.year)
current_month = date(int(year), int(month), 15)
prev_month = current_month - timedelta(days=28)
two_months_ago = prev_month - timedelta(days=28)
expiration_date = (
workspace.legacy_task_order and workspace.legacy_task_order.expiration_date
)
if expiration_date:
remaining_difference = expiration_date - today
remaining_days = remaining_difference.days
else:
remaining_days = None
return render_template(
"workspaces/reports/index.html",
cumulative_budget=Reports.cumulative_budget(workspace),
workspace_totals=Reports.workspace_totals(workspace),
monthly_totals=Reports.monthly_totals(workspace),
jedi_request=workspace.request,
legacy_task_order=workspace.legacy_task_order,
current_month=current_month,
prev_month=prev_month,
two_months_ago=two_months_ago,
expiration_date=expiration_date,
remaining_days=remaining_days,
)
@workspaces_bp.route("/workspaces/<workspace_id>/activity")
def workspace_activity(workspace_id):
workspace = Workspaces.get(g.current_user, workspace_id)
pagination_opts = Paginator.get_pagination_opts(http_request)
audit_events = AuditLog.get_workspace_events(
g.current_user, workspace, pagination_opts
)
return render_template(
"workspaces/activity/index.html",
workspace_name=workspace.name,
workspace_id=workspace_id,
audit_events=audit_events,
)

View File

@ -10,7 +10,7 @@ class Invitation:
inviter, inviter,
member, member,
email, email,
subject="{} has invited you to a JEDI Cloud Workspace", subject="{} has invited you to a JEDI Cloud Portfolio",
email_template="emails/invitation.txt", email_template="emails/invitation.txt",
): ):
self.inviter = inviter self.inviter = inviter

View File

@ -1,29 +1,29 @@
from flask import flash, render_template_string from flask import flash, render_template_string
MESSAGES = { MESSAGES = {
"new_workspace_member": { "new_portfolio_member": {
"title_template": "Member added successfully", "title_template": "Member added successfully",
"message_template": """ "message_template": """
<p>{{ new_member.user_name }} was successfully invited via email to this workspace. They do not yet have access to any environments.</p> <p>{{ new_member.user_name }} was successfully invited via email to this portfolio. They do not yet have access to any environments.</p>
<p><a href="{{ url_for('workspaces.update_member', workspace_id=workspace.id, member_id=new_member.user_id) }}">Add environment access.</a></p> <p><a href="{{ url_for('portfolios.update_member', portfolio_id=portfolio.id, member_id=new_member.user_id) }}">Add environment access.</a></p>
""", """,
"category": "success", "category": "success",
}, },
"revoked_workspace_access": { "revoked_portfolio_access": {
"title_template": "Removed workspace access", "title_template": "Removed portfolio access",
"message_template": """ "message_template": """
<p>Portfolio access successfully removed from {{ member_name }}.</p> <p>Portfolio access successfully removed from {{ member_name }}.</p>
""", """,
"category": "success", "category": "success",
}, },
"resend_workspace_invitation": { "resend_portfolio_invitation": {
"title_template": "Invitation resent", "title_template": "Invitation resent",
"message_template": """ "message_template": """
<p>Successfully sent a new invitation to {{ user_name }}.</p> <p>Successfully sent a new invitation to {{ user_name }}.</p>
""", """,
"category": "success", "category": "success",
}, },
"workspace_role_updated": { "portfolio_role_updated": {
"title_template": "Portfolio role updated successfully", "title_template": "Portfolio role updated successfully",
"message_template": """ "message_template": """
<p>{{ member_name }}'s role was successfully updated to {{ updated_role }}</p> <p>{{ member_name }}'s role was successfully updated to {{ updated_role }}</p>
@ -44,14 +44,14 @@ MESSAGES = {
""", """,
"category": "warning", "category": "warning",
}, },
"new_workspace": { "new_portfolio": {
"title_template": "Portfolio created!", "title_template": "Portfolio created!",
"message_template": """ "message_template": """
<p>You are now ready to create applications and environments within the JEDI Cloud.</p> <p>You are now ready to create applications and environments within the JEDI Cloud.</p>
""", """,
"category": "success", "category": "success",
}, },
"workspace_member_dod_id_error": { "portfolio_member_dod_id_error": {
"title_template": "CAC ID Error", "title_template": "CAC ID Error",
"message_template": """ "message_template": """
The member attempted to accept this invite, but their CAC ID did not match the CAC ID you specified on the invite. Please confirm that the DOD ID is accurate. The member attempted to accept this invite, but their CAC ID did not match the CAC ID you specified on the invite. Please confirm that the DOD ID is accurate.

View File

@ -84,7 +84,7 @@ export default {
sortFunc: alphabeticalSort sortFunc: alphabeticalSort
}, },
{ {
displayName: 'Workspace Role', displayName: 'Portfolio Role',
attr: 'role', attr: 'role',
sortFunc: alphabeticalSort, sortFunc: alphabeticalSort,
}, },

View File

@ -6,7 +6,7 @@ export default {
props: { props: {
applications: Object, applications: Object,
workspace: Object, portfolio: Object,
environments: Object, environments: Object,
currentMonthIndex: String, currentMonthIndex: String,
prevMonthIndex: String, prevMonthIndex: String,

View File

@ -83,10 +83,10 @@ export default {
unmask: [], unmask: [],
validationError: 'Please enter a valid BA Code. Note that it should be two digits, followed by an optional letter.' validationError: 'Please enter a valid BA Code. Note that it should be two digits, followed by an optional letter.'
}, },
workspaceName: { portfolioName: {
mask: false, mask: false,
match: /^.{4,100}$/, match: /^.{4,100}$/,
unmask: [], unmask: [],
validationError: 'Workspace and request names must be at least 4 and not more than 100 characters' validationError: 'Portfolio and request names must be at least 4 and not more than 100 characters'
}, },
} }

View File

@ -25,7 +25,7 @@
@import 'components/topbar'; @import 'components/topbar';
@import 'components/global_layout'; @import 'components/global_layout';
@import 'components/global_navigation'; @import 'components/global_navigation';
@import 'components/workspace_layout'; @import 'components/portfolio_layout';
@import 'components/site_action'; @import 'components/site_action';
@import 'components/empty_state'; @import 'components/empty_state';
@import 'components/alerts'; @import 'components/alerts';

View File

@ -18,7 +18,7 @@
} }
} }
&.global-navigation__context--workspace { &.global-navigation__context--portfolio {
.sidenav__link { .sidenav__link {
padding-right: $gap; padding-right: $gap;
} }

View File

@ -1,10 +1,10 @@
.workspace-panel-container { .portfolio-panel-container {
@include media($large-screen) { @include media($large-screen) {
@include grid-row; @include grid-row;
} }
} }
.workspace-navigation { .portfolio-navigation {
@include panel-margin; @include panel-margin;
margin-bottom: $gap * 4; margin-bottom: $gap * 4;

View File

@ -59,11 +59,11 @@
align-items: stretch; align-items: stretch;
justify-content: flex-end; justify-content: flex-end;
.topbar__workspace-menu { .topbar__portfolio-menu {
margin-right: auto; margin-right: auto;
position: relative; position: relative;
.topbar__workspace-menu__toggle { .topbar__portfolio-menu__toggle {
margin: 0; margin: 0;
border-radius: 0; border-radius: 0;
@ -89,12 +89,12 @@
} }
} }
.topbar__workspace-menu__panel { .topbar__portfolio-menu__panel {
position: absolute; position: absolute;
} }
} }
&.topbar__context--workspace { &.topbar__context--portfolio {
background-color: $color-primary; background-color: $color-primary;
-ms-flex-pack: start; -ms-flex-pack: start;

View File

@ -207,7 +207,7 @@
&--validation { &--validation {
&--anything, &--anything,
&--workspaceName, &--portfolioName,
&--requiredField, &--requiredField,
&--email { &--email {
input { input {

View File

@ -283,7 +283,7 @@
} }
} }
.spend-table__workspace { .spend-table__portfolio {
th, td { th, td {
font-weight: bold; font-weight: bold;
} }

View File

@ -1,5 +1,5 @@
{% extends "audit_log/events/_base.html" %} {% extends "audit_log/events/_base.html" %}
{% block content %} {% block content %}
in Portfolio <code>{{ event.workspace_id }}</code> ({{ event.workspace.name }}) in Portfolio <code>{{ event.portfolio_id }}</code> ({{ event.portfolio.name }})
{% endblock %} {% endblock %}

View File

@ -14,6 +14,6 @@
<br> <br>
in Application <code>{{ event.event_details["application_id"] }}</code> ({{ event.event_details["application"] }}) in Application <code>{{ event.event_details["application_id"] }}</code> ({{ event.event_details["application"] }})
<br> <br>
in Portfolio <code>{{ event.event_details["workspace_id"] }}</code> ({{ event.event_details["workspace"] }}) in Portfolio <code>{{ event.event_details["portfolio_id"] }}</code> ({{ event.event_details["portfolio"] }})
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -10,5 +10,5 @@
invited {{ event.event_details.email }} (DOD <code>{{ event.event_details.dod_id }}</code>) invited {{ event.event_details.email }} (DOD <code>{{ event.event_details.dod_id }}</code>)
<br> <br>
{% endif %} {% endif %}
in Portfolio <code>{{ event.workspace_id }}</code> ({{ event.workspace.name }}) in Portfolio <code>{{ event.portfolio_id }}</code> ({{ event.portfolio.name }})
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
for User <code>{{ event.event_details.updated_user_id }}</code> ({{ event.event_details.updated_user_name }}) for User <code>{{ event.event_details.updated_user_id }}</code> ({{ event.event_details.updated_user_name }})
in Portfolio <code>{{ event.workspace_id }}</code> ({{ event.workspace.name }}) in Portfolio <code>{{ event.portfolio_id }}</code> ({{ event.portfolio.name }})
{% if event.changed_state.status %} {% if event.changed_state.status %}
from status "{{ event.changed_state.status[0] }}" to "{{ event.changed_state.status[1] }}" from status "{{ event.changed_state.status[0] }}" to "{{ event.changed_state.status[1] }}"

View File

@ -1,4 +1,4 @@
{% macro Page(pagination, route, i, label=None, disabled=False, workspace_id=None) -%} {% macro Page(pagination, route, i, label=None, disabled=False, portfolio_id=None) -%}
{% set label = label or i %} {% set label = label or i %}
{% set button_class = "page usa-button " %} {% set button_class = "page usa-button " %}
@ -11,42 +11,42 @@
{% set button_class = button_class + "usa-button-secondary" %} {% set button_class = button_class + "usa-button-secondary" %}
{% endif %} {% endif %}
<a id="{{ label }}" type="button" class="{{ button_class }}" href="{{ url_for(route, workspace_id=workspace_id, page=i, perPage=pagination.per_page) if not disabled else 'null' }}">{{ label }}</a> <a id="{{ label }}" type="button" class="{{ button_class }}" href="{{ url_for(route, portfolio_id=portfolio_id, page=i, perPage=pagination.per_page) if not disabled else 'null' }}">{{ label }}</a>
{%- endmacro %} {%- endmacro %}
{% macro Pagination(pagination, route, workspace_id=None) -%} {% macro Pagination(pagination, route, portfolio_id=None) -%}
<div class="pagination"> <div class="pagination">
{% if pagination.page == 1 %} {% if pagination.page == 1 %}
{{ Page(pagination, route, 1, label="first", disabled=True, workspace_id=workspace_id) }} {{ Page(pagination, route, 1, label="first", disabled=True, portfolio_id=portfolio_id) }}
{{ Page(pagination, route, pagination.page - 1, label="prev", disabled=True, workspace_id=workspace_id) }} {{ Page(pagination, route, pagination.page - 1, label="prev", disabled=True, portfolio_id=portfolio_id) }}
{% else %} {% else %}
{{ Page(pagination, route, 1, label="first", workspace_id=workspace_id) }} {{ Page(pagination, route, 1, label="first", portfolio_id=portfolio_id) }}
{{ Page(pagination, route, pagination.page - 1, label="prev", workspace_id=workspace_id) }} {{ Page(pagination, route, pagination.page - 1, label="prev", portfolio_id=portfolio_id) }}
{% endif %} {% endif %}
{% if pagination.page == 1 %} {% if pagination.page == 1 %}
{% set max_page = [pagination.pages, 5] | min %} {% set max_page = [pagination.pages, 5] | min %}
{% for i in range(1, max_page + 1) %} {% for i in range(1, max_page + 1) %}
{{ Page(pagination, route, i, workspace_id=workspace_id) }} {{ Page(pagination, route, i, portfolio_id=portfolio_id) }}
{% endfor %} {% endfor %}
{% elif pagination.page == pagination.pages %} {% elif pagination.page == pagination.pages %}
{% for i in range(pagination.pages - 4, pagination.pages + 1) %} {% for i in range(pagination.pages - 4, pagination.pages + 1) %}
{{ Page(pagination, route, i, workspace_id=workspace_id) }} {{ Page(pagination, route, i, portfolio_id=portfolio_id) }}
{% endfor %} {% endfor %}
{% else %} {% else %}
{% set window = pagination | pageWindow %} {% set window = pagination | pageWindow %}
{% for i in range(window.0, window.1 + 1) %} {% for i in range(window.0, window.1 + 1) %}
{{ Page(pagination, route, i, workspace_id=workspace_id) }} {{ Page(pagination, route, i, portfolio_id=portfolio_id) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if pagination.page == pagination.pages %} {% if pagination.page == pagination.pages %}
{{ Page(pagination, route, pagination.page + 1, label="next", disabled=True, workspace_id=workspace_id) }} {{ Page(pagination, route, pagination.page + 1, label="next", disabled=True, portfolio_id=portfolio_id) }}
{{ Page(pagination, route, pagination.pages, label="last", disabled=True, workspace_id=workspace_id) }} {{ Page(pagination, route, pagination.pages, label="last", disabled=True, portfolio_id=portfolio_id) }}
{% else %} {% else %}
{{ Page(pagination, route, pagination.page + 1, label="next", workspace_id=workspace_id) }} {{ Page(pagination, route, pagination.page + 1, label="next", portfolio_id=portfolio_id) }}
{{ Page(pagination, route, pagination.pages, label="last", workspace_id=workspace_id) }} {{ Page(pagination, route, pagination.pages, label="last", portfolio_id=portfolio_id) }}
{% endif %} {% endif %}
</div> </div>

View File

@ -1,7 +1,7 @@
Join this JEDI Cloud Portfolio Join this JEDI Cloud Portfolio
{{ owner }} has invited you to join a JEDI Cloud Portfolio. Login now to view or use your JEDI Cloud resources. {{ owner }} has invited you to join a JEDI Cloud Portfolio. Login now to view or use your JEDI Cloud resources.
{{ url_for("workspaces.accept_invitation", token=token, _external=True) }} {{ url_for("portfolios.accept_invitation", token=token, _external=True) }}
What is JEDI Cloud? What is JEDI Cloud?
JEDI Cloud is a DoD enterprise-wide solution for commercial cloud services. JEDI Cloud is a DoD enterprise-wide solution for commercial cloud services.

View File

@ -1,6 +1,6 @@
{% from "components/sidenav_item.html" import SidenavItem %} {% from "components/sidenav_item.html" import SidenavItem %}
<div class="global-navigation sidenav {% if workspace %}global-navigation__context--workspace{% endif %}"> <div class="global-navigation sidenav {% if portfolio %}global-navigation__context--portfolio{% endif %}">
<ul> <ul>
{{ SidenavItem("New Task Order", {{ SidenavItem("New Task Order",
href=url_for("task_orders.get_started"), href=url_for("task_orders.get_started"),
@ -8,8 +8,8 @@
active=g.matchesPath('/task_orders/new'), active=g.matchesPath('/task_orders/new'),
) }} ) }}
{% if g.current_user.has_workspaces %} {% if g.current_user.has_portfolios %}
{{ SidenavItem("Portfolios", href="/workspaces", icon="cloud", active=g.matchesPath('/workspaces')) }} {{ SidenavItem("Portfolios", href="/portfolios", icon="cloud", active=g.matchesPath('/portfolios')) }}
{% endif %} {% endif %}
{% if g.Authorization.has_atat_permission(g.current_user, g.Permissions.VIEW_AUDIT_LOG) %} {% if g.Authorization.has_atat_permission(g.current_user, g.Permissions.VIEW_AUDIT_LOG) %}

View File

@ -0,0 +1,68 @@
{% from "components/sidenav_item.html" import SidenavItem %}
<nav class='sidenav portfolio-navigation'>
<ul>
{{ SidenavItem(
("navigation.portfolio_navigation.applications" | translate),
href=url_for("portfolios.portfolio_applications", portfolio_id=portfolio.id),
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/applications'),
subnav=None if not user_can(permissions.ADD_APPLICATION_IN_WORKSPACE) else [
{
"label": ("navigation.portfolio_navigation.add_new_application_label" | translate),
"href": url_for('portfolios.new_application', portfolio_id=portfolio.id),
"active": g.matchesPath('\/portfolios\/[A-Za-z0-9-]*\/applications'),
"icon": "plus"
}
]
) }}
{% if user_can(permissions.VIEW_WORKSPACE_MEMBERS) %}
{{ SidenavItem(
("navigation.portfolio_navigation.members" | translate),
href=url_for("portfolios.portfolio_members", portfolio_id=portfolio.id),
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/members'),
subnav=None if not user_can(permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE) else [
{
"label": ("navigation.portfolio_navigation.add_new_member_label" | translate),
"href": url_for("portfolios.new_member", portfolio_id=portfolio.id),
"active": request.url_rule.rule.startswith('/portfolios/<portfolio_id>/members/new'),
"icon": "plus"
}
]
) }}
{% endif %}
{% if user_can(permissions.VIEW_USAGE_DOLLARS) %}
{{ SidenavItem(
("navigation.portfolio_navigation.budget_report" | translate),
href=url_for("portfolios.portfolio_reports", portfolio_id=portfolio.id),
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/reports')
) }}
{% endif %}
{{ SidenavItem(
("navigation.portfolio_navigation.task_orders" | translate),
href=url_for("portfolios.portfolio_task_orders", portfolio_id=portfolio.id),
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/task_order'),
subnav=None
) }}
{% if user_can(permissions.EDIT_WORKSPACE_INFORMATION) %}
{{ SidenavItem(
("navigation.portfolio_navigation.portfolio_settings" | translate),
href=url_for("portfolios.portfolio", portfolio_id=portfolio.id),
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/edit'),
subnav=None
) }}
{% endif %}
{% if user_can(permissions.VIEW_WORKSPACE_AUDIT_LOG) %}
{{ SidenavItem(
("navigation.portfolio_navigation.activity_log" | translate),
href=url_for("portfolios.portfolio_activity", portfolio_id=portfolio.id),
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/activity')
) }}
{% endif %}
</ul>
</nav>

View File

@ -2,7 +2,7 @@
<header class="topbar"> <header class="topbar">
<nav class="topbar__navigation"> <nav class="topbar__navigation">
{% if not workspace %} {% if not portfolio %}
<a href="{{ url_for('atst.home') }}" class="topbar__link topbar__link--home"> <a href="{{ url_for('atst.home') }}" class="topbar__link topbar__link--home">
{{ Icon('shield', classes='topbar__link-icon') }} {{ Icon('shield', classes='topbar__link-icon') }}
<span class="topbar__link-label"> <span class="topbar__link-label">
@ -15,31 +15,31 @@
</a> </a>
{% endif %} {% endif %}
<div class="topbar__context {% if workspace %}topbar__context--workspace{% endif %}"> <div class="topbar__context {% if portfolio %}topbar__context--portfolio{% endif %}">
{% if workspace %} {% if portfolio %}
<div is='toggler' class='topbar__workspace-menu'> <div is='toggler' class='topbar__portfolio-menu'>
<template slot-scope='props'> <template slot-scope='props'>
<button <button
v-on:click='props.toggle' v-on:click='props.toggle'
class="topbar__link topbar__workspace-menu__toggle" class="topbar__link topbar__portfolio-menu__toggle"
v-bind:class="{ 'topbar__workspace-menu__toggle--open': props.isVisible }"> v-bind:class="{ 'topbar__portfolio-menu__toggle--open': props.isVisible }">
<span class="topbar__link-label">{{ "navigation.topbar.named_workspace" | translate({ "workspace": workspace.name }) }}</span> <span class="topbar__link-label">{{ "navigation.topbar.named_portfolio" | translate({ "portfolio": portfolio.name }) }}</span>
<template v-if='props.isVisible'>{{ Icon('caret_up', classes='topbar__link-icon') }}</template> <template v-if='props.isVisible'>{{ Icon('caret_up', classes='topbar__link-icon') }}</template>
<template v-else>{{ Icon('caret_down', classes='topbar__link-icon') }}</template> <template v-else>{{ Icon('caret_down', classes='topbar__link-icon') }}</template>
</button> </button>
<div v-show='props.isVisible' class='topbar__workspace-menu__panel menu'> <div v-show='props.isVisible' class='topbar__portfolio-menu__panel menu'>
<h2 class='menu__heading'> <h2 class='menu__heading'>
{{ "navigation.topbar.other_active_workspaces" | translate }} {{ "navigation.topbar.other_active_portfolios" | translate }}
</h2> </h2>
{% if workspaces %} {% if portfolios %}
<ul class='menu__list'> <ul class='menu__list'>
{% for other_workspace in workspaces %} {% for other_portfolio in portfolios %}
<li class='menu__list__item'> <li class='menu__list__item'>
<a href="{{ url_for('workspaces.show_workspace', workspace_id=other_workspace.id)}}"> <a href="{{ url_for('portfolios.show_portfolio', portfolio_id=other_portfolio.id)}}">
{{ other_workspace.name }} {{ other_portfolio.name }}
{{ Icon('caret_right', classes='topbar__link-icon') }} {{ Icon('caret_right', classes='topbar__link-icon') }}
</a> </a>
</li> </li>
@ -49,7 +49,7 @@
{% else %} {% else %}
<p class='menu__message'> <p class='menu__message'>
{{ "navigation.topbar.no_other_active_workspaces" | translate }} {{ "navigation.topbar.no_other_active_portfolios" | translate }}
</p> </p>
{% endif %} {% endif %}

View File

@ -1,68 +0,0 @@
{% from "components/sidenav_item.html" import SidenavItem %}
<nav class='sidenav workspace-navigation'>
<ul>
{{ SidenavItem(
("navigation.workspace_navigation.applications" | translate),
href=url_for("workspaces.workspace_applications", workspace_id=workspace.id),
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/applications'),
subnav=None if not user_can(permissions.ADD_APPLICATION_IN_WORKSPACE) else [
{
"label": ("navigation.workspace_navigation.add_new_application_label" | translate),
"href": url_for('workspaces.new_application', workspace_id=workspace.id),
"active": g.matchesPath('\/workspaces\/[A-Za-z0-9-]*\/applications'),
"icon": "plus"
}
]
) }}
{% if user_can(permissions.VIEW_WORKSPACE_MEMBERS) %}
{{ SidenavItem(
("navigation.workspace_navigation.members" | translate),
href=url_for("workspaces.workspace_members", workspace_id=workspace.id),
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/members'),
subnav=None if not user_can(permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE) else [
{
"label": ("navigation.workspace_navigation.add_new_member_label" | translate),
"href": url_for("workspaces.new_member", workspace_id=workspace.id),
"active": request.url_rule.rule.startswith('/workspaces/<workspace_id>/members/new'),
"icon": "plus"
}
]
) }}
{% endif %}
{% if user_can(permissions.VIEW_USAGE_DOLLARS) %}
{{ SidenavItem(
("navigation.workspace_navigation.budget_report" | translate),
href=url_for("workspaces.workspace_reports", workspace_id=workspace.id),
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/reports')
) }}
{% endif %}
{{ SidenavItem(
("navigation.workspace_navigation.task_orders" | translate),
href=url_for("workspaces.workspace_task_orders", workspace_id=workspace.id),
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/task_order'),
subnav=None
) }}
{% if user_can(permissions.EDIT_WORKSPACE_INFORMATION) %}
{{ SidenavItem(
("navigation.workspace_navigation.workspace_settings" | translate),
href=url_for("workspaces.workspace", workspace_id=workspace.id),
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/edit'),
subnav=None
) }}
{% endif %}
{% if user_can(permissions.VIEW_WORKSPACE_AUDIT_LOG) %}
{{ SidenavItem(
("navigation.workspace_navigation.activity_log" | translate),
href=url_for("workspaces.workspace_activity", workspace_id=workspace.id),
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/activity')
) }}
{% endif %}
</ul>
</nav>

View File

@ -0,0 +1,9 @@
{% extends "portfolios/base.html" %}
{% from "components/pagination.html" import Pagination %}
{% block portfolio_content %}
<div v-cloak>
{% include "fragments/audit_events_log.html" %}
{{ Pagination(audit_events, 'portfolios.portfolio_activity', portfolio_id=portfolio_id) }}
</div>
{% endblock %}

View File

@ -1,10 +1,10 @@
{% extends "workspaces/base.html" %} {% extends "portfolios/base.html" %}
{% from "components/text_input.html" import TextInput %} {% from "components/text_input.html" import TextInput %}
{% block workspace_content %} {% block portfolio_content %}
<form method="POST" action="{{ url_for('workspaces.edit_application', workspace_id=workspace.id, application_id=application.id) }}"> <form method="POST" action="{{ url_for('portfolios.edit_application', portfolio_id=portfolio.id, application_id=application.id) }}">
{% include "fragments/edit_application_form.html" %} {% include "fragments/edit_application_form.html" %}

View File

@ -1,31 +1,31 @@
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/empty_state.html" import EmptyState %} {% from "components/empty_state.html" import EmptyState %}
{% extends "workspaces/base.html" %} {% extends "portfolios/base.html" %}
{% block workspace_content %} {% block portfolio_content %}
{% if not workspace.applications %} {% if not portfolio.applications %}
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_WORKSPACE) %} {% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_WORKSPACE) %}
{{ EmptyState( {{ EmptyState(
'This portfolio doesnt have any applications yet.', 'This portfolio doesnt have any applications yet.',
action_label='Add a New Application' if can_create_applications else None, action_label='Add a New Application' if can_create_applications else None,
action_href=url_for('workspaces.new_application', workspace_id=workspace.id) if can_create_applications else None, action_href=url_for('portfolios.new_application', portfolio_id=portfolio.id) if can_create_applications else None,
icon='cloud', icon='cloud',
sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.' sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.'
) }} ) }}
{% else %} {% else %}
{% for application in workspace.applications %} {% for application in portfolio.applications %}
<div v-cloak class='block-list application-list-item'> <div v-cloak class='block-list application-list-item'>
<header class='block-list__header'> <header class='block-list__header'>
<h2 class='block-list__title'>{{ application.name }} ({{ application.environments|length }} environments)</h2> <h2 class='block-list__title'>{{ application.name }} ({{ application.environments|length }} environments)</h2>
{% if user_can(permissions.RENAME_APPLICATION_IN_WORKSPACE) %} {% if user_can(permissions.RENAME_APPLICATION_IN_WORKSPACE) %}
<a class='icon-link' href='{{ url_for("workspaces.edit_application", workspace_id=workspace.id, application_id=application.id) }}'> <a class='icon-link' href='{{ url_for("portfolios.edit_application", portfolio_id=portfolio.id, application_id=application.id) }}'>
{{ Icon('edit') }} {{ Icon('edit') }}
<span>edit</span> <span>edit</span>
</a> </a>
@ -34,7 +34,7 @@
<ul> <ul>
{% for environment in application.environments %} {% for environment in application.environments %}
<li class='block-list__item application-list-item__environment'> <li class='block-list__item application-list-item__environment'>
<a href='{{ url_for("workspaces.access_environment", workspace_id=workspace.id, environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer' class='application-list-item__environment__link'> <a href='{{ url_for("portfolios.access_environment", portfolio_id=portfolio.id, environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer' class='application-list-item__environment__link'>
{{ Icon('link') }} {{ Icon('link') }}
<span>{{ environment.name }}</span> <span>{{ environment.name }}</span>
</a> </a>

View File

@ -1,17 +1,17 @@
{% extends "workspaces/base.html" %} {% extends "portfolios/base.html" %}
{% from "components/alert.html" import Alert %} {% from "components/alert.html" import Alert %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/modal.html" import Modal %} {% from "components/modal.html" import Modal %}
{% from "components/text_input.html" import TextInput %} {% from "components/text_input.html" import TextInput %}
{% block workspace_content %} {% block portfolio_content %}
{% set modalName = "newApplicationConfirmation" %} {% set modalName = "newApplicationConfirmation" %}
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}
<new-application inline-template v-bind:initial-data='{{ form.data|tojson }}' modal-name='{{ modalName }}'> <new-application inline-template v-bind:initial-data='{{ form.data|tojson }}' modal-name='{{ modalName }}'>
<form method="POST" action="{{ url_for('workspaces.create_application', workspace_id=workspace.id) }}" v-on:submit="handleSubmit"> <form method="POST" action="{{ url_for('portfolios.create_application', portfolio_id=portfolio.id) }}" v-on:submit="handleSubmit">
{% call Modal(name=modalName, dismissable=False) %} {% call Modal(name=modalName, dismissable=False) %}
<h1>Create application !{ name }</h1> <h1>Create application !{ name }</h1>

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block content %}
<div class='portfolio-panel-container'>
<div class='col'>
{% include 'navigation/portfolio_navigation.html' %}
</div>
<div class='col col--grow'>
{% block portfolio_content %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -1,13 +1,13 @@
{% extends "workspaces/base.html" %} {% extends "portfolios/base.html" %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/text_input.html" import TextInput %} {% from "components/text_input.html" import TextInput %}
{% block workspace_content %} {% block portfolio_content %}
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}
<form method="POST" action="{{ url_for('workspaces.edit_workspace', workspace_id=workspace.id) }}" autocomplete="false"> <form method="POST" action="{{ url_for('portfolios.edit_portfolio', portfolio_id=portfolio.id) }}" autocomplete="false">
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="panel"> <div class="panel">
@ -17,14 +17,14 @@
</div> </div>
<div class="panel__content"> <div class="panel__content">
{{ TextInput(form.name, validation="workspaceName") }} {{ TextInput(form.name, validation="portfolioName") }}
</div> </div>
</div> </div>
<div class='action-group'> <div class='action-group'>
<button type="submit" class="usa-button usa-button-big usa-button-primary" tabindex="0">Save</button> <button type="submit" class="usa-button usa-button-big usa-button-primary" tabindex="0">Save</button>
<a href='{{ url_for("workspaces.workspace_applications", workspace_id=workspace.id) }}' class='action-group__action icon-link'> <a href='{{ url_for("portfolios.portfolio_applications", portfolio_id=portfolio.id) }}' class='action-group__action icon-link'>
{{ Icon('x') }} {{ Icon('x') }}
<span>Cancel</span> <span>Cancel</span>
</a> </a>

View File

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

View File

@ -10,7 +10,7 @@
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}
<form method="POST" action="{{ url_for('workspaces.update_member', workspace_id=workspace.id, member_id=member.user_id) }}" autocomplete="false"> <form method="POST" action="{{ url_for('portfolios.update_member', portfolio_id=portfolio.id, member_id=member.user_id) }}" autocomplete="false">
{{ form.csrf_token }} {{ form.csrf_token }}
<div class='panel member-card'> <div class='panel member-card'>
@ -18,7 +18,7 @@
<h1 class='member-card__heading'>{{ member.user.full_name }}</h1> <h1 class='member-card__heading'>{{ member.user.full_name }}</h1>
<div class="usa-input member-card__input"> <div class="usa-input member-card__input">
{{ Selector(form.workspace_role) }} {{ Selector(form.portfolio_role) }}
</div> </div>
</div> </div>
@ -40,20 +40,20 @@
{% if member.latest_invitation.is_revokable %} {% if member.latest_invitation.is_revokable %}
{{ ConfirmationButton( {{ ConfirmationButton(
"Revoke Invitation", "Revoke Invitation",
url_for("workspaces.revoke_invitation", workspace_id=workspace.id, token=member.latest_invitation.token), url_for("portfolios.revoke_invitation", portfolio_id=portfolio.id, token=member.latest_invitation.token),
) }} ) }}
{% endif %} {% endif %}
{% if member.can_resend_invitation %} {% if member.can_resend_invitation %}
{{ ConfirmationButton ( {{ ConfirmationButton (
"Resend Invitation", "Resend Invitation",
url_for("workspaces.resend_invitation", workspace_id=workspace.id, token=member.latest_invitation.token), url_for("portfolios.resend_invitation", portfolio_id=portfolio.id, token=member.latest_invitation.token),
confirm_msg="Are you sure? This will send an email to invite the user to join this portfolio." confirm_msg="Are you sure? This will send an email to invite the user to join this portfolio."
)}} )}}
{% endif %} {% endif %}
{% if can_revoke_access %} {% if can_revoke_access %}
{{ ConfirmationButton ( {{ ConfirmationButton (
"Remove Portfolio Access", "Remove Portfolio Access",
url_for("workspaces.revoke_access", workspace_id=workspace.id, member_id=member.id), url_for("portfolios.revoke_access", portfolio_id=portfolio.id, member_id=member.id),
confirm_msg="Are you sure? This will remove this user from the portfolio.", confirm_msg="Are you sure? This will remove this user from the portfolio.",
)}} )}}
{% endif %} {% endif %}
@ -177,7 +177,7 @@
<button class='action-group__action usa-button usa-button-big'> <button class='action-group__action usa-button usa-button-big'>
{% if is_new_member %}Create{% else %}Save{% endif %} {% if is_new_member %}Create{% else %}Save{% endif %}
</button> </button>
<a href='{{ url_for("workspaces.workspace_members", workspace_id=workspace.id) }}' class='action-group__action icon-link'> <a href='{{ url_for("portfolios.portfolio_members", portfolio_id=portfolio.id) }}' class='action-group__action icon-link'>
{{ Icon('x') }} {{ Icon('x') }}
<span>Cancel</span> <span>Cancel</span>
</a> </a>

View File

@ -1,11 +1,11 @@
{% extends "workspaces/base.html" %} {% extends "portfolios/base.html" %}
{% from "components/empty_state.html" import EmptyState %} {% from "components/empty_state.html" import EmptyState %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% block workspace_content %} {% block portfolio_content %}
{% if not workspace.members %} {% if not portfolio.members %}
{% set user_can_invite = user_can(permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE) %} {% set user_can_invite = user_can(permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE) %}

View File

@ -7,7 +7,7 @@
{% block content %} {% block content %}
<form method="POST" action="{{ url_for('workspaces.create_member', workspace_id=workspace.id) }}" autocomplete="false"> <form method="POST" action="{{ url_for('portfolios.create_member', portfolio_id=portfolio.id) }}" autocomplete="false">
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="panel"> <div class="panel">
@ -22,14 +22,14 @@
{{ TextInput(form.last_name) }} {{ TextInput(form.last_name) }}
{{ TextInput(form.email,placeholder='jane@mail.mil', validation='email') }} {{ TextInput(form.email,placeholder='jane@mail.mil', validation='email') }}
{{ TextInput(form.dod_id,placeholder='10-digit number on the back of the CAC', validation='dodId') }} {{ TextInput(form.dod_id,placeholder='10-digit number on the back of the CAC', validation='dodId') }}
{{ Selector(form.workspace_role) }} {{ Selector(form.portfolio_role) }}
</div> </div>
</div> </div>
<div class='action-group'> <div class='action-group'>
<button class="usa-button usa-button-big usa-button-primary" tabindex="0">Add User</button> <button class="usa-button usa-button-big usa-button-primary" tabindex="0">Add User</button>
<a href='{{ url_for("workspaces.workspace_members", workspace_id=workspace.id) }}' class='action-group__action icon-link'> <a href='{{ url_for("portfolios.portfolio_members", portfolio_id=portfolio.id) }}' class='action-group__action icon-link'>
{{ Icon('x') }} {{ Icon('x') }}
<span>Cancel</span> <span>Cancel</span>
</a> </a>

View File

@ -1,12 +1,12 @@
{% extends "workspaces/base.html" %} {% extends "portfolios/base.html" %}
{% from "components/alert.html" import Alert %} {% from "components/alert.html" import Alert %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/empty_state.html" import EmptyState %} {% from "components/empty_state.html" import EmptyState %}
{% block workspace_content %} {% block portfolio_content %}
{{ Alert("Budget Report for Portfolio " + workspace.name, {{ Alert("Budget Report for Portfolio " + portfolio.name,
message="<p>Track your monthly and cumulative expenditures for your portfolio, applications, and environments below.</p>\ message="<p>Track your monthly and cumulative expenditures for your portfolio, applications, and environments below.</p>\
<p>Please note that the projected spend is based on the <em>average expense over the last three completed months</em> and therefore does not account for future changes that might be made in scale or configuration of your cloud services.</p>", <p>Please note that the projected spend is based on the <em>average expense over the last three completed months</em> and therefore does not account for future changes that might be made in scale or configuration of your cloud services.</p>",
actions=[ actions=[
@ -20,8 +20,8 @@
<div class='row'> <div class='row'>
<h2 class='spend-summary__heading col'>Portfolio Total Spend</h2> <h2 class='spend-summary__heading col'>Portfolio Total Spend</h2>
<dl class='spend-summary__budget'> <dl class='spend-summary__budget'>
{% set budget = workspace_totals['budget'] %} {% set budget = portfolio_totals['budget'] %}
{% set spent = workspace_totals['spent'] %} {% set spent = portfolio_totals['spent'] %}
{% set remaining = budget - spent %} {% set remaining = budget - spent %}
<div> <div>
<dt>Budget </dt> <dt>Budget </dt>
@ -88,7 +88,7 @@
</div> </div>
</dl> </dl>
<a href='{{ url_for("workspaces.workspace", workspace_id=workspace.id) }}' class='icon-link'> <a href='{{ url_for("portfolios.portfolio", portfolio_id=portfolio.id) }}' class='icon-link'>
Manage Task Order Manage Task Order
</a> </a>
</div> </div>
@ -107,13 +107,13 @@
</div> </div>
{% set workspace_totals = monthly_totals['workspace'] %} {% set portfolio_totals = monthly_totals['portfolio'] %}
{% set current_month_index = current_month.strftime('%m/%Y') %} {% set current_month_index = current_month.strftime('%m/%Y') %}
{% set prev_month_index = prev_month.strftime('%m/%Y') %} {% set prev_month_index = prev_month.strftime('%m/%Y') %}
{% set two_months_ago_index = two_months_ago.strftime('%m/%Y') %} {% set two_months_ago_index = two_months_ago.strftime('%m/%Y') %}
{% set reports_url = url_for("workspaces.workspace_reports", workspace_id=workspace.id) %} {% set reports_url = url_for("portfolios.portfolio_reports", portfolio_id=portfolio.id) %}
{% if not workspace.applications %} {% if not portfolio.applications %}
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_WORKSPACE) %} {% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_WORKSPACE) %}
{% set message = 'This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.' {% set message = 'This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.'
@ -124,7 +124,7 @@
{{ EmptyState( {{ EmptyState(
'Nothing to report', 'Nothing to report',
action_label='Add a New Application' if can_create_applications else None, action_label='Add a New Application' if can_create_applications else None,
action_href=url_for('workspaces.new_application', workspace_id=workspace.id) if can_create_applications else None, action_href=url_for('portfolios.new_application', portfolio_id=portfolio.id) if can_create_applications else None,
icon='chart', icon='chart',
sub_message=message sub_message=message
) }} ) }}
@ -338,8 +338,8 @@
{% if month.month == current_month.month and month.year == current_month.year %} {% if month.month == current_month.month and month.year == current_month.year %}
selected='selected' selected='selected'
{% endif %} {% endif %}
value='{{ url_for("workspaces.workspace_reports", value='{{ url_for("portfolios.portfolio_reports",
workspace_id=workspace.id, portfolio_id=portfolio.id,
month=month.month, month=month.month,
year=month.year) }}' year=month.year) }}'
> >
@ -354,7 +354,7 @@
<spend-table <spend-table
v-bind:applications='{{ monthly_totals['applications'] | tojson }}' v-bind:applications='{{ monthly_totals['applications'] | tojson }}'
v-bind:workspace='{{ workspace_totals | tojson }}' v-bind:portfolio='{{ portfolio_totals | tojson }}'
v-bind:environments='{{ monthly_totals['environments'] | tojson }}' v-bind:environments='{{ monthly_totals['environments'] | tojson }}'
current-month-index='{{ current_month_index }}' current-month-index='{{ current_month_index }}'
prev-month-index='{{ prev_month_index }}' prev-month-index='{{ prev_month_index }}'
@ -369,14 +369,14 @@
<th class='current-month'>% of total spend this month</th> <th class='current-month'>% of total spend this month</th>
</thead> </thead>
<tbody class='spend-table__workspace'> <tbody class='spend-table__portfolio'>
<tr> <tr>
<th scope='row'>Total</th> <th scope='row'>Total</th>
<td class='table-cell--align-right previous-month'>{{ workspace_totals.get(two_months_ago_index, 0) | dollars }}</td> <td class='table-cell--align-right previous-month'>{{ portfolio_totals.get(two_months_ago_index, 0) | dollars }}</td>
<td class='table-cell--align-right previous-month'>{{ workspace_totals.get(prev_month_index, 0) | dollars }}</td> <td class='table-cell--align-right previous-month'>{{ portfolio_totals.get(prev_month_index, 0) | dollars }}</td>
<td class='table-cell--align-right current-month'>{{ workspace_totals.get(current_month_index, 0) | dollars }}</td> <td class='table-cell--align-right current-month'>{{ portfolio_totals.get(current_month_index, 0) | dollars }}</td>
<td class='table-cell--expand current-month meter-cell'> <td class='table-cell--expand current-month meter-cell'>
<meter value='{{ workspace_totals.get(current_month_index, 0) }}' min='0' max='{{ workspace_totals.get(current_month_index, 0) }}'> <meter value='{{ portfolio_totals.get(current_month_index, 0) }}' min='0' max='{{ portfolio_totals.get(current_month_index, 0) }}'>
<div class='meter__fallback' style='width: 100%'></div> <div class='meter__fallback' style='width: 100%'></div>
</meter> </meter>
</td> </td>
@ -406,10 +406,10 @@
<td class='table-cell--expand current-month meter-cell'> <td class='table-cell--expand current-month meter-cell'>
<span class='spend-table__meter-value'> <span class='spend-table__meter-value'>
<span v-html='round( 100 * ((application[currentMonthIndex] || 0) / (workspace[currentMonthIndex] || 1) )) + "%"'></span> <span v-html='round( 100 * ((application[currentMonthIndex] || 0) / (portfolio[currentMonthIndex] || 1) )) + "%"'></span>
</span> </span>
<meter v-bind:value='application[currentMonthIndex] || 0' min='0' v-bind:max='workspace[currentMonthIndex] || 1'> <meter v-bind:value='application[currentMonthIndex] || 0' min='0' v-bind:max='portfolio[currentMonthIndex] || 1'>
<div class='meter__fallback' v-bind:style='"width:" + round( 100 * ((application[currentMonthIndex] || 0) / (workspace[currentMonthIndex] || 1) )) + "%;"'></div> <div class='meter__fallback' v-bind:style='"width:" + round( 100 * ((application[currentMonthIndex] || 0) / (portfolio[currentMonthIndex] || 1) )) + "%;"'></div>
</meter> </meter>
</td> </td>
</tr> </tr>

View File

@ -1,24 +1,24 @@
{% from "components/empty_state.html" import EmptyState %} {% from "components/empty_state.html" import EmptyState %}
{% extends "workspaces/base.html" %} {% extends "portfolios/base.html" %}
{% block workspace_content %} {% block portfolio_content %}
{% if not workspace.task_orders %} {% if not portfolio.task_orders %}
{{ EmptyState( {{ EmptyState(
'This portfolio doesnt have any task orders yet.', 'This portfolio doesnt have any task orders yet.',
action_label='Add a New Task Order', action_label='Add a New Task Order',
action_href=url_for('task_orders.new', screen=1, workspace_id=workspace.id), action_href=url_for('task_orders.new', screen=1, portfolio_id=portfolio.id),
icon='cloud', icon='cloud',
) }} ) }}
{% else %} {% else %}
<ul> <ul>
{% for task_order in workspace.task_orders %} {% for task_order in portfolio.task_orders %}
<li class='block-list__item'> <li class='block-list__item'>
<a href='{{ url_for("workspaces.view_task_order", workspace_id=workspace.id, task_order_id=task_order.id)}}'> <a href='{{ url_for("portfolios.view_task_order", portfolio_id=portfolio.id, task_order_id=task_order.id)}}'>
<span>{{ task_order.start_date }} - {{ task_order.end_date }}</span> <span>{{ task_order.start_date }} - {{ task_order.end_date }}</span>
</a> </a>
</li> </li>

View File

@ -0,0 +1,7 @@
{% extends "portfolios/base.html" %}
{% block portfolio_content %}
You're looking at TO {{ task_order.id }}
{% endblock %}

View File

@ -50,9 +50,9 @@
{% if not requests %} {% if not requests %}
{{ EmptyState( {{ EmptyState(
("requests.index.no_workspaces_label" | translate), ("requests.index.no_portfolios_label" | translate),
sub_message=("requests.index.no_workspaces_sub_message" | translate), sub_message=("requests.index.no_portfolios_sub_message" | translate),
action_label=("requests.index.no_workspaces_action_label" | translate), action_label=("requests.index.no_portfolios_action_label" | translate),
action_href=url_for('requests.requests_form_new', screen=1), action_href=url_for('requests.requests_form_new', screen=1),
icon='document' icon='document'
) }} ) }}
@ -146,7 +146,7 @@
{% endif %} {% endif %}
<td>!{ dollars(r.annual_usage) }</td> <td>!{ dollars(r.annual_usage) }</td>
<td> <td>
<a v-if="r.is_approved" class="icon-link icon-link--large" :href="r.workspace_link"> <a v-if="r.is_approved" class="icon-link icon-link--large" :href="r.portfolio_link">
!{ r.status } !{ r.status }
</a> </a>
<span v-else> <span v-else>

View File

@ -135,7 +135,7 @@
) )
}} }}
{{ DateInput(f.start_date, placeholder='MM / DD / YYYY', validation='date') }} {{ DateInput(f.start_date, placeholder='MM / DD / YYYY', validation='date') }}
{{ TextInput(f.name, placeholder='Request Name', validation='workspaceName') }} {{ TextInput(f.name, placeholder='Request Name', validation='portfolioName') }}
</div> </div>
</details-of-use> </details-of-use>

View File

@ -1,9 +0,0 @@
{% extends "workspaces/base.html" %}
{% from "components/pagination.html" import Pagination %}
{% block workspace_content %}
<div v-cloak>
{% include "fragments/audit_events_log.html" %}
{{ Pagination(audit_events, 'workspaces.workspace_activity', workspace_id=workspace_id) }}
</div>
{% endblock %}

View File

@ -1,15 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class='workspace-panel-container'>
<div class='col'>
{% include 'navigation/workspace_navigation.html' %}
</div>
<div class='col col--grow'>
{% block workspace_content %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -1,35 +1,35 @@
from atst.domain.applications import Applications from atst.domain.applications import Applications
from tests.factories import RequestFactory, UserFactory, WorkspaceFactory from tests.factories import RequestFactory, UserFactory, PortfolioFactory
from atst.domain.workspaces import Workspaces from atst.domain.portfolios import Portfolios
def test_create_application_with_multiple_environments(): def test_create_application_with_multiple_environments():
request = RequestFactory.create() request = RequestFactory.create()
workspace = Workspaces.create_from_request(request) portfolio = Portfolios.create_from_request(request)
application = Applications.create( application = Applications.create(
workspace.owner, workspace, "My Test Application", "Test", ["dev", "prod"] portfolio.owner, portfolio, "My Test Application", "Test", ["dev", "prod"]
) )
assert application.workspace == workspace assert application.portfolio == portfolio
assert application.name == "My Test Application" assert application.name == "My Test Application"
assert application.description == "Test" assert application.description == "Test"
assert sorted(e.name for e in application.environments) == ["dev", "prod"] assert sorted(e.name for e in application.environments) == ["dev", "prod"]
def test_workspace_owner_can_view_environments(): def test_portfolio_owner_can_view_environments():
owner = UserFactory.create() owner = UserFactory.create()
workspace = WorkspaceFactory.create( portfolio = PortfolioFactory.create(
owner=owner, owner=owner,
applications=[{"environments": [{"name": "dev"}, {"name": "prod"}]}], applications=[{"environments": [{"name": "dev"}, {"name": "prod"}]}],
) )
application = Applications.get(owner, workspace, workspace.applications[0].id) application = Applications.get(owner, portfolio, portfolio.applications[0].id)
assert len(application.environments) == 2 assert len(application.environments) == 2
def test_can_only_update_name_and_description(): def test_can_only_update_name_and_description():
owner = UserFactory.create() owner = UserFactory.create()
workspace = WorkspaceFactory.create( portfolio = PortfolioFactory.create(
owner=owner, owner=owner,
applications=[ applications=[
{ {
@ -39,11 +39,11 @@ def test_can_only_update_name_and_description():
} }
], ],
) )
application = Applications.get(owner, workspace, workspace.applications[0].id) application = Applications.get(owner, portfolio, portfolio.applications[0].id)
env_name = application.environments[0].name env_name = application.environments[0].name
Applications.update( Applications.update(
owner, owner,
workspace, portfolio,
application, application,
{ {
"name": "New Name", "name": "New Name",

View File

@ -3,11 +3,11 @@ import pytest
from atst.domain.audit_log import AuditLog from atst.domain.audit_log import AuditLog
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
from atst.domain.roles import Roles from atst.domain.roles import Roles
from atst.models.workspace_role import Status as WorkspaceRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from tests.factories import ( from tests.factories import (
UserFactory, UserFactory,
WorkspaceFactory, PortfolioFactory,
WorkspaceRoleFactory, PortfolioRoleFactory,
ApplicationFactory, ApplicationFactory,
) )
@ -42,69 +42,69 @@ def test_paginate_audit_log(ccpo):
def test_ccpo_can_view_ws_audit_log(ccpo): def test_ccpo_can_view_ws_audit_log(ccpo):
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
events = AuditLog.get_workspace_events(ccpo, workspace) events = AuditLog.get_portfolio_events(ccpo, portfolio)
assert len(events) > 0 assert len(events) > 0
def test_ws_admin_can_view_ws_audit_log(): def test_ws_admin_can_view_ws_audit_log():
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
admin = UserFactory.create() admin = UserFactory.create()
WorkspaceRoleFactory.create( PortfolioRoleFactory.create(
workspace=workspace, portfolio=portfolio,
user=admin, user=admin,
role=Roles.get("admin"), role=Roles.get("admin"),
status=WorkspaceRoleStatus.ACTIVE, status=PortfolioRoleStatus.ACTIVE,
) )
events = AuditLog.get_workspace_events(admin, workspace) events = AuditLog.get_portfolio_events(admin, portfolio)
assert len(events) > 0 assert len(events) > 0
def test_ws_owner_can_view_ws_audit_log(): def test_ws_owner_can_view_ws_audit_log():
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
events = AuditLog.get_workspace_events(workspace.owner, workspace) events = AuditLog.get_portfolio_events(portfolio.owner, portfolio)
assert len(events) > 0 assert len(events) > 0
def test_other_users_cannot_view_ws_audit_log(): def test_other_users_cannot_view_ws_audit_log():
with pytest.raises(UnauthorizedError): with pytest.raises(UnauthorizedError):
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
dev = UserFactory.create() dev = UserFactory.create()
WorkspaceRoleFactory.create( PortfolioRoleFactory.create(
workspace=workspace, portfolio=portfolio,
user=dev, user=dev,
role=Roles.get("developer"), role=Roles.get("developer"),
status=WorkspaceRoleStatus.ACTIVE, status=PortfolioRoleStatus.ACTIVE,
) )
AuditLog.get_workspace_events(dev, workspace) AuditLog.get_portfolio_events(dev, portfolio)
def test_paginate_ws_audit_log(): def test_paginate_ws_audit_log():
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
application = ApplicationFactory.create(workspace=workspace) application = ApplicationFactory.create(portfolio=portfolio)
for _ in range(100): for _ in range(100):
AuditLog.log_system_event( AuditLog.log_system_event(
resource=application, action="create", workspace=workspace resource=application, action="create", portfolio=portfolio
) )
events = AuditLog.get_workspace_events( events = AuditLog.get_portfolio_events(
workspace.owner, workspace, pagination_opts={"per_page": 25, "page": 2} portfolio.owner, portfolio, pagination_opts={"per_page": 25, "page": 2}
) )
assert len(events) == 25 assert len(events) == 25
def test_ws_audit_log_only_includes_current_ws_events(): def test_ws_audit_log_only_includes_current_ws_events():
owner = UserFactory.create() owner = UserFactory.create()
workspace = WorkspaceFactory.create(owner=owner) portfolio = PortfolioFactory.create(owner=owner)
other_workspace = WorkspaceFactory.create(owner=owner) other_portfolio = PortfolioFactory.create(owner=owner)
# Add some audit events # Add some audit events
application_1 = ApplicationFactory.create(workspace=workspace) application_1 = ApplicationFactory.create(portfolio=portfolio)
application_2 = ApplicationFactory.create(workspace=other_workspace) application_2 = ApplicationFactory.create(portfolio=other_portfolio)
events = AuditLog.get_workspace_events(workspace.owner, workspace) events = AuditLog.get_portfolio_events(portfolio.owner, portfolio)
for event in events: for event in events:
assert event.workspace_id == workspace.id or event.resource_id == workspace.id assert event.portfolio_id == portfolio.id or event.resource_id == portfolio.id
assert ( assert (
not event.workspace_id == other_workspace.id not event.portfolio_id == other_portfolio.id
or event.resource_id == other_workspace.id or event.resource_id == other_portfolio.id
) )

View File

@ -1,8 +1,8 @@
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.workspace_roles import WorkspaceRoles from atst.domain.portfolio_roles import PortfolioRoles
from tests.factories import ApplicationFactory, UserFactory, WorkspaceFactory from tests.factories import ApplicationFactory, UserFactory, PortfolioFactory
def test_create_environments(): def test_create_environments():
@ -16,7 +16,7 @@ def test_create_environment_role_creates_cloud_id(session):
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") developer = UserFactory.from_atat_role("developer")
workspace = WorkspaceFactory.create( portfolio = PortfolioFactory.create(
owner=owner, owner=owner,
members=[{"user": developer, "role_name": "developer"}], members=[{"user": developer, "role_name": "developer"}],
applications=[ applications=[
@ -24,23 +24,23 @@ def test_create_environment_role_creates_cloud_id(session):
], ],
) )
env = workspace.applications[0].environments[0] env = portfolio.applications[0].environments[0]
new_role = [{"id": env.id, "role": "developer"}] new_role = [{"id": env.id, "role": "developer"}]
workspace_role = workspace.members[0] portfolio_role = portfolio.members[0]
assert not workspace_role.user.cloud_id assert not portfolio_role.user.cloud_id
assert Environments.update_environment_roles( assert Environments.update_environment_roles(
owner, workspace, workspace_role, new_role owner, portfolio, portfolio_role, new_role
) )
assert workspace_role.user.cloud_id is not None assert portfolio_role.user.cloud_id is not None
def test_update_environment_roles(): def test_update_environment_roles():
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") developer = UserFactory.from_atat_role("developer")
workspace = WorkspaceFactory.create( portfolio = PortfolioFactory.create(
owner=owner, owner=owner,
members=[{"user": developer, "role_name": "developer"}], members=[{"user": developer, "role_name": "developer"}],
applications=[ applications=[
@ -61,19 +61,19 @@ def test_update_environment_roles():
], ],
) )
dev_env = workspace.applications[0].environments[0] dev_env = portfolio.applications[0].environments[0]
staging_env = workspace.applications[0].environments[1] staging_env = portfolio.applications[0].environments[1]
new_ids_and_roles = [ new_ids_and_roles = [
{"id": dev_env.id, "role": "billing_admin"}, {"id": dev_env.id, "role": "billing_admin"},
{"id": staging_env.id, "role": "developer"}, {"id": staging_env.id, "role": "developer"},
] ]
workspace_role = workspace.members[0] portfolio_role = portfolio.members[0]
assert Environments.update_environment_roles( assert Environments.update_environment_roles(
owner, workspace, workspace_role, new_ids_and_roles owner, portfolio, portfolio_role, new_ids_and_roles
) )
new_dev_env_role = EnvironmentRoles.get(workspace_role.user.id, dev_env.id) new_dev_env_role = EnvironmentRoles.get(portfolio_role.user.id, dev_env.id)
staging_env_role = EnvironmentRoles.get(workspace_role.user.id, staging_env.id) staging_env_role = EnvironmentRoles.get(portfolio_role.user.id, staging_env.id)
assert new_dev_env_role.role == "billing_admin" assert new_dev_env_role.role == "billing_admin"
assert staging_env_role.role == "developer" assert staging_env_role.role == "developer"
@ -82,7 +82,7 @@ def test_update_environment_roles():
def test_remove_environment_role(): def test_remove_environment_role():
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") developer = UserFactory.from_atat_role("developer")
workspace = WorkspaceFactory.create( portfolio = PortfolioFactory.create(
owner=owner, owner=owner,
members=[{"user": developer, "role_name": "developer"}], members=[{"user": developer, "role_name": "developer"}],
applications=[ applications=[
@ -109,7 +109,7 @@ def test_remove_environment_role():
], ],
) )
application = workspace.applications[0] application = portfolio.applications[0]
now_ba = application.environments[0].id now_ba = application.environments[0].id
now_none = application.environments[1].id now_none = application.environments[1].id
still_fa = application.environments[2].id still_fa = application.environments[2].id
@ -119,12 +119,12 @@ def test_remove_environment_role():
{"id": now_none, "role": None}, {"id": now_none, "role": None},
] ]
workspace_role = WorkspaceRoles.get(workspace.id, developer.id) portfolio_role = PortfolioRoles.get(portfolio.id, developer.id)
assert Environments.update_environment_roles( assert Environments.update_environment_roles(
owner, workspace, workspace_role, new_environment_roles owner, portfolio, portfolio_role, new_environment_roles
) )
assert workspace_role.num_environment_roles == 2 assert portfolio_role.num_environment_roles == 2
assert EnvironmentRoles.get(developer.id, now_ba).role == "billing_auditor" assert EnvironmentRoles.get(developer.id, now_ba).role == "billing_auditor"
assert EnvironmentRoles.get(developer.id, now_none) is None assert EnvironmentRoles.get(developer.id, now_none) is None
assert EnvironmentRoles.get(developer.id, still_fa).role == "financial_auditor" assert EnvironmentRoles.get(developer.id, still_fa).role == "financial_auditor"
@ -134,7 +134,7 @@ def test_no_update_to_environment_roles():
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") developer = UserFactory.from_atat_role("developer")
workspace = WorkspaceFactory.create( portfolio = PortfolioFactory.create(
owner=owner, owner=owner,
members=[{"user": developer, "role_name": "developer"}], members=[{"user": developer, "role_name": "developer"}],
applications=[ applications=[
@ -150,18 +150,18 @@ def test_no_update_to_environment_roles():
], ],
) )
dev_env = workspace.applications[0].environments[0] dev_env = portfolio.applications[0].environments[0]
new_ids_and_roles = [{"id": dev_env.id, "role": "devops"}] new_ids_and_roles = [{"id": dev_env.id, "role": "devops"}]
workspace_role = WorkspaceRoles.get(workspace.id, developer.id) portfolio_role = PortfolioRoles.get(portfolio.id, developer.id)
assert not Environments.update_environment_roles( assert not Environments.update_environment_roles(
owner, workspace, workspace_role, new_ids_and_roles owner, portfolio, portfolio_role, new_ids_and_roles
) )
def test_get_scoped_environments(db): def test_get_scoped_environments(db):
developer = UserFactory.create() developer = UserFactory.create()
workspace = WorkspaceFactory.create( portfolio = PortfolioFactory.create(
members=[{"user": developer, "role_name": "developer"}], members=[{"user": developer, "role_name": "developer"}],
applications=[ applications=[
{ {
@ -189,8 +189,8 @@ def test_get_scoped_environments(db):
], ],
) )
application1_envs = Environments.for_user(developer, workspace.applications[0]) application1_envs = Environments.for_user(developer, portfolio.applications[0])
assert [env.name for env in application1_envs] == ["application1 dev"] assert [env.name for env in application1_envs] == ["application1 dev"]
application2_envs = Environments.for_user(developer, workspace.applications[1]) application2_envs = Environments.for_user(developer, portfolio.applications[1])
assert [env.name for env in application2_envs] == ["application2 staging"] assert [env.name for env in application2_envs] == ["application2 staging"]

View File

@ -11,8 +11,8 @@ from atst.domain.invitations import (
from atst.models.invitation import Status from atst.models.invitation import Status
from tests.factories import ( from tests.factories import (
WorkspaceFactory, PortfolioFactory,
WorkspaceRoleFactory, PortfolioRoleFactory,
UserFactory, UserFactory,
InvitationFactory, InvitationFactory,
) )
@ -21,22 +21,22 @@ from atst.domain.audit_log import AuditLog
def test_create_invitation(): def test_create_invitation():
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
invite = Invitations.create(workspace.owner, ws_role, user.email) invite = Invitations.create(portfolio.owner, ws_role, user.email)
assert invite.user == user assert invite.user == user
assert invite.workspace_role == ws_role assert invite.portfolio_role == ws_role
assert invite.inviter == workspace.owner assert invite.inviter == portfolio.owner
assert invite.status == Status.PENDING assert invite.status == Status.PENDING
assert re.match(r"^[\w\-_]+$", invite.token) assert re.match(r"^[\w\-_]+$", invite.token)
def test_accept_invitation(): def test_accept_invitation():
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
invite = Invitations.create(workspace.owner, ws_role, user.email) invite = Invitations.create(portfolio.owner, ws_role, user.email)
assert invite.is_pending assert invite.is_pending
accepted_invite = Invitations.accept(user, invite.token) accepted_invite = Invitations.accept(user, invite.token)
assert accepted_invite.is_accepted assert accepted_invite.is_accepted
@ -44,15 +44,15 @@ def test_accept_invitation():
def test_accept_expired_invitation(): def test_accept_expired_invitation():
user = UserFactory.create() user = UserFactory.create()
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
increment = Invitations.EXPIRATION_LIMIT_MINUTES + 1 increment = Invitations.EXPIRATION_LIMIT_MINUTES + 1
expiration_time = datetime.datetime.now() - datetime.timedelta(minutes=increment) expiration_time = datetime.datetime.now() - datetime.timedelta(minutes=increment)
invite = InvitationFactory.create( invite = InvitationFactory.create(
user=user, user=user,
expiration_time=expiration_time, expiration_time=expiration_time,
status=Status.PENDING, status=Status.PENDING,
workspace_role=ws_role, portfolio_role=ws_role,
) )
with pytest.raises(ExpiredError): with pytest.raises(ExpiredError):
Invitations.accept(user, invite.token) Invitations.accept(user, invite.token)
@ -62,10 +62,10 @@ def test_accept_expired_invitation():
def test_accept_rejected_invite(): def test_accept_rejected_invite():
user = UserFactory.create() user = UserFactory.create()
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
invite = InvitationFactory.create( invite = InvitationFactory.create(
user=user, status=Status.REJECTED_EXPIRED, workspace_role=ws_role user=user, status=Status.REJECTED_EXPIRED, portfolio_role=ws_role
) )
with pytest.raises(InvitationError): with pytest.raises(InvitationError):
Invitations.accept(user, invite.token) Invitations.accept(user, invite.token)
@ -73,10 +73,10 @@ def test_accept_rejected_invite():
def test_accept_revoked_invite(): def test_accept_revoked_invite():
user = UserFactory.create() user = UserFactory.create()
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
invite = InvitationFactory.create( invite = InvitationFactory.create(
user=user, status=Status.REVOKED, workspace_role=ws_role user=user, status=Status.REVOKED, portfolio_role=ws_role
) )
with pytest.raises(InvitationError): with pytest.raises(InvitationError):
Invitations.accept(user, invite.token) Invitations.accept(user, invite.token)
@ -84,20 +84,20 @@ def test_accept_revoked_invite():
def test_wrong_user_accepts_invitation(): def test_wrong_user_accepts_invitation():
user = UserFactory.create() user = UserFactory.create()
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
wrong_user = UserFactory.create() wrong_user = UserFactory.create()
invite = InvitationFactory.create(user=user, workspace_role=ws_role) invite = InvitationFactory.create(user=user, portfolio_role=ws_role)
with pytest.raises(WrongUserError): with pytest.raises(WrongUserError):
Invitations.accept(wrong_user, invite.token) Invitations.accept(wrong_user, invite.token)
def test_user_cannot_accept_invitation_accepted_by_wrong_user(): def test_user_cannot_accept_invitation_accepted_by_wrong_user():
user = UserFactory.create() user = UserFactory.create()
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
wrong_user = UserFactory.create() wrong_user = UserFactory.create()
invite = InvitationFactory.create(user=user, workspace_role=ws_role) invite = InvitationFactory.create(user=user, portfolio_role=ws_role)
with pytest.raises(WrongUserError): with pytest.raises(WrongUserError):
Invitations.accept(wrong_user, invite.token) Invitations.accept(wrong_user, invite.token)
with pytest.raises(InvitationError): with pytest.raises(InvitationError):
@ -105,40 +105,40 @@ def test_user_cannot_accept_invitation_accepted_by_wrong_user():
def test_accept_invitation_twice(): def test_accept_invitation_twice():
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
invite = Invitations.create(workspace.owner, ws_role, user.email) invite = Invitations.create(portfolio.owner, ws_role, user.email)
Invitations.accept(user, invite.token) Invitations.accept(user, invite.token)
with pytest.raises(InvitationError): with pytest.raises(InvitationError):
Invitations.accept(user, invite.token) Invitations.accept(user, invite.token)
def test_revoke_invitation(): def test_revoke_invitation():
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
invite = Invitations.create(workspace.owner, ws_role, user.email) invite = Invitations.create(portfolio.owner, ws_role, user.email)
assert invite.is_pending assert invite.is_pending
Invitations.revoke(invite.token) Invitations.revoke(invite.token)
assert invite.is_revoked assert invite.is_revoked
def test_resend_invitation(): def test_resend_invitation():
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
invite = Invitations.create(workspace.owner, ws_role, user.email) invite = Invitations.create(portfolio.owner, ws_role, user.email)
Invitations.resend(workspace.owner, workspace.id, invite.token) Invitations.resend(portfolio.owner, portfolio.id, invite.token)
assert ws_role.invitations[0].is_revoked assert ws_role.invitations[0].is_revoked
assert ws_role.invitations[1].is_pending assert ws_role.invitations[1].is_pending
def test_audit_event_for_accepted_invite(): def test_audit_event_for_accepted_invite():
workspace = WorkspaceFactory.create() portfolio = PortfolioFactory.create()
user = UserFactory.create() user = UserFactory.create()
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace) ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
invite = Invitations.create(workspace.owner, ws_role, user.email) invite = Invitations.create(portfolio.owner, ws_role, user.email)
invite = Invitations.accept(user, invite.token) invite = Invitations.accept(user, invite.token)
accepted_event = AuditLog.get_by_resource(invite.id)[0] accepted_event = AuditLog.get_by_resource(invite.id)[0]

View File

@ -0,0 +1,65 @@
from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.users import Users
from atst.models.portfolio_role import Status as PortfolioRoleStatus
from atst.domain.roles import Roles
from tests.factories import (
PortfolioFactory,
UserFactory,
InvitationFactory,
PortfolioRoleFactory,
)
def test_can_create_new_portfolio_role():
portfolio = PortfolioFactory.create()
new_user = UserFactory.create()
portfolio_role_dicts = [{"id": new_user.id, "portfolio_role": "owner"}]
portfolio_roles = PortfolioRoles.add_many(portfolio.id, portfolio_role_dicts)
assert portfolio_roles[0].user_id == new_user.id
assert portfolio_roles[0].user.atat_role.name == new_user.atat_role.name
assert portfolio_roles[0].role.name == new_user.portfolio_roles[0].role.name
def test_can_update_existing_portfolio_role():
portfolio = PortfolioFactory.create()
new_user = UserFactory.create()
PortfolioRoles.add_many(
portfolio.id, [{"id": new_user.id, "portfolio_role": "owner"}]
)
portfolio_roles = PortfolioRoles.add_many(
portfolio.id, [{"id": new_user.id, "portfolio_role": "developer"}]
)
assert portfolio_roles[0].user.atat_role.name == new_user.atat_role.name
assert portfolio_roles[0].role.name == new_user.portfolio_roles[0].role.name
def test_portfolio_role_permissions():
portfolio_one = PortfolioFactory.create()
portfolio_two = PortfolioFactory.create()
new_user = UserFactory.create()
PortfolioRoleFactory.create(
portfolio=portfolio_one,
user=new_user,
role=Roles.get("developer"),
status=PortfolioRoleStatus.ACTIVE,
)
PortfolioRoleFactory.create(
portfolio=portfolio_two,
user=new_user,
role=Roles.get("developer"),
status=PortfolioRoleStatus.PENDING,
)
default_perms = set(new_user.atat_role.permissions)
assert len(
PortfolioRoles.portfolio_role_permissions(portfolio_one, new_user)
) > len(default_perms)
assert (
PortfolioRoles.portfolio_role_permissions(portfolio_two, new_user)
== default_perms
)

View File

@ -0,0 +1,358 @@
import pytest
from uuid import uuid4
from atst.domain.exceptions import NotFoundError, UnauthorizedError
from atst.domain.portfolios import Portfolios, PortfolioError
from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.applications import Applications
from atst.domain.environments import Environments
from atst.models.portfolio_role import Status as PortfolioRoleStatus
from tests.factories import (
RequestFactory,
UserFactory,
PortfolioRoleFactory,
PortfolioFactory,
)
@pytest.fixture(scope="function")
def portfolio_owner():
return UserFactory.create()
@pytest.fixture(scope="function")
def request_(portfolio_owner):
return RequestFactory.create(creator=portfolio_owner)
@pytest.fixture(scope="function")
def portfolio(request_):
portfolio = Portfolios.create_from_request(request_)
return portfolio
def test_can_create_portfolio(request_):
portfolio = Portfolios.create_from_request(request_, name="frugal-whale")
assert portfolio.name == "frugal-whale"
def test_request_is_associated_with_portfolio(portfolio, request_):
assert portfolio.request == request_
def test_default_portfolio_name_is_request_name(portfolio, request_):
assert portfolio.name == str(request_.displayname)
def test_get_nonexistent_portfolio_raises():
with pytest.raises(NotFoundError):
Portfolios.get(UserFactory.build(), uuid4())
def test_can_get_portfolio_by_request(portfolio):
found = Portfolios.get_by_request(portfolio.request)
assert portfolio == found
def test_creating_portfolio_adds_owner(portfolio, portfolio_owner):
assert portfolio.roles[0].user == portfolio_owner
def test_portfolio_has_timestamps(portfolio):
assert portfolio.time_created == portfolio.time_updated
def test_portfolios_get_ensures_user_is_in_portfolio(portfolio, portfolio_owner):
outside_user = UserFactory.create()
with pytest.raises(UnauthorizedError):
Portfolios.get(outside_user, portfolio.id)
def test_get_for_update_applications_allows_owner(portfolio, portfolio_owner):
Portfolios.get_for_update_applications(portfolio_owner, portfolio.id)
def test_get_for_update_applications_blocks_developer(portfolio):
developer = UserFactory.create()
PortfolioRoles.add(developer, portfolio.id, "developer")
with pytest.raises(UnauthorizedError):
Portfolios.get_for_update_applications(developer, portfolio.id)
def test_can_create_portfolio_role(portfolio, portfolio_owner):
user_data = {
"first_name": "New",
"last_name": "User",
"email": "new.user@mail.com",
"portfolio_role": "developer",
"dod_id": "1234567890",
}
new_member = Portfolios.create_member(portfolio_owner, portfolio, user_data)
assert new_member.portfolio == portfolio
assert new_member.user.provisional
def test_can_add_existing_user_to_portfolio(portfolio, portfolio_owner):
user = UserFactory.create()
user_data = {
"first_name": "New",
"last_name": "User",
"email": "new.user@mail.com",
"portfolio_role": "developer",
"dod_id": user.dod_id,
}
new_member = Portfolios.create_member(portfolio_owner, portfolio, user_data)
assert new_member.portfolio == portfolio
assert new_member.user.email == user.email
assert not new_member.user.provisional
def test_need_permission_to_create_portfolio_role(portfolio, portfolio_owner):
random_user = UserFactory.create()
user_data = {
"first_name": "New",
"last_name": "User",
"email": "new.user@mail.com",
"portfolio_role": "developer",
"dod_id": "1234567890",
}
with pytest.raises(UnauthorizedError):
Portfolios.create_member(random_user, portfolio, user_data)
def test_update_portfolio_role_role(portfolio, portfolio_owner):
user_data = {
"first_name": "New",
"last_name": "User",
"email": "new.user@mail.com",
"portfolio_role": "developer",
"dod_id": "1234567890",
}
PortfolioRoleFactory._meta.sqlalchemy_session_persistence = "flush"
member = PortfolioRoleFactory.create(portfolio=portfolio)
role_name = "admin"
updated_member = Portfolios.update_member(
portfolio_owner, portfolio, member, role_name
)
assert updated_member.portfolio == portfolio
assert updated_member.role_name == role_name
def test_need_permission_to_update_portfolio_role_role(portfolio, portfolio_owner):
random_user = UserFactory.create()
user_data = {
"first_name": "New",
"last_name": "User",
"email": "new.user@mail.com",
"portfolio_role": "developer",
"dod_id": "1234567890",
}
member = Portfolios.create_member(portfolio_owner, portfolio, user_data)
role_name = "developer"
with pytest.raises(UnauthorizedError):
Portfolios.update_member(random_user, portfolio, member, role_name)
def test_owner_can_view_portfolio_members(portfolio, portfolio_owner):
portfolio_owner = UserFactory.create()
portfolio = Portfolios.create_from_request(
RequestFactory.create(creator=portfolio_owner)
)
portfolio = Portfolios.get_with_members(portfolio_owner, portfolio.id)
assert portfolio
def test_ccpo_can_view_portfolio_members(portfolio, portfolio_owner):
ccpo = UserFactory.from_atat_role("ccpo")
assert Portfolios.get_with_members(ccpo, portfolio.id)
def test_random_user_cannot_view_portfolio_members(portfolio):
developer = UserFactory.from_atat_role("developer")
with pytest.raises(UnauthorizedError):
portfolio = Portfolios.get_with_members(developer, portfolio.id)
def test_scoped_portfolio_only_returns_a_users_applications_and_environments(
portfolio, portfolio_owner
):
new_application = Applications.create(
portfolio_owner,
portfolio,
"My Application",
"My application",
["dev", "staging", "prod"],
)
Applications.create(
portfolio_owner,
portfolio,
"My Application 2",
"My application 2",
["dev", "staging", "prod"],
)
developer = UserFactory.from_atat_role("developer")
dev_environment = Environments.add_member(
new_application.environments[0], developer, "developer"
)
scoped_portfolio = Portfolios.get(developer, portfolio.id)
# Should only return the application and environment in which the user has an
# environment role.
assert scoped_portfolio.applications == [new_application]
assert scoped_portfolio.applications[0].environments == [dev_environment]
def test_scoped_portfolio_returns_all_applications_for_portfolio_admin(
portfolio, portfolio_owner
):
for _ in range(5):
Applications.create(
portfolio_owner,
portfolio,
"My Application",
"My application",
["dev", "staging", "prod"],
)
admin = UserFactory.from_atat_role("default")
Portfolios._create_portfolio_role(
admin, portfolio, "admin", status=PortfolioRoleStatus.ACTIVE
)
scoped_portfolio = Portfolios.get(admin, portfolio.id)
assert len(scoped_portfolio.applications) == 5
assert len(scoped_portfolio.applications[0].environments) == 3
def test_scoped_portfolio_returns_all_applications_for_portfolio_owner(
portfolio, portfolio_owner
):
for _ in range(5):
Applications.create(
portfolio_owner,
portfolio,
"My Application",
"My application",
["dev", "staging", "prod"],
)
scoped_portfolio = Portfolios.get(portfolio_owner, portfolio.id)
assert len(scoped_portfolio.applications) == 5
assert len(scoped_portfolio.applications[0].environments) == 3
def test_for_user_returns_active_portfolios_for_user(portfolio, portfolio_owner):
bob = UserFactory.from_atat_role("default")
PortfolioRoleFactory.create(
user=bob, portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE
)
Portfolios.create_from_request(RequestFactory.create())
bobs_portfolios = Portfolios.for_user(bob)
assert len(bobs_portfolios) == 1
def test_for_user_does_not_return_inactive_portfolios(portfolio, portfolio_owner):
bob = UserFactory.from_atat_role("default")
Portfolios.add_member(portfolio, bob, "developer")
Portfolios.create_from_request(RequestFactory.create())
bobs_portfolios = Portfolios.for_user(bob)
assert len(bobs_portfolios) == 0
def test_for_user_returns_all_portfolios_for_ccpo(portfolio, portfolio_owner):
sam = UserFactory.from_atat_role("ccpo")
Portfolios.create_from_request(RequestFactory.create())
sams_portfolios = Portfolios.for_user(sam)
assert len(sams_portfolios) == 2
def test_get_for_update_information():
portfolio_owner = UserFactory.create()
portfolio = Portfolios.create_from_request(
RequestFactory.create(creator=portfolio_owner)
)
owner_ws = Portfolios.get_for_update_information(portfolio_owner, portfolio.id)
assert portfolio == owner_ws
admin = UserFactory.create()
Portfolios._create_portfolio_role(
admin, portfolio, "admin", status=PortfolioRoleStatus.ACTIVE
)
admin_ws = Portfolios.get_for_update_information(admin, portfolio.id)
assert portfolio == admin_ws
ccpo = UserFactory.from_atat_role("ccpo")
with pytest.raises(UnauthorizedError):
Portfolios.get_for_update_information(ccpo, portfolio.id)
def test_can_create_portfolios_with_matching_names():
portfolio_name = "Great Portfolio"
Portfolios.create_from_request(RequestFactory.create(), name=portfolio_name)
Portfolios.create_from_request(RequestFactory.create(), name=portfolio_name)
def test_able_to_revoke_portfolio_access_for_active_member():
portfolio = PortfolioFactory.create()
portfolio_role = PortfolioRoleFactory.create(
portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE
)
Portfolios.revoke_access(portfolio.owner, portfolio.id, portfolio_role.id)
assert Portfolios.for_user(portfolio_role.user) == []
def test_can_revoke_access():
portfolio = PortfolioFactory.create()
owner_role = portfolio.roles[0]
portfolio_role = PortfolioRoleFactory.create(
portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE
)
assert Portfolios.can_revoke_access_for(portfolio, portfolio_role)
assert not Portfolios.can_revoke_access_for(portfolio, owner_role)
def test_unable_to_revoke_owner_portfolio_access():
portfolio = PortfolioFactory.create()
owner_portfolio_role = portfolio.roles[0]
with pytest.raises(PortfolioError):
Portfolios.revoke_access(portfolio.owner, portfolio.id, owner_portfolio_role.id)
def test_disabled_members_dont_show_up(session):
portfolio = PortfolioFactory.create()
PortfolioRoleFactory.create(portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE)
PortfolioRoleFactory.create(
portfolio=portfolio, status=PortfolioRoleStatus.DISABLED
)
# should only return portfolio owner and ACTIVE member
assert len(portfolio.members) == 2
def test_does_not_count_disabled_members(session):
portfolio = PortfolioFactory.create()
PortfolioRoleFactory.create(portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE)
PortfolioRoleFactory.create(portfolio=portfolio)
PortfolioRoleFactory.create(
portfolio=portfolio, status=PortfolioRoleStatus.DISABLED
)
assert portfolio.user_count == 3

View File

@ -1,19 +1,19 @@
from atst.domain.reports import Reports from atst.domain.reports import Reports
from tests.factories import RequestFactory, LegacyTaskOrderFactory, WorkspaceFactory from tests.factories import RequestFactory, LegacyTaskOrderFactory, PortfolioFactory
CLIN_NUMS = ["0001", "0003", "1001", "1003", "2001", "2003"] CLIN_NUMS = ["0001", "0003", "1001", "1003", "2001", "2003"]
def test_workspace_totals(): def test_portfolio_totals():
legacy_task_order = LegacyTaskOrderFactory.create() legacy_task_order = LegacyTaskOrderFactory.create()
for num in CLIN_NUMS: for num in CLIN_NUMS:
setattr(legacy_task_order, "clin_{}".format(num), 200) setattr(legacy_task_order, "clin_{}".format(num), 200)
request = RequestFactory.create(legacy_task_order=legacy_task_order) request = RequestFactory.create(legacy_task_order=legacy_task_order)
workspace = WorkspaceFactory.create(request=request) portfolio = PortfolioFactory.create(request=request)
report = Reports.workspace_totals(workspace) report = Reports.portfolio_totals(portfolio)
total = 200 * len(CLIN_NUMS) total = 200 * len(CLIN_NUMS)
assert report == {"budget": total, "spent": 0} assert report == {"budget": total, "spent": 0}
@ -21,18 +21,18 @@ def test_workspace_totals():
# this is sketched in until we do real reporting # this is sketched in until we do real reporting
def test_monthly_totals(): def test_monthly_totals():
request = RequestFactory.create() request = RequestFactory.create()
workspace = WorkspaceFactory.create(request=request) portfolio = PortfolioFactory.create(request=request)
monthly = Reports.monthly_totals(workspace) monthly = Reports.monthly_totals(portfolio)
assert not monthly["environments"] assert not monthly["environments"]
assert not monthly["applications"] assert not monthly["applications"]
assert not monthly["workspace"] assert not monthly["portfolio"]
# this is sketched in until we do real reporting # this is sketched in until we do real reporting
def test_cumulative_budget(): def test_cumulative_budget():
request = RequestFactory.create() request = RequestFactory.create()
workspace = WorkspaceFactory.create(request=request) portfolio = PortfolioFactory.create(request=request)
months = Reports.cumulative_budget(workspace) months = Reports.cumulative_budget(portfolio)
assert len(months["months"]) == 12 assert len(months["months"]) == 12

Some files were not shown because too many files have changed in this diff Show More