workspace -> portfolio everywhere
This commit is contained in:
parent
3fc323d785
commit
d3d36822df
@ -12,7 +12,7 @@ from atst.database import db
|
||||
from atst.assets import environment as assets_environment
|
||||
from atst.filters import register_filters
|
||||
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.task_orders import task_orders_bp
|
||||
from atst.routes.dev import bp as dev_routes
|
||||
@ -63,7 +63,7 @@ def make_app(config):
|
||||
|
||||
make_error_pages(app)
|
||||
app.register_blueprint(bp)
|
||||
app.register_blueprint(workspace_routes)
|
||||
app.register_blueprint(portfolio_routes)
|
||||
app.register_blueprint(task_orders_bp)
|
||||
app.register_blueprint(user_routes)
|
||||
app.register_blueprint(requests_bp)
|
||||
|
@ -10,9 +10,9 @@ from atst.models.environment_role import EnvironmentRole
|
||||
|
||||
class Applications(object):
|
||||
@classmethod
|
||||
def create(cls, user, workspace, name, description, environment_names):
|
||||
def create(cls, user, portfolio, name, description, environment_names):
|
||||
application = Application(
|
||||
workspace=workspace, name=name, description=description
|
||||
portfolio=portfolio, name=name, description=description
|
||||
)
|
||||
db.session.add(application)
|
||||
|
||||
@ -22,13 +22,13 @@ class Applications(object):
|
||||
return application
|
||||
|
||||
@classmethod
|
||||
def get(cls, user, workspace, application_id):
|
||||
def get(cls, user, portfolio, application_id):
|
||||
# TODO: this should check permission for this particular application
|
||||
Authorization.check_workspace_permission(
|
||||
Authorization.check_portfolio_permission(
|
||||
user,
|
||||
workspace,
|
||||
portfolio,
|
||||
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
|
||||
"view application in workspace",
|
||||
"view application in portfolio",
|
||||
)
|
||||
|
||||
try:
|
||||
@ -41,28 +41,28 @@ class Applications(object):
|
||||
return application
|
||||
|
||||
@classmethod
|
||||
def for_user(self, user, workspace):
|
||||
def for_user(self, user, portfolio):
|
||||
return (
|
||||
db.session.query(Application)
|
||||
.join(Environment)
|
||||
.join(EnvironmentRole)
|
||||
.filter(Application.workspace_id == workspace.id)
|
||||
.filter(Application.workspace_id == portfolio.id)
|
||||
.filter(EnvironmentRole.user_id == user.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, user, workspace_role, workspace):
|
||||
Authorization.check_workspace_permission(
|
||||
def get_all(cls, user, portfolio_role, portfolio):
|
||||
Authorization.check_portfolio_permission(
|
||||
user,
|
||||
workspace,
|
||||
portfolio,
|
||||
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
|
||||
"view application in workspace",
|
||||
"view application in portfolio",
|
||||
)
|
||||
|
||||
try:
|
||||
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:
|
||||
raise NotFoundError("applications")
|
||||
@ -70,7 +70,7 @@ class Applications(object):
|
||||
return applications
|
||||
|
||||
@classmethod
|
||||
def update(cls, user, workspace, application, new_data):
|
||||
def update(cls, user, portfolio, application, new_data):
|
||||
if "name" in new_data:
|
||||
application.name = new_data["name"]
|
||||
if "description" in new_data:
|
||||
|
@ -15,13 +15,13 @@ class AuditEventQuery(Query):
|
||||
return cls.paginate(query, pagination_opts)
|
||||
|
||||
@classmethod
|
||||
def get_ws_events(cls, workspace_id, pagination_opts):
|
||||
def get_ws_events(cls, portfolio_id, pagination_opts):
|
||||
query = (
|
||||
db.session.query(cls.model)
|
||||
.filter(
|
||||
or_(
|
||||
cls.model.workspace_id == workspace_id,
|
||||
cls.model.resource_id == workspace_id,
|
||||
cls.model.portfolio_id == portfolio_id,
|
||||
cls.model.resource_id == portfolio_id,
|
||||
)
|
||||
)
|
||||
.order_by(cls.model.time_created.desc())
|
||||
@ -31,8 +31,8 @@ class AuditEventQuery(Query):
|
||||
|
||||
class AuditLog(object):
|
||||
@classmethod
|
||||
def log_system_event(cls, resource, action, workspace=None):
|
||||
return cls._log(resource=resource, action=action, workspace=workspace)
|
||||
def log_system_event(cls, resource, action, portfolio=None):
|
||||
return cls._log(resource=resource, action=action, portfolio=portfolio)
|
||||
|
||||
@classmethod
|
||||
def get_all_events(cls, user, pagination_opts=None):
|
||||
@ -42,14 +42,14 @@ class AuditLog(object):
|
||||
return AuditEventQuery.get_all(pagination_opts)
|
||||
|
||||
@classmethod
|
||||
def get_workspace_events(cls, user, workspace, pagination_opts=None):
|
||||
Authorization.check_workspace_permission(
|
||||
def get_portfolio_events(cls, user, portfolio, pagination_opts=None):
|
||||
Authorization.check_portfolio_permission(
|
||||
user,
|
||||
workspace,
|
||||
portfolio,
|
||||
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
|
||||
def get_by_resource(cls, resource_id):
|
||||
@ -65,14 +65,14 @@ class AuditLog(object):
|
||||
return type(resource).__name__.lower()
|
||||
|
||||
@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_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(
|
||||
user=user,
|
||||
workspace_id=workspace_id,
|
||||
portfolio_id=portfolio_id,
|
||||
resource_id=resource_id,
|
||||
resource_type=resource_type,
|
||||
action=action,
|
||||
|
@ -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.domain.exceptions import UnauthorizedError
|
||||
|
||||
|
||||
class Authorization(object):
|
||||
@classmethod
|
||||
def has_workspace_permission(cls, user, workspace, permission):
|
||||
return permission in WorkspaceRoles.workspace_role_permissions(workspace, user)
|
||||
def has_portfolio_permission(cls, user, portfolio, permission):
|
||||
return permission in PortfolioRoles.portfolio_role_permissions(portfolio, user)
|
||||
|
||||
@classmethod
|
||||
def has_atat_permission(cls, user, permission):
|
||||
return permission in user.atat_role.permissions
|
||||
|
||||
@classmethod
|
||||
def is_in_workspace(cls, user, workspace):
|
||||
return user in workspace.users
|
||||
def is_in_portfolio(cls, user, portfolio):
|
||||
return user in portfolio.users
|
||||
|
||||
@classmethod
|
||||
def check_workspace_permission(cls, user, workspace, permission, message):
|
||||
if not Authorization.has_workspace_permission(user, workspace, permission):
|
||||
def check_portfolio_permission(cls, user, portfolio, permission, message):
|
||||
if not Authorization.has_portfolio_permission(user, portfolio, permission):
|
||||
raise UnauthorizedError(user, message)
|
||||
|
||||
@classmethod
|
||||
@ -39,8 +39,8 @@ class Authorization(object):
|
||||
if Authorization._check_is_task_order_officer(task_order, user):
|
||||
return True
|
||||
|
||||
Authorization.check_workspace_permission(
|
||||
user, task_order.workspace, permission, message
|
||||
Authorization.check_portfolio_permission(
|
||||
user, task_order.portfolio, permission, message
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -200,17 +200,17 @@ class MockReportingProvider(ReportingInterface):
|
||||
]
|
||||
)
|
||||
|
||||
def get_budget(self, workspace):
|
||||
if workspace.name in self.REPORT_FIXTURE_MAP:
|
||||
return self.REPORT_FIXTURE_MAP[workspace.name]["budget"]
|
||||
elif workspace.request and workspace.legacy_task_order:
|
||||
return workspace.legacy_task_order.budget
|
||||
def get_budget(self, portfolio):
|
||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
||||
return self.REPORT_FIXTURE_MAP[portfolio.name]["budget"]
|
||||
elif portfolio.request and portfolio.legacy_task_order:
|
||||
return portfolio.legacy_task_order.budget
|
||||
return 0
|
||||
|
||||
def get_total_spending(self, workspace):
|
||||
if workspace.name in self.REPORT_FIXTURE_MAP:
|
||||
def get_total_spending(self, portfolio):
|
||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
||||
return self._sum_monthly_spend(
|
||||
self.REPORT_FIXTURE_MAP[workspace.name]["applications"]
|
||||
self.REPORT_FIXTURE_MAP[portfolio.name]["applications"]
|
||||
)
|
||||
return 0
|
||||
|
||||
@ -230,17 +230,17 @@ class MockReportingProvider(ReportingInterface):
|
||||
|
||||
return application_totals
|
||||
|
||||
def _rollup_workspace_totals(self, application_totals):
|
||||
def _rollup_portfolio_totals(self, application_totals):
|
||||
monthly_spend = [
|
||||
(month, spend)
|
||||
for application in application_totals.values()
|
||||
for month, spend in application.items()
|
||||
]
|
||||
workspace_totals = {}
|
||||
portfolio_totals = {}
|
||||
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):
|
||||
"""Return the monthly totals for the specified environment.
|
||||
@ -253,26 +253,26 @@ class MockReportingProvider(ReportingInterface):
|
||||
"""
|
||||
return self.MONTHLY_SPEND_BY_ENVIRONMENT.get(environment_id, {})
|
||||
|
||||
def monthly_totals(self, workspace):
|
||||
"""Return month totals rolled up by environment, application, and workspace.
|
||||
def monthly_totals(self, portfolio):
|
||||
"""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".
|
||||
The "applications" key will have budget data per month for each application,
|
||||
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:
|
||||
|
||||
{
|
||||
"environments": { "X-Wing": { "Prod": { "01/2018": 75.42 } } },
|
||||
"applications": { "X-Wing": { "01/2018": 75.42 } },
|
||||
"workspace": { "01/2018": 75.42 },
|
||||
"portfolio": { "01/2018": 75.42 },
|
||||
}
|
||||
|
||||
"""
|
||||
applications = workspace.applications
|
||||
if workspace.name in self.REPORT_FIXTURE_MAP:
|
||||
applications = self.REPORT_FIXTURE_MAP[workspace.name]["applications"]
|
||||
applications = portfolio.applications
|
||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
||||
applications = self.REPORT_FIXTURE_MAP[portfolio.name]["applications"]
|
||||
environments = {
|
||||
application.name: {
|
||||
env.name: self.monthly_totals_for_environment(env.id)
|
||||
@ -282,17 +282,17 @@ class MockReportingProvider(ReportingInterface):
|
||||
}
|
||||
|
||||
application_totals = self._rollup_application_totals(environments)
|
||||
workspace_totals = self._rollup_workspace_totals(application_totals)
|
||||
portfolio_totals = self._rollup_portfolio_totals(application_totals)
|
||||
|
||||
return {
|
||||
"environments": environments,
|
||||
"applications": application_totals,
|
||||
"workspace": workspace_totals,
|
||||
"portfolio": portfolio_totals,
|
||||
}
|
||||
|
||||
def cumulative_budget(self, workspace):
|
||||
if workspace.name in self.REPORT_FIXTURE_MAP:
|
||||
budget_months = self.REPORT_FIXTURE_MAP[workspace.name]["cumulative"]
|
||||
def cumulative_budget(self, portfolio):
|
||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
||||
budget_months = self.REPORT_FIXTURE_MAP[portfolio.name]["cumulative"]
|
||||
else:
|
||||
budget_months = {}
|
||||
|
||||
|
@ -60,10 +60,10 @@ class Environments(object):
|
||||
return env
|
||||
|
||||
@classmethod
|
||||
def update_environment_roles(cls, user, workspace, workspace_role, ids_and_roles):
|
||||
Authorization.check_workspace_permission(
|
||||
def update_environment_roles(cls, user, portfolio, portfolio_role, ids_and_roles):
|
||||
Authorization.check_portfolio_permission(
|
||||
user,
|
||||
workspace,
|
||||
portfolio,
|
||||
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
|
||||
"assign environment roles",
|
||||
)
|
||||
@ -75,13 +75,13 @@ class Environments(object):
|
||||
|
||||
if new_role is None:
|
||||
role_deleted = EnvironmentRoles.delete(
|
||||
workspace_role.user.id, environment.id
|
||||
portfolio_role.user.id, environment.id
|
||||
)
|
||||
if role_deleted:
|
||||
updated = True
|
||||
else:
|
||||
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:
|
||||
env_role.role = new_role
|
||||
@ -89,7 +89,7 @@ class Environments(object):
|
||||
db.session.add(env_role)
|
||||
elif not env_role:
|
||||
env_role = EnvironmentRoles.create(
|
||||
user=workspace_role.user, environment=environment, role=new_role
|
||||
user=portfolio_role.user, environment=environment, role=new_role
|
||||
)
|
||||
updated = True
|
||||
db.session.add(env_role)
|
||||
@ -101,9 +101,9 @@ class Environments(object):
|
||||
|
||||
@classmethod
|
||||
def revoke_access(cls, user, environment, target_user):
|
||||
Authorization.check_workspace_permission(
|
||||
Authorization.check_portfolio_permission(
|
||||
user,
|
||||
environment.workspace,
|
||||
environment.portfolio,
|
||||
Permissions.REMOVE_CSP_ROLES,
|
||||
"revoke environment access",
|
||||
)
|
||||
|
@ -3,9 +3,9 @@ from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from atst.database import db
|
||||
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.workspaces import Workspaces
|
||||
from atst.domain.portfolios import Portfolios
|
||||
|
||||
from .exceptions import NotFoundError
|
||||
|
||||
@ -54,11 +54,11 @@ class Invitations(object):
|
||||
return invite
|
||||
|
||||
@classmethod
|
||||
def create(cls, inviter, workspace_role, email):
|
||||
def create(cls, inviter, portfolio_role, email):
|
||||
invite = Invitation(
|
||||
workspace_role=workspace_role,
|
||||
portfolio_role=portfolio_role,
|
||||
inviter=inviter,
|
||||
user=workspace_role.user,
|
||||
user=portfolio_role.user,
|
||||
status=InvitationStatus.PENDING,
|
||||
expiration_time=Invitations.current_expiration_time(),
|
||||
email=email,
|
||||
@ -86,7 +86,7 @@ class Invitations(object):
|
||||
|
||||
elif invite.is_pending: # pragma: no branch
|
||||
Invitations._update_status(invite, InvitationStatus.ACCEPTED)
|
||||
WorkspaceRoles.enable(invite.workspace_role)
|
||||
PortfolioRoles.enable(invite.portfolio_role)
|
||||
return invite
|
||||
|
||||
@classmethod
|
||||
@ -109,18 +109,18 @@ class Invitations(object):
|
||||
return Invitations._update_status(invite, InvitationStatus.REVOKED)
|
||||
|
||||
@classmethod
|
||||
def resend(cls, user, workspace_id, token):
|
||||
workspace = Workspaces.get(user, workspace_id)
|
||||
Authorization.check_workspace_permission(
|
||||
def resend(cls, user, portfolio_id, token):
|
||||
portfolio = Portfolios.get(user, portfolio_id)
|
||||
Authorization.check_portfolio_permission(
|
||||
user,
|
||||
workspace,
|
||||
portfolio,
|
||||
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
|
||||
"resend a workspace invitation",
|
||||
"resend a portfolio invitation",
|
||||
)
|
||||
|
||||
previous_invitation = Invitations._get(token)
|
||||
Invitations._update_status(previous_invitation, InvitationStatus.REVOKED)
|
||||
|
||||
return Invitations.create(
|
||||
user, previous_invitation.workspace_role, previous_invitation.email
|
||||
user, previous_invitation.portfolio_role, previous_invitation.email
|
||||
)
|
||||
|
167
atst/domain/portfolio_roles.py
Normal file
167
atst/domain/portfolio_roles.py
Normal 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()
|
1
atst/domain/portfolios/__init__.py
Normal file
1
atst/domain/portfolios/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .portfolios import Portfolios, PortfolioError
|
182
atst/domain/portfolios/portfolios.py
Normal file
182
atst/domain/portfolios/portfolios.py
Normal 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
|
34
atst/domain/portfolios/query.py
Normal file
34
atst/domain/portfolios/query.py
Normal 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)
|
@ -21,16 +21,16 @@ class ScopedResource(object):
|
||||
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)
|
||||
that the given user is allowed to see.
|
||||
"""
|
||||
|
||||
@property
|
||||
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
|
||||
)
|
||||
|
||||
@ -46,16 +46,16 @@ class ScopedWorkspace(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)
|
||||
that the given user is allowed to see.
|
||||
"""
|
||||
|
||||
@property
|
||||
def environments(self):
|
||||
can_view_all_environments = Authorization.has_workspace_permission(
|
||||
can_view_all_environments = Authorization.has_portfolio_permission(
|
||||
self.user,
|
||||
self.resource.workspace,
|
||||
self.resource.portfolio,
|
||||
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
|
||||
)
|
||||
|
@ -3,15 +3,15 @@ from flask import current_app
|
||||
|
||||
class Reports:
|
||||
@classmethod
|
||||
def workspace_totals(cls, workspace):
|
||||
budget = current_app.csp.reports.get_budget(workspace)
|
||||
spent = current_app.csp.reports.get_total_spending(workspace)
|
||||
def portfolio_totals(cls, portfolio):
|
||||
budget = current_app.csp.reports.get_budget(portfolio)
|
||||
spent = current_app.csp.reports.get_total_spending(portfolio)
|
||||
return {"budget": budget, "spent": spent}
|
||||
|
||||
@classmethod
|
||||
def monthly_totals(cls, workspace):
|
||||
return current_app.csp.reports.monthly_totals(workspace)
|
||||
def monthly_totals(cls, portfolio):
|
||||
return current_app.csp.reports.monthly_totals(portfolio)
|
||||
|
||||
@classmethod
|
||||
def cumulative_budget(cls, workspace):
|
||||
return current_app.csp.reports.cumulative_budget(workspace)
|
||||
def cumulative_budget(cls, portfolio):
|
||||
return current_app.csp.reports.cumulative_budget(portfolio)
|
||||
|
@ -1,6 +1,6 @@
|
||||
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_status_event import RequestStatusEvent, RequestStatus
|
||||
from atst.models.request_review import RequestReview
|
||||
@ -99,25 +99,25 @@ class Requests(object):
|
||||
return RequestsQuery.add_and_commit(request)
|
||||
|
||||
@classmethod
|
||||
def approve_and_create_workspace(cls, request):
|
||||
def approve_and_create_portfolio(cls, request):
|
||||
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)
|
||||
|
||||
return workspace
|
||||
return portfolio
|
||||
|
||||
@classmethod
|
||||
def auto_approve_and_create_workspace(
|
||||
def auto_approve_and_create_portfolio(
|
||||
cls,
|
||||
request,
|
||||
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(
|
||||
user=None, request=request, review_data={"comment": reason}
|
||||
)
|
||||
return workspace
|
||||
return portfolio
|
||||
|
||||
@classmethod
|
||||
def set_status(cls, request, status: RequestStatus):
|
||||
@ -214,7 +214,7 @@ class Requests(object):
|
||||
if request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE:
|
||||
Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION)
|
||||
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)
|
||||
|
||||
|
@ -57,7 +57,7 @@ ATAT_ROLES = [
|
||||
WORKSPACE_ROLES = [
|
||||
{
|
||||
"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.",
|
||||
"permissions": [
|
||||
Permissions.REQUEST_JEDI_WORKSPACE,
|
||||
@ -131,7 +131,7 @@ WORKSPACE_ROLES = [
|
||||
{
|
||||
"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.VIEW_USAGE_REPORT,
|
||||
Permissions.VIEW_USAGE_DOLLARS,
|
||||
|
@ -3,7 +3,7 @@ from sqlalchemy.orm.exc import NoResultFound
|
||||
from atst.database import db
|
||||
from atst.models.task_order import TaskOrder
|
||||
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 .exceptions import NotFoundError
|
||||
|
||||
@ -63,11 +63,11 @@ class TaskOrders(object):
|
||||
raise NotFoundError("task_order")
|
||||
|
||||
@classmethod
|
||||
def create(cls, creator, workspace):
|
||||
Authorization.check_workspace_permission(
|
||||
creator, workspace, Permissions.UPDATE_TASK_ORDER, "add task order"
|
||||
def create(cls, creator, portfolio):
|
||||
Authorization.check_portfolio_permission(
|
||||
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.commit()
|
||||
@ -116,39 +116,39 @@ class TaskOrders(object):
|
||||
|
||||
@classmethod
|
||||
def add_officer(cls, user, task_order, officer_type, officer_data):
|
||||
Authorization.check_workspace_permission(
|
||||
Authorization.check_portfolio_permission(
|
||||
user,
|
||||
task_order.workspace,
|
||||
task_order.portfolio,
|
||||
Permissions.ADD_TASK_ORDER_OFFICER,
|
||||
"add task order officer",
|
||||
)
|
||||
|
||||
if officer_type in TaskOrders.OFFICERS:
|
||||
workspace = task_order.workspace
|
||||
portfolio = task_order.portfolio
|
||||
|
||||
existing_member = next(
|
||||
(
|
||||
member
|
||||
for member in workspace.members
|
||||
for member in portfolio.members
|
||||
if member.user.dod_id == officer_data["dod_id"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if existing_member:
|
||||
workspace_user = existing_member.user
|
||||
portfolio_user = existing_member.user
|
||||
else:
|
||||
member = Workspaces.create_member(
|
||||
user, workspace, {**officer_data, "workspace_role": "officer"}
|
||||
member = Portfolios.create_member(
|
||||
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.commit()
|
||||
|
||||
return workspace_user
|
||||
return portfolio_user
|
||||
else:
|
||||
raise TaskOrderError(
|
||||
"{} is not an officer role on task orders".format(officer_type)
|
||||
|
@ -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()
|
@ -1 +0,0 @@
|
||||
from .workspaces import Workspaces, WorkspaceError
|
@ -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)
|
@ -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
|
@ -162,7 +162,7 @@ ENVIRONMENT_ROLES = [
|
||||
|
||||
ENV_ROLE_MODAL_DESCRIPTION = {
|
||||
"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 = [
|
||||
|
@ -11,8 +11,8 @@ class EditMemberForm(FlaskForm):
|
||||
# This form also accepts a field for each environment in each application
|
||||
# that the user is a member of
|
||||
|
||||
workspace_role = SelectField(
|
||||
translate("forms.edit_member.workspace_role_label"),
|
||||
portfolio_role = SelectField(
|
||||
translate("forms.edit_member.portfolio_role_label"),
|
||||
choices=WORKSPACE_ROLES,
|
||||
validators=[Required()],
|
||||
)
|
||||
|
@ -25,10 +25,10 @@ class NewMemberForm(FlaskForm):
|
||||
translate("forms.new_member.dod_id_label"),
|
||||
validators=[Required(), Length(min=10), IsNumber()],
|
||||
)
|
||||
workspace_role = SelectField(
|
||||
translate("forms.new_member.workspace_role_label"),
|
||||
portfolio_role = SelectField(
|
||||
translate("forms.new_member.portfolio_role_label"),
|
||||
choices=WORKSPACE_ROLES,
|
||||
validators=[Required()],
|
||||
default="",
|
||||
description=translate("forms.new_member.workspace_role_description"),
|
||||
description=translate("forms.new_member.portfolio_role_description"),
|
||||
)
|
||||
|
@ -181,7 +181,7 @@ class InformationAboutYouForm(CacheableForm):
|
||||
date_latest_training = inherit_field(USER_FIELDS["date_latest_training"])
|
||||
|
||||
|
||||
class WorkspaceOwnerForm(CacheableForm):
|
||||
class PortfolioOwnerForm(CacheableForm):
|
||||
def validate(self, *args, **kwargs):
|
||||
if self.am_poc.data:
|
||||
# Prepend Optional validators so that the validation chain
|
||||
|
@ -5,14 +5,14 @@ from .forms import CacheableForm
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
class WorkspaceForm(CacheableForm):
|
||||
class PortfolioForm(CacheableForm):
|
||||
name = StringField(
|
||||
translate("forms.workspace.name_label"),
|
||||
translate("forms.portfolio.name_label"),
|
||||
validators=[
|
||||
Length(
|
||||
min=4,
|
||||
max=100,
|
||||
message=translate("forms.workspace.name_length_validation_message"),
|
||||
message=translate("forms.portfolio.name_length_validation_message"),
|
||||
)
|
||||
],
|
||||
)
|
@ -7,10 +7,10 @@ from .request_status_event import RequestStatusEvent
|
||||
from .permissions import Permissions
|
||||
from .role import Role
|
||||
from .user import User
|
||||
from .workspace_role import WorkspaceRole
|
||||
from .portfolio_role import PortfolioRole
|
||||
from .pe_number import PENumber
|
||||
from .legacy_task_order import LegacyTaskOrder
|
||||
from .workspace import Workspace
|
||||
from .portfolio import Portfolio
|
||||
from .application import Application
|
||||
from .environment import Environment
|
||||
from .attachment import Attachment
|
||||
|
@ -14,7 +14,7 @@ class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
description = Column(String, nullable=False)
|
||||
|
||||
workspace_id = Column(ForeignKey("workspaces.id"), nullable=False)
|
||||
workspace = relationship("Workspace")
|
||||
portfolio = relationship("Portfolio")
|
||||
environments = relationship("Environment", back_populates="application")
|
||||
|
||||
@property
|
||||
@ -22,6 +22,6 @@ class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
return self.name
|
||||
|
||||
def __repr__(self): # pragma: no cover
|
||||
return "<Application(name='{}', description='{}', workspace='{}', id='{}')>".format(
|
||||
self.name, self.description, self.workspace.name, self.id
|
||||
return "<Application(name='{}', description='{}', portfolio='{}', id='{}')>".format(
|
||||
self.name, self.description, self.portfolio.name, self.id
|
||||
)
|
||||
|
@ -15,7 +15,7 @@ class AuditEvent(Base, TimestampsMixin):
|
||||
user = relationship("User", backref="audit_events")
|
||||
|
||||
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 = relationship("Request", backref="audit_events")
|
||||
|
@ -30,17 +30,17 @@ class Environment(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def workspace(self):
|
||||
return self.application.workspace
|
||||
def portfolio(self):
|
||||
return self.application.portfolio
|
||||
|
||||
def auditable_workspace_id(self):
|
||||
def auditable_portfolio_id(self):
|
||||
return self.application.workspace_id
|
||||
|
||||
def __repr__(self):
|
||||
return "<Environment(name='{}', num_users='{}', application='{}', workspace='{}', id='{}')>".format(
|
||||
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
|
||||
self.name,
|
||||
self.num_users,
|
||||
self.application.name,
|
||||
self.application.workspace.name,
|
||||
self.application.portfolio.name,
|
||||
self.id,
|
||||
)
|
||||
|
@ -47,8 +47,8 @@ class EnvironmentRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
"environment_id": str(self.environment_id),
|
||||
"application": self.environment.application.name,
|
||||
"application_id": str(self.environment.project_id),
|
||||
"workspace": self.environment.application.workspace.name,
|
||||
"workspace_id": str(self.environment.application.workspace.id),
|
||||
"portfolio": self.environment.application.portfolio.name,
|
||||
"portfolio_id": str(self.environment.application.portfolio.id),
|
||||
}
|
||||
|
||||
|
||||
|
@ -30,8 +30,8 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
||||
workspace_role_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("workspace_roles.id"), index=True
|
||||
)
|
||||
workspace_role = relationship(
|
||||
"WorkspaceRole",
|
||||
portfolio_role = relationship(
|
||||
"PortfolioRole",
|
||||
backref=backref("invitations", order_by="Invitation.time_created"),
|
||||
)
|
||||
|
||||
@ -47,8 +47,8 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
||||
email = Column(String, nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Invitation(user='{}', workspace_role='{}', id='{}', email='{}')>".format(
|
||||
self.user_id, self.workspace_role_id, self.id, self.email
|
||||
return "<Invitation(user='{}', portfolio_role='{}', id='{}', email='{}')>".format(
|
||||
self.user_id, self.portfolio_role_id, self.id, self.email
|
||||
)
|
||||
|
||||
@property
|
||||
@ -91,13 +91,13 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
||||
]
|
||||
|
||||
@property
|
||||
def workspace(self):
|
||||
if self.workspace_role: # pragma: no branch
|
||||
return self.workspace_role.workspace
|
||||
def portfolio(self):
|
||||
if self.portfolio_role: # pragma: no branch
|
||||
return self.portfolio_role.portfolio
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
return self.workspace_role.user.full_name
|
||||
return self.portfolio_role.user.full_name
|
||||
|
||||
@property
|
||||
def is_revokable(self):
|
||||
@ -122,5 +122,5 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
||||
return change_set
|
||||
|
||||
@property
|
||||
def workspace_id(self):
|
||||
return self.workspace_role.workspace_id
|
||||
def portfolio_id(self):
|
||||
return self.portfolio_role.workspace_id
|
||||
|
@ -13,7 +13,7 @@ class AuditableMixin(object):
|
||||
@staticmethod
|
||||
def create_audit_event(connection, resource, action):
|
||||
user_id = getattr_path(g, "current_user.id")
|
||||
workspace_id = resource.workspace_id
|
||||
portfolio_id = resource.workspace_id
|
||||
request_id = resource.request_id
|
||||
resource_type = resource.resource_type
|
||||
display_name = resource.displayname
|
||||
@ -23,7 +23,7 @@ class AuditableMixin(object):
|
||||
|
||||
audit_event = AuditEvent(
|
||||
user_id=user_id,
|
||||
workspace_id=workspace_id,
|
||||
workspace_id=portfolio_id,
|
||||
request_id=request_id,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource.id,
|
||||
|
@ -1,10 +1,10 @@
|
||||
class Permissions(object):
|
||||
VIEW_AUDIT_LOG = "view_audit_log"
|
||||
VIEW_WORKSPACE_AUDIT_LOG = "view_workspace_audit_log"
|
||||
REQUEST_JEDI_WORKSPACE = "request_jedi_workspace"
|
||||
VIEW_WORKSPACE_AUDIT_LOG = "view_portfolio_audit_log"
|
||||
REQUEST_JEDI_WORKSPACE = "request_jedi_portfolio"
|
||||
VIEW_ORIGINAL_JEDI_REQEUST = "view_original_jedi_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"
|
||||
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_CSP_ROLE_CONFIGURATIONS = "view_assigned_csp_role_configurations"
|
||||
|
||||
EDIT_WORKSPACE_INFORMATION = "edit_workspace_information"
|
||||
DEACTIVATE_WORKSPACE = "deactivate_workspace"
|
||||
EDIT_WORKSPACE_INFORMATION = "edit_portfolio_information"
|
||||
DEACTIVATE_WORKSPACE = "deactivate_portfolio"
|
||||
VIEW_ATAT_PERMISSIONS = "view_atat_permissions"
|
||||
TRANSFER_OWNERSHIP_OF_WORKSPACE = "transfer_ownership_of_workspace"
|
||||
VIEW_WORKSPACE_MEMBERS = "view_workspace_members"
|
||||
VIEW_WORKSPACE = "view_workspace"
|
||||
TRANSFER_OWNERSHIP_OF_WORKSPACE = "transfer_ownership_of_portfolio"
|
||||
VIEW_WORKSPACE_MEMBERS = "view_portfolio_members"
|
||||
VIEW_WORKSPACE = "view_portfolio"
|
||||
|
||||
ADD_APPLICATION_IN_WORKSPACE = "add_application_in_workspace"
|
||||
DELETE_APPLICATION_IN_WORKSPACE = "delete_application_in_workspace"
|
||||
DEACTIVATE_APPLICATION_IN_WORKSPACE = "deactivate_application_in_workspace"
|
||||
VIEW_APPLICATION_IN_WORKSPACE = "view_application_in_workspace"
|
||||
RENAME_APPLICATION_IN_WORKSPACE = "rename_application_in_workspace"
|
||||
ADD_APPLICATION_IN_WORKSPACE = "add_application_in_portfolio"
|
||||
DELETE_APPLICATION_IN_WORKSPACE = "delete_application_in_portfolio"
|
||||
DEACTIVATE_APPLICATION_IN_WORKSPACE = "deactivate_application_in_portfolio"
|
||||
VIEW_APPLICATION_IN_WORKSPACE = "view_application_in_portfolio"
|
||||
RENAME_APPLICATION_IN_WORKSPACE = "rename_application_in_portfolio"
|
||||
|
||||
ADD_ENVIRONMENT_IN_APPLICATION = "add_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"
|
||||
RENAME_ENVIRONMENT_IN_APPLICATION = "rename_environment_in_application"
|
||||
|
||||
ADD_TAG_TO_WORKSPACE = "add_tag_to_workspace"
|
||||
REMOVE_TAG_FROM_WORKSPACE = "remove_tag_from_workspace"
|
||||
ADD_TAG_TO_WORKSPACE = "add_tag_to_portfolio"
|
||||
REMOVE_TAG_FROM_WORKSPACE = "remove_tag_from_portfolio"
|
||||
|
||||
VIEW_TASK_ORDER = "view_task_order"
|
||||
UPDATE_TASK_ORDER = "update_task_order"
|
||||
|
@ -3,28 +3,28 @@ from sqlalchemy.orm import relationship
|
||||
from itertools import chain
|
||||
|
||||
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.database import db
|
||||
|
||||
|
||||
class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
__tablename__ = "workspaces"
|
||||
|
||||
id = types.Id()
|
||||
name = Column(String)
|
||||
request_id = Column(ForeignKey("requests.id"), nullable=True)
|
||||
applications = relationship("Application", back_populates="workspace")
|
||||
roles = relationship("WorkspaceRole")
|
||||
applications = relationship("Application", back_populates="portfolio")
|
||||
roles = relationship("PortfolioRole")
|
||||
|
||||
task_orders = relationship("TaskOrder")
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
def _is_workspace_owner(workspace_role):
|
||||
return workspace_role.role.name == "owner"
|
||||
def _is_portfolio_owner(portfolio_role):
|
||||
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
|
||||
|
||||
@property
|
||||
@ -42,9 +42,9 @@ class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
@property
|
||||
def members(self):
|
||||
return (
|
||||
db.session.query(WorkspaceRole)
|
||||
.filter(WorkspaceRole.workspace_id == self.id)
|
||||
.filter(WorkspaceRole.status != WorkspaceRoleStatus.DISABLED)
|
||||
db.session.query(PortfolioRole)
|
||||
.filter(PortfolioRole.portfolio_id == self.id)
|
||||
.filter(PortfolioRole.status != PortfolioRoleStatus.DISABLED)
|
||||
.all()
|
||||
)
|
||||
|
||||
@ -56,10 +56,10 @@ class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
def all_environments(self):
|
||||
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
|
||||
|
||||
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
|
||||
)
|
@ -30,14 +30,14 @@ class Status(Enum):
|
||||
PENDING = "pending"
|
||||
|
||||
|
||||
class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
class PortfolioRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
__tablename__ = "workspace_roles"
|
||||
|
||||
id = Id()
|
||||
workspace_id = Column(
|
||||
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 = relationship("Role")
|
||||
@ -49,8 +49,8 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING)
|
||||
|
||||
def __repr__(self):
|
||||
return "<WorkspaceRole(role='{}', workspace='{}', user_id='{}', id='{}')>".format(
|
||||
self.role.name, self.workspace.name, self.user_id, self.id
|
||||
return "<PortfolioRole(role='{}', portfolio='{}', user_id='{}', id='{}')>".format(
|
||||
self.role.name, self.portfolio.name, self.user_id, self.id
|
||||
)
|
||||
|
||||
@property
|
||||
@ -127,8 +127,8 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
db.session.query(EnvironmentRole)
|
||||
.join(EnvironmentRole.environment)
|
||||
.join(Environment.application)
|
||||
.join(Application.workspace)
|
||||
.filter(Application.workspace_id == self.workspace_id)
|
||||
.join(Application.portfolio)
|
||||
.filter(Application.portfolio_id == self.portfolio_id)
|
||||
.filter(EnvironmentRole.user_id == self.user_id)
|
||||
.count()
|
||||
)
|
||||
@ -139,8 +139,8 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
db.session.query(EnvironmentRole)
|
||||
.join(EnvironmentRole.environment)
|
||||
.join(Environment.application)
|
||||
.join(Application.workspace)
|
||||
.filter(Application.workspace_id == self.workspace_id)
|
||||
.join(Application.portfolio)
|
||||
.filter(Application.portfolio_id == self.portfolio_id)
|
||||
.filter(EnvironmentRole.user_id == self.user_id)
|
||||
.all()
|
||||
)
|
||||
@ -157,8 +157,8 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
|
||||
|
||||
Index(
|
||||
"workspace_role_user_workspace",
|
||||
WorkspaceRole.user_id,
|
||||
WorkspaceRole.workspace_id,
|
||||
"portfolio_role_user_portfolio",
|
||||
PortfolioRole.user_id,
|
||||
PortfolioRole.workspace_id,
|
||||
unique=True,
|
||||
)
|
@ -34,7 +34,7 @@ class Request(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
"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)
|
||||
creator = relationship("User", backref="owned_requests")
|
||||
|
@ -25,7 +25,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
id = types.Id()
|
||||
|
||||
workspace_id = Column(ForeignKey("workspaces.id"))
|
||||
workspace = relationship("Workspace")
|
||||
portfolio = relationship("Portfolio")
|
||||
|
||||
user_id = Column(ForeignKey("users.id"))
|
||||
creator = relationship("User", foreign_keys="TaskOrder.user_id")
|
||||
@ -92,7 +92,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
|
||||
@property
|
||||
def portfolio_name(self):
|
||||
return self.workspace.name
|
||||
return self.portfolio.name
|
||||
|
||||
@property
|
||||
def is_pending(self):
|
||||
|
@ -14,7 +14,7 @@ class User(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
atat_role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"))
|
||||
|
||||
atat_role = relationship("Role")
|
||||
workspace_roles = relationship("WorkspaceRole", backref="user")
|
||||
portfolio_roles = relationship("PortfolioRole", backref="user")
|
||||
|
||||
email = Column(String, unique=True)
|
||||
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)
|
||||
|
||||
@property
|
||||
def has_workspaces(self):
|
||||
def has_portfolios(self):
|
||||
return (
|
||||
Permissions.VIEW_WORKSPACE in self.atat_role.permissions
|
||||
) or self.workspace_roles
|
||||
) or self.portfolio_roles
|
||||
|
||||
@property
|
||||
def displayname(self):
|
||||
return self.full_name
|
||||
|
||||
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.dod_id,
|
||||
self.email,
|
||||
self.atat_role_name,
|
||||
self.has_workspaces,
|
||||
self.has_portfolios,
|
||||
self.id,
|
||||
)
|
||||
|
||||
|
@ -52,25 +52,25 @@ def home():
|
||||
if user.atat_role_name == "ccpo":
|
||||
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"))
|
||||
elif num_workspaces == 1:
|
||||
workspace_role = user.workspace_roles[0]
|
||||
workspace_id = workspace_role.workspace.id
|
||||
is_request_owner = workspace_role.role.name == "owner"
|
||||
elif num_portfolios == 1:
|
||||
portfolio_role = user.portfolio_roles[0]
|
||||
portfolio_id = portfolio_role.portfolio.id
|
||||
is_request_owner = portfolio_role.role.name == "owner"
|
||||
|
||||
if is_request_owner:
|
||||
return redirect(
|
||||
url_for("workspaces.workspace_reports", workspace_id=workspace_id)
|
||||
url_for("portfolios.portfolio_reports", portfolio_id=portfolio_id)
|
||||
)
|
||||
else:
|
||||
return redirect(
|
||||
url_for("workspaces.workspace_applications", workspace_id=workspace_id)
|
||||
url_for("portfolios.portfolio_applications", portfolio_id=portfolio_id)
|
||||
)
|
||||
else:
|
||||
return redirect(url_for("workspaces.workspaces"))
|
||||
return redirect(url_for("portfolios.portfolios"))
|
||||
|
||||
|
||||
@bp.route("/styleguide")
|
||||
|
@ -8,7 +8,7 @@ from atst.domain.invitations import (
|
||||
ExpiredError as InvitationExpiredError,
|
||||
WrongUserError as InvitationWrongUserError,
|
||||
)
|
||||
from atst.domain.workspaces import WorkspaceError
|
||||
from atst.domain.portfolios import PortfolioError
|
||||
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(exceptions.NotFoundError)
|
||||
@app.errorhandler(exceptions.UnauthorizedError)
|
||||
@app.errorhandler(WorkspaceError)
|
||||
@app.errorhandler(PortfolioError)
|
||||
# pylint: disable=unused-variable
|
||||
def not_found(e):
|
||||
return handle_error(e)
|
||||
|
41
atst/routes/portfolios/__init__.py
Normal file
41
atst/routes/portfolios/__init__.py
Normal 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,
|
||||
}
|
102
atst/routes/portfolios/applications.py
Normal file
102
atst/routes/portfolios/applications.py
Normal 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))
|
102
atst/routes/portfolios/index.py
Normal file
102
atst/routes/portfolios/index.py
Normal 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,
|
||||
)
|
@ -1,7 +1,7 @@
|
||||
from flask import g, redirect, url_for, render_template
|
||||
|
||||
from . import workspaces_bp
|
||||
from atst.domain.workspaces import Workspaces
|
||||
from . import portfolios_bp
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.domain.invitations import Invitations
|
||||
from atst.queue import queue
|
||||
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)
|
||||
queue.send_mail(
|
||||
[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,
|
||||
)
|
||||
|
||||
|
||||
@workspaces_bp.route("/workspaces/invitations/<token>", methods=["GET"])
|
||||
@portfolios_bp.route("/portfolios/invitations/<token>", methods=["GET"])
|
||||
def accept_invitation(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:
|
||||
# - 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
|
||||
for task_order in invite.workspace.task_orders:
|
||||
for task_order in invite.portfolio.task_orders:
|
||||
if g.current_user == task_order.contracting_officer:
|
||||
return redirect(
|
||||
url_for("task_orders.new", screen=4, task_order_id=task_order.id)
|
||||
@ -40,25 +40,25 @@ def accept_invitation(token):
|
||||
)
|
||||
|
||||
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(
|
||||
"/workspaces/<workspace_id>/invitations/<token>/revoke", methods=["POST"]
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/invitations/<token>/revoke", methods=["POST"]
|
||||
)
|
||||
def revoke_invitation(workspace_id, token):
|
||||
workspace = Workspaces.get_for_update_member(g.current_user, workspace_id)
|
||||
def revoke_invitation(portfolio_id, token):
|
||||
portfolio = Portfolios.get_for_update_member(g.current_user, portfolio_id)
|
||||
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(
|
||||
"/workspaces/<workspace_id>/invitations/<token>/resend", methods=["POST"]
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/invitations/<token>/resend", methods=["POST"]
|
||||
)
|
||||
def resend_invitation(workspace_id, token):
|
||||
invite = Invitations.resend(g.current_user, workspace_id, token)
|
||||
def resend_invitation(portfolio_id, token):
|
||||
invite = Invitations.resend(g.current_user, portfolio_id, token)
|
||||
send_invite_email(g.current_user.full_name, invite.token, invite.email)
|
||||
flash("resend_workspace_invitation", user_name=invite.user_name)
|
||||
return redirect(url_for("workspaces.workspace_members", workspace_id=workspace_id))
|
||||
flash("resend_portfolio_invitation", user_name=invite.user_name)
|
||||
return redirect(url_for("portfolios.portfolio_members", portfolio_id=portfolio_id))
|
@ -2,11 +2,11 @@ import re
|
||||
|
||||
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.applications import Applications
|
||||
from atst.domain.workspaces import Workspaces
|
||||
from atst.domain.workspace_roles import WorkspaceRoles, MEMBER_STATUS_CHOICES
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.domain.portfolio_roles import PortfolioRoles, MEMBER_STATUS_CHOICES
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
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
|
||||
|
||||
|
||||
@workspaces_bp.route("/workspaces/<workspace_id>/members")
|
||||
def workspace_members(workspace_id):
|
||||
workspace = Workspaces.get_with_members(g.current_user, workspace_id)
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/members")
|
||||
def portfolio_members(portfolio_id):
|
||||
portfolio = Portfolios.get_with_members(g.current_user, portfolio_id)
|
||||
new_member_name = http_request.args.get("newMemberName")
|
||||
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 = [
|
||||
{
|
||||
@ -38,15 +38,15 @@ def workspace_members(workspace_id):
|
||||
"role": k.role_displayname,
|
||||
"num_env": k.num_environment_roles,
|
||||
"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(
|
||||
"workspaces/members/index.html",
|
||||
workspace=workspace,
|
||||
"portfolios/members/index.html",
|
||||
portfolio=portfolio,
|
||||
role_choices=WORKSPACE_ROLE_DEFINITIONS,
|
||||
status_choices=MEMBER_STATUS_CHOICES,
|
||||
members=members_list,
|
||||
@ -54,32 +54,32 @@ def workspace_members(workspace_id):
|
||||
)
|
||||
|
||||
|
||||
@workspaces_bp.route("/workspaces/<workspace_id>/members/new")
|
||||
def new_member(workspace_id):
|
||||
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/members/new")
|
||||
def new_member(portfolio_id):
|
||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
form = NewMemberForm()
|
||||
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"])
|
||||
def create_member(workspace_id):
|
||||
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/members/new", methods=["POST"])
|
||||
def create_member(portfolio_id):
|
||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
form = NewMemberForm(http_request.form)
|
||||
|
||||
if form.validate():
|
||||
try:
|
||||
member = Workspaces.create_member(g.current_user, workspace, form.data)
|
||||
member = Portfolios.create_member(g.current_user, portfolio, form.data)
|
||||
invite_service = InvitationService(
|
||||
g.current_user, member, form.data.get("email")
|
||||
)
|
||||
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(
|
||||
url_for("workspaces.workspace_members", workspace_id=workspace.id)
|
||||
url_for("portfolios.portfolio_members", portfolio_id=portfolio.id)
|
||||
)
|
||||
except AlreadyExistsError:
|
||||
return render_template(
|
||||
@ -87,31 +87,31 @@ def create_member(workspace_id):
|
||||
)
|
||||
else:
|
||||
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")
|
||||
def view_member(workspace_id, member_id):
|
||||
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||
Authorization.check_workspace_permission(
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/members/<member_id>/member_edit")
|
||||
def view_member(portfolio_id, member_id):
|
||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
Authorization.check_portfolio_permission(
|
||||
g.current_user,
|
||||
workspace,
|
||||
portfolio,
|
||||
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
|
||||
"edit this workspace user",
|
||||
"edit this portfolio user",
|
||||
)
|
||||
member = WorkspaceRoles.get(workspace_id, member_id)
|
||||
applications = Applications.get_all(g.current_user, member, workspace)
|
||||
form = EditMemberForm(workspace_role=member.role_name)
|
||||
member = PortfolioRoles.get(portfolio_id, member_id)
|
||||
applications = Applications.get_all(g.current_user, member, portfolio)
|
||||
form = EditMemberForm(portfolio_role=member.role_name)
|
||||
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:
|
||||
flash("workspace_member_dod_id_error")
|
||||
flash("portfolio_member_dod_id_error")
|
||||
|
||||
return render_template(
|
||||
"workspaces/members/edit.html",
|
||||
workspace=workspace,
|
||||
"portfolios/members/edit.html",
|
||||
portfolio=portfolio,
|
||||
member=member,
|
||||
applications=applications,
|
||||
form=form,
|
||||
@ -123,18 +123,18 @@ def view_member(workspace_id, member_id):
|
||||
)
|
||||
|
||||
|
||||
@workspaces_bp.route(
|
||||
"/workspaces/<workspace_id>/members/<member_id>/member_edit", methods=["POST"]
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/members/<member_id>/member_edit", methods=["POST"]
|
||||
)
|
||||
def update_member(workspace_id, member_id):
|
||||
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||
Authorization.check_workspace_permission(
|
||||
def update_member(portfolio_id, member_id):
|
||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
Authorization.check_portfolio_permission(
|
||||
g.current_user,
|
||||
workspace,
|
||||
portfolio,
|
||||
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 = []
|
||||
form_dict = http_request.form.to_dict()
|
||||
@ -147,39 +147,39 @@ def update_member(workspace_id, member_id):
|
||||
form = EditMemberForm(http_request.form)
|
||||
if form.validate():
|
||||
new_role_name = None
|
||||
if form.data["workspace_role"] != member.role.name:
|
||||
member = Workspaces.update_member(
|
||||
g.current_user, workspace, member, form.data["workspace_role"]
|
||||
if form.data["portfolio_role"] != member.role.name:
|
||||
member = Portfolios.update_member(
|
||||
g.current_user, portfolio, member, form.data["portfolio_role"]
|
||||
)
|
||||
new_role_name = member.role_displayname
|
||||
flash(
|
||||
"workspace_role_updated",
|
||||
"portfolio_role_updated",
|
||||
member_name=member.user_name,
|
||||
updated_role=new_role_name,
|
||||
)
|
||||
|
||||
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:
|
||||
flash("environment_access_changed")
|
||||
|
||||
return redirect(
|
||||
url_for("workspaces.workspace_members", workspace_id=workspace.id)
|
||||
url_for("portfolios.portfolio_members", portfolio_id=portfolio.id)
|
||||
)
|
||||
else:
|
||||
return render_template(
|
||||
"workspaces/members/edit.html",
|
||||
"portfolios/members/edit.html",
|
||||
form=form,
|
||||
workspace=workspace,
|
||||
portfolio=portfolio,
|
||||
member=member,
|
||||
)
|
||||
|
||||
|
||||
@workspaces_bp.route(
|
||||
"/workspaces/<workspace_id>/members/<member_id>/revoke_access", methods=["POST"]
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/members/<member_id>/revoke_access", methods=["POST"]
|
||||
)
|
||||
def revoke_access(workspace_id, member_id):
|
||||
revoked_role = Workspaces.revoke_access(g.current_user, workspace_id, member_id)
|
||||
flash("revoked_workspace_access", member_name=revoked_role.user.full_name)
|
||||
return redirect(url_for("workspaces.workspace_members", workspace_id=workspace_id))
|
||||
def revoke_access(portfolio_id, member_id):
|
||||
revoked_role = Portfolios.revoke_access(g.current_user, portfolio_id, member_id)
|
||||
flash("revoked_portfolio_access", member_name=revoked_role.user.full_name)
|
||||
return redirect(url_for("portfolios.portfolio_members", portfolio_id=portfolio_id))
|
20
atst/routes/portfolios/task_orders.py
Normal file
20
atst/routes/portfolios/task_orders.py
Normal 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
|
||||
)
|
@ -248,10 +248,10 @@ def update_financial_verification(request_id):
|
||||
)
|
||||
|
||||
if updated_request.legacy_task_order.verified:
|
||||
workspace = Requests.auto_approve_and_create_workspace(updated_request)
|
||||
flash("new_workspace")
|
||||
portfolio = Requests.auto_approve_and_create_portfolio(updated_request)
|
||||
flash("new_portfolio")
|
||||
return redirect(
|
||||
url_for("workspaces.new_application", workspace_id=workspace.id)
|
||||
url_for("portfolios.new_application", portfolio_id=portfolio.id)
|
||||
)
|
||||
else:
|
||||
return redirect(url_for("requests.requests_index", modal="pendingCCPOApproval"))
|
||||
|
@ -63,10 +63,10 @@ class RequestsIndex(object):
|
||||
"extended_view": False,
|
||||
}
|
||||
|
||||
def _workspace_link_for_request(self, request):
|
||||
def _portfolio_link_for_request(self, request):
|
||||
if request.is_approved:
|
||||
return url_for(
|
||||
"workspaces.workspace_applications", workspace_id=request.workspace.id
|
||||
"portfolios.portfolio_applications", portfolio_id=request.portfolio.id
|
||||
)
|
||||
else:
|
||||
return None
|
||||
@ -80,7 +80,7 @@ class RequestsIndex(object):
|
||||
annual_usage = request.annual_spend
|
||||
|
||||
return {
|
||||
"workspace_id": request.workspace.id if request.workspace else None,
|
||||
"portfolio_id": request.portfolio.id if request.portfolio else None,
|
||||
"name": request.displayname,
|
||||
"is_new": is_new,
|
||||
"is_approved": request.is_approved,
|
||||
@ -93,7 +93,7 @@ class RequestsIndex(object):
|
||||
"edit_link": url_for("requests.edit", request_id=request.id),
|
||||
"action_required": request.action_required_by == viewing_role,
|
||||
"dod_component": request.latest_revision.dod_component,
|
||||
"workspace_link": self._workspace_link_for_request(request),
|
||||
"portfolio_link": self._portfolio_link_for_request(request),
|
||||
}
|
||||
|
||||
|
||||
|
@ -113,9 +113,9 @@ class JEDIRequestFlow(object):
|
||||
"form": request_forms.InformationAboutYouForm,
|
||||
},
|
||||
{
|
||||
"title": "Workspace Owner",
|
||||
"title": "Portfolio Owner",
|
||||
"section": "primary_poc",
|
||||
"form": request_forms.WorkspaceOwnerForm,
|
||||
"form": request_forms.PortfolioOwnerForm,
|
||||
},
|
||||
{
|
||||
"title": "Review & Submit",
|
||||
|
@ -10,5 +10,5 @@ def invite(task_order_id):
|
||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||
flash("task_order_submitted", task_order=task_order)
|
||||
return redirect(
|
||||
url_for("workspaces.workspace_members", workspace_id=task_order.workspace.id)
|
||||
url_for("portfolios.portfolio_members", portfolio_id=task_order.portfolio.id)
|
||||
)
|
||||
|
@ -11,8 +11,8 @@ from flask import (
|
||||
|
||||
from . import task_orders_bp
|
||||
from atst.domain.task_orders import TaskOrders
|
||||
from atst.domain.workspaces import Workspaces
|
||||
from atst.domain.workspace_roles import WorkspaceRoles
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.domain.portfolio_roles import PortfolioRoles
|
||||
import atst.forms.task_order as task_order_form
|
||||
from atst.services.invitation import Invitation as InvitationService
|
||||
|
||||
@ -114,9 +114,9 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
||||
return self._form
|
||||
|
||||
@property
|
||||
def workspace(self):
|
||||
def portfolio(self):
|
||||
if self.task_order:
|
||||
return self.task_order.workspace
|
||||
return self.task_order.portfolio
|
||||
|
||||
def validate(self):
|
||||
return self.form.validate()
|
||||
@ -125,7 +125,7 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
||||
if self.task_order:
|
||||
TaskOrders.update(self.user, self.task_order, **self.form.data)
|
||||
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.pop("portfolio_name")
|
||||
self._task_order = TaskOrders.create(self.user, ws)
|
||||
@ -177,7 +177,7 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
||||
officer = TaskOrders.add_officer(
|
||||
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(
|
||||
self.user,
|
||||
ws_officer_member,
|
||||
|
@ -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,
|
||||
}
|
@ -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))
|
@ -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,
|
||||
)
|
@ -10,7 +10,7 @@ class Invitation:
|
||||
inviter,
|
||||
member,
|
||||
email,
|
||||
subject="{} has invited you to a JEDI Cloud Workspace",
|
||||
subject="{} has invited you to a JEDI Cloud Portfolio",
|
||||
email_template="emails/invitation.txt",
|
||||
):
|
||||
self.inviter = inviter
|
||||
|
@ -1,29 +1,29 @@
|
||||
from flask import flash, render_template_string
|
||||
|
||||
MESSAGES = {
|
||||
"new_workspace_member": {
|
||||
"new_portfolio_member": {
|
||||
"title_template": "Member added successfully",
|
||||
"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><a href="{{ url_for('workspaces.update_member', workspace_id=workspace.id, member_id=new_member.user_id) }}">Add environment access.</a></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('portfolios.update_member', portfolio_id=portfolio.id, member_id=new_member.user_id) }}">Add environment access.</a></p>
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
"revoked_workspace_access": {
|
||||
"title_template": "Removed workspace access",
|
||||
"revoked_portfolio_access": {
|
||||
"title_template": "Removed portfolio access",
|
||||
"message_template": """
|
||||
<p>Portfolio access successfully removed from {{ member_name }}.</p>
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
"resend_workspace_invitation": {
|
||||
"resend_portfolio_invitation": {
|
||||
"title_template": "Invitation resent",
|
||||
"message_template": """
|
||||
<p>Successfully sent a new invitation to {{ user_name }}.</p>
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
"workspace_role_updated": {
|
||||
"portfolio_role_updated": {
|
||||
"title_template": "Portfolio role updated successfully",
|
||||
"message_template": """
|
||||
<p>{{ member_name }}'s role was successfully updated to {{ updated_role }}</p>
|
||||
@ -44,14 +44,14 @@ MESSAGES = {
|
||||
""",
|
||||
"category": "warning",
|
||||
},
|
||||
"new_workspace": {
|
||||
"new_portfolio": {
|
||||
"title_template": "Portfolio created!",
|
||||
"message_template": """
|
||||
<p>You are now ready to create applications and environments within the JEDI Cloud.</p>
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
"workspace_member_dod_id_error": {
|
||||
"portfolio_member_dod_id_error": {
|
||||
"title_template": "CAC ID Error",
|
||||
"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.
|
||||
|
@ -84,7 +84,7 @@ export default {
|
||||
sortFunc: alphabeticalSort
|
||||
},
|
||||
{
|
||||
displayName: 'Workspace Role',
|
||||
displayName: 'Portfolio Role',
|
||||
attr: 'role',
|
||||
sortFunc: alphabeticalSort,
|
||||
},
|
||||
|
@ -6,7 +6,7 @@ export default {
|
||||
|
||||
props: {
|
||||
applications: Object,
|
||||
workspace: Object,
|
||||
portfolio: Object,
|
||||
environments: Object,
|
||||
currentMonthIndex: String,
|
||||
prevMonthIndex: String,
|
||||
|
@ -83,10 +83,10 @@ export default {
|
||||
unmask: [],
|
||||
validationError: 'Please enter a valid BA Code. Note that it should be two digits, followed by an optional letter.'
|
||||
},
|
||||
workspaceName: {
|
||||
portfolioName: {
|
||||
mask: false,
|
||||
match: /^.{4,100}$/,
|
||||
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'
|
||||
},
|
||||
}
|
||||
|
@ -25,7 +25,7 @@
|
||||
@import 'components/topbar';
|
||||
@import 'components/global_layout';
|
||||
@import 'components/global_navigation';
|
||||
@import 'components/workspace_layout';
|
||||
@import 'components/portfolio_layout';
|
||||
@import 'components/site_action';
|
||||
@import 'components/empty_state';
|
||||
@import 'components/alerts';
|
||||
|
@ -18,7 +18,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.global-navigation__context--workspace {
|
||||
&.global-navigation__context--portfolio {
|
||||
.sidenav__link {
|
||||
padding-right: $gap;
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
.workspace-panel-container {
|
||||
.portfolio-panel-container {
|
||||
@include media($large-screen) {
|
||||
@include grid-row;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-navigation {
|
||||
.portfolio-navigation {
|
||||
@include panel-margin;
|
||||
margin-bottom: $gap * 4;
|
||||
|
@ -59,11 +59,11 @@
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
|
||||
.topbar__workspace-menu {
|
||||
.topbar__portfolio-menu {
|
||||
margin-right: auto;
|
||||
position: relative;
|
||||
|
||||
.topbar__workspace-menu__toggle {
|
||||
.topbar__portfolio-menu__toggle {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
|
||||
@ -89,12 +89,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.topbar__workspace-menu__panel {
|
||||
.topbar__portfolio-menu__panel {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&.topbar__context--workspace {
|
||||
&.topbar__context--portfolio {
|
||||
background-color: $color-primary;
|
||||
-ms-flex-pack: start;
|
||||
|
||||
|
@ -207,7 +207,7 @@
|
||||
&--validation {
|
||||
|
||||
&--anything,
|
||||
&--workspaceName,
|
||||
&--portfolioName,
|
||||
&--requiredField,
|
||||
&--email {
|
||||
input {
|
||||
|
@ -283,7 +283,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.spend-table__workspace {
|
||||
.spend-table__portfolio {
|
||||
th, td {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "audit_log/events/_base.html" %}
|
||||
|
||||
{% block content %}
|
||||
in Portfolio <code>{{ event.workspace_id }}</code> ({{ event.workspace.name }})
|
||||
in Portfolio <code>{{ event.portfolio_id }}</code> ({{ event.portfolio.name }})
|
||||
{% endblock %}
|
||||
|
@ -14,6 +14,6 @@
|
||||
<br>
|
||||
in Application <code>{{ event.event_details["application_id"] }}</code> ({{ event.event_details["application"] }})
|
||||
<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 %}
|
||||
{% endblock %}
|
||||
|
@ -10,5 +10,5 @@
|
||||
invited {{ event.event_details.email }} (DOD <code>{{ event.event_details.dod_id }}</code>)
|
||||
<br>
|
||||
{% endif %}
|
||||
in Portfolio <code>{{ event.workspace_id }}</code> ({{ event.workspace.name }})
|
||||
in Portfolio <code>{{ event.portfolio_id }}</code> ({{ event.portfolio.name }})
|
||||
{% endblock %}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% block content %}
|
||||
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 %}
|
||||
from status "{{ event.changed_state.status[0] }}" to "{{ event.changed_state.status[1] }}"
|
@ -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 button_class = "page usa-button " %}
|
||||
@ -11,42 +11,42 @@
|
||||
{% set button_class = button_class + "usa-button-secondary" %}
|
||||
{% 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 %}
|
||||
|
||||
{% macro Pagination(pagination, route, workspace_id=None) -%}
|
||||
{% macro Pagination(pagination, route, portfolio_id=None) -%}
|
||||
<div class="pagination">
|
||||
|
||||
{% if pagination.page == 1 %}
|
||||
{{ Page(pagination, route, 1, label="first", disabled=True, workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.page - 1, label="prev", 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, portfolio_id=portfolio_id) }}
|
||||
{% else %}
|
||||
{{ Page(pagination, route, 1, label="first", workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.page - 1, label="prev", workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, 1, label="first", portfolio_id=portfolio_id) }}
|
||||
{{ Page(pagination, route, pagination.page - 1, label="prev", portfolio_id=portfolio_id) }}
|
||||
{% endif %}
|
||||
|
||||
{% if pagination.page == 1 %}
|
||||
{% set max_page = [pagination.pages, 5] | min %}
|
||||
{% 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 %}
|
||||
{% elif pagination.page == pagination.pages %}
|
||||
{% 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 %}
|
||||
{% else %}
|
||||
{% set window = pagination | pageWindow %}
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
|
||||
{% if pagination.page == pagination.pages %}
|
||||
{{ Page(pagination, route, pagination.page + 1, label="next", disabled=True, workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.pages, label="last", 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, portfolio_id=portfolio_id) }}
|
||||
{% else %}
|
||||
{{ Page(pagination, route, pagination.page + 1, label="next", workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.pages, label="last", workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.page + 1, label="next", portfolio_id=portfolio_id) }}
|
||||
{{ Page(pagination, route, pagination.pages, label="last", portfolio_id=portfolio_id) }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
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.
|
||||
|
||||
{{ url_for("workspaces.accept_invitation", token=token, _external=True) }}
|
||||
{{ url_for("portfolios.accept_invitation", token=token, _external=True) }}
|
||||
|
||||
What is JEDI Cloud?
|
||||
JEDI Cloud is a DoD enterprise-wide solution for commercial cloud services.
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% 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>
|
||||
{{ SidenavItem("New Task Order",
|
||||
href=url_for("task_orders.get_started"),
|
||||
@ -8,8 +8,8 @@
|
||||
active=g.matchesPath('/task_orders/new'),
|
||||
) }}
|
||||
|
||||
{% if g.current_user.has_workspaces %}
|
||||
{{ SidenavItem("Portfolios", href="/workspaces", icon="cloud", active=g.matchesPath('/workspaces')) }}
|
||||
{% if g.current_user.has_portfolios %}
|
||||
{{ SidenavItem("Portfolios", href="/portfolios", icon="cloud", active=g.matchesPath('/portfolios')) }}
|
||||
{% endif %}
|
||||
|
||||
{% if g.Authorization.has_atat_permission(g.current_user, g.Permissions.VIEW_AUDIT_LOG) %}
|
||||
|
68
templates/navigation/portfolio_navigation.html
Normal file
68
templates/navigation/portfolio_navigation.html
Normal 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>
|
@ -2,7 +2,7 @@
|
||||
|
||||
<header class="topbar">
|
||||
<nav class="topbar__navigation">
|
||||
{% if not workspace %}
|
||||
{% if not portfolio %}
|
||||
<a href="{{ url_for('atst.home') }}" class="topbar__link topbar__link--home">
|
||||
{{ Icon('shield', classes='topbar__link-icon') }}
|
||||
<span class="topbar__link-label">
|
||||
@ -15,31 +15,31 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="topbar__context {% if workspace %}topbar__context--workspace{% endif %}">
|
||||
{% if workspace %}
|
||||
<div class="topbar__context {% if portfolio %}topbar__context--portfolio{% endif %}">
|
||||
{% if portfolio %}
|
||||
|
||||
<div is='toggler' class='topbar__workspace-menu'>
|
||||
<div is='toggler' class='topbar__portfolio-menu'>
|
||||
<template slot-scope='props'>
|
||||
<button
|
||||
v-on:click='props.toggle'
|
||||
class="topbar__link topbar__workspace-menu__toggle"
|
||||
v-bind:class="{ 'topbar__workspace-menu__toggle--open': props.isVisible }">
|
||||
<span class="topbar__link-label">{{ "navigation.topbar.named_workspace" | translate({ "workspace": workspace.name }) }}</span>
|
||||
class="topbar__link topbar__portfolio-menu__toggle"
|
||||
v-bind:class="{ 'topbar__portfolio-menu__toggle--open': props.isVisible }">
|
||||
<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-else>{{ Icon('caret_down', classes='topbar__link-icon') }}</template>
|
||||
</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'>
|
||||
{{ "navigation.topbar.other_active_workspaces" | translate }}
|
||||
{{ "navigation.topbar.other_active_portfolios" | translate }}
|
||||
</h2>
|
||||
{% if workspaces %}
|
||||
{% if portfolios %}
|
||||
|
||||
<ul class='menu__list'>
|
||||
{% for other_workspace in workspaces %}
|
||||
{% for other_portfolio in portfolios %}
|
||||
<li class='menu__list__item'>
|
||||
<a href="{{ url_for('workspaces.show_workspace', workspace_id=other_workspace.id)}}">
|
||||
{{ other_workspace.name }}
|
||||
<a href="{{ url_for('portfolios.show_portfolio', portfolio_id=other_portfolio.id)}}">
|
||||
{{ other_portfolio.name }}
|
||||
{{ Icon('caret_right', classes='topbar__link-icon') }}
|
||||
</a>
|
||||
</li>
|
||||
@ -49,7 +49,7 @@
|
||||
{% else %}
|
||||
|
||||
<p class='menu__message'>
|
||||
{{ "navigation.topbar.no_other_active_workspaces" | translate }}
|
||||
{{ "navigation.topbar.no_other_active_portfolios" | translate }}
|
||||
</p>
|
||||
|
||||
{% endif %}
|
||||
|
@ -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>
|
9
templates/portfolios/activity/index.html
Normal file
9
templates/portfolios/activity/index.html
Normal 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 %}
|
@ -1,10 +1,10 @@
|
||||
{% extends "workspaces/base.html" %}
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% 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" %}
|
||||
|
@ -1,31 +1,31 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% 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) %}
|
||||
|
||||
{{ EmptyState(
|
||||
'This portfolio doesn’t have any applications yet.',
|
||||
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',
|
||||
sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.'
|
||||
) }}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% for application in workspace.applications %}
|
||||
{% for application in portfolio.applications %}
|
||||
<div v-cloak class='block-list application-list-item'>
|
||||
<header class='block-list__header'>
|
||||
<h2 class='block-list__title'>{{ application.name }} ({{ application.environments|length }} environments)</h2>
|
||||
{% 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') }}
|
||||
<span>edit</span>
|
||||
</a>
|
||||
@ -34,7 +34,7 @@
|
||||
<ul>
|
||||
{% for environment in application.environments %}
|
||||
<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') }}
|
||||
<span>{{ environment.name }}</span>
|
||||
</a>
|
@ -1,17 +1,17 @@
|
||||
{% extends "workspaces/base.html" %}
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/modal.html" import Modal %}
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
|
||||
{% block workspace_content %}
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% set modalName = "newApplicationConfirmation" %}
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<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) %}
|
||||
<h1>Create application !{ name }</h1>
|
15
templates/portfolios/base.html
Normal file
15
templates/portfolios/base.html
Normal 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 %}
|
@ -1,13 +1,13 @@
|
||||
{% extends "workspaces/base.html" %}
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
|
||||
{% block workspace_content %}
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% 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 }}
|
||||
|
||||
<div class="panel">
|
||||
@ -17,14 +17,14 @@
|
||||
</div>
|
||||
|
||||
<div class="panel__content">
|
||||
{{ TextInput(form.name, validation="workspaceName") }}
|
||||
{{ TextInput(form.name, validation="portfolioName") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='action-group'>
|
||||
<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') }}
|
||||
<span>Cancel</span>
|
||||
</a>
|
@ -11,16 +11,16 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for workspace in workspaces %}
|
||||
{% for portfolio in portfolios %}
|
||||
<tr>
|
||||
<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>
|
||||
#{{ workspace.legacy_task_order.number }}
|
||||
#{{ portfolio.legacy_task_order.number }}
|
||||
</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>
|
||||
</tr>
|
||||
{% endfor %}
|
@ -10,7 +10,7 @@
|
||||
|
||||
{% 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 }}
|
||||
|
||||
<div class='panel member-card'>
|
||||
@ -18,7 +18,7 @@
|
||||
<h1 class='member-card__heading'>{{ member.user.full_name }}</h1>
|
||||
|
||||
<div class="usa-input member-card__input">
|
||||
{{ Selector(form.workspace_role) }}
|
||||
{{ Selector(form.portfolio_role) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -40,20 +40,20 @@
|
||||
{% if member.latest_invitation.is_revokable %}
|
||||
{{ ConfirmationButton(
|
||||
"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 %}
|
||||
{% if member.can_resend_invitation %}
|
||||
{{ ConfirmationButton (
|
||||
"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."
|
||||
)}}
|
||||
{% endif %}
|
||||
{% if can_revoke_access %}
|
||||
{{ ConfirmationButton (
|
||||
"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.",
|
||||
)}}
|
||||
{% endif %}
|
||||
@ -177,7 +177,7 @@
|
||||
<button class='action-group__action usa-button usa-button-big'>
|
||||
{% if is_new_member %}Create{% else %}Save{% endif %}
|
||||
</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') }}
|
||||
<span>Cancel</span>
|
||||
</a>
|
@ -1,11 +1,11 @@
|
||||
{% extends "workspaces/base.html" %}
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% from "components/empty_state.html" import EmptyState %}
|
||||
{% 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) %}
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
{% 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 }}
|
||||
|
||||
<div class="panel">
|
||||
@ -22,14 +22,14 @@
|
||||
{{ TextInput(form.last_name) }}
|
||||
{{ 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') }}
|
||||
{{ Selector(form.workspace_role) }}
|
||||
{{ Selector(form.portfolio_role) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='action-group'>
|
||||
<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') }}
|
||||
<span>Cancel</span>
|
||||
</a>
|
@ -1,12 +1,12 @@
|
||||
{% extends "workspaces/base.html" %}
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% 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>\
|
||||
<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=[
|
||||
@ -20,8 +20,8 @@
|
||||
<div class='row'>
|
||||
<h2 class='spend-summary__heading col'>Portfolio Total Spend</h2>
|
||||
<dl class='spend-summary__budget'>
|
||||
{% set budget = workspace_totals['budget'] %}
|
||||
{% set spent = workspace_totals['spent'] %}
|
||||
{% set budget = portfolio_totals['budget'] %}
|
||||
{% set spent = portfolio_totals['spent'] %}
|
||||
{% set remaining = budget - spent %}
|
||||
<div>
|
||||
<dt>Budget </dt>
|
||||
@ -88,7 +88,7 @@
|
||||
</div>
|
||||
</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
|
||||
</a>
|
||||
</div>
|
||||
@ -107,13 +107,13 @@
|
||||
|
||||
</div>
|
||||
|
||||
{% set workspace_totals = monthly_totals['workspace'] %}
|
||||
{% set portfolio_totals = monthly_totals['portfolio'] %}
|
||||
{% set current_month_index = current_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 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 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(
|
||||
'Nothing to report',
|
||||
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',
|
||||
sub_message=message
|
||||
) }}
|
||||
@ -338,8 +338,8 @@
|
||||
{% if month.month == current_month.month and month.year == current_month.year %}
|
||||
selected='selected'
|
||||
{% endif %}
|
||||
value='{{ url_for("workspaces.workspace_reports",
|
||||
workspace_id=workspace.id,
|
||||
value='{{ url_for("portfolios.portfolio_reports",
|
||||
portfolio_id=portfolio.id,
|
||||
month=month.month,
|
||||
year=month.year) }}'
|
||||
>
|
||||
@ -354,7 +354,7 @@
|
||||
|
||||
<spend-table
|
||||
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 }}'
|
||||
current-month-index='{{ current_month_index }}'
|
||||
prev-month-index='{{ prev_month_index }}'
|
||||
@ -369,14 +369,14 @@
|
||||
<th class='current-month'>% of total spend this month</th>
|
||||
</thead>
|
||||
|
||||
<tbody class='spend-table__workspace'>
|
||||
<tbody class='spend-table__portfolio'>
|
||||
<tr>
|
||||
<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'>{{ workspace_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 previous-month'>{{ portfolio_totals.get(two_months_ago_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'>{{ portfolio_totals.get(current_month_index, 0) | dollars }}</td>
|
||||
<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>
|
||||
</meter>
|
||||
</td>
|
||||
@ -406,10 +406,10 @@
|
||||
|
||||
<td class='table-cell--expand current-month meter-cell'>
|
||||
<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>
|
||||
<meter v-bind:value='application[currentMonthIndex] || 0' min='0' v-bind:max='workspace[currentMonthIndex] || 1'>
|
||||
<div class='meter__fallback' v-bind:style='"width:" + round( 100 * ((application[currentMonthIndex] || 0) / (workspace[currentMonthIndex] || 1) )) + "%;"'></div>
|
||||
<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) / (portfolio[currentMonthIndex] || 1) )) + "%;"'></div>
|
||||
</meter>
|
||||
</td>
|
||||
</tr>
|
@ -1,24 +1,24 @@
|
||||
{% 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(
|
||||
'This portfolio doesn’t have any task orders yet.',
|
||||
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',
|
||||
) }}
|
||||
|
||||
{% else %}
|
||||
|
||||
<ul>
|
||||
{% for task_order in workspace.task_orders %}
|
||||
{% for task_order in portfolio.task_orders %}
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
7
templates/portfolios/task_orders/show.html
Normal file
7
templates/portfolios/task_orders/show.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
You're looking at TO {{ task_order.id }}
|
||||
|
||||
{% endblock %}
|
@ -50,9 +50,9 @@
|
||||
{% if not requests %}
|
||||
|
||||
{{ EmptyState(
|
||||
("requests.index.no_workspaces_label" | translate),
|
||||
sub_message=("requests.index.no_workspaces_sub_message" | translate),
|
||||
action_label=("requests.index.no_workspaces_action_label" | translate),
|
||||
("requests.index.no_portfolios_label" | translate),
|
||||
sub_message=("requests.index.no_portfolios_sub_message" | translate),
|
||||
action_label=("requests.index.no_portfolios_action_label" | translate),
|
||||
action_href=url_for('requests.requests_form_new', screen=1),
|
||||
icon='document'
|
||||
) }}
|
||||
@ -146,7 +146,7 @@
|
||||
{% endif %}
|
||||
<td>!{ dollars(r.annual_usage) }</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 }
|
||||
</a>
|
||||
<span v-else>
|
||||
|
@ -135,7 +135,7 @@
|
||||
)
|
||||
}}
|
||||
{{ 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>
|
||||
</details-of-use>
|
||||
|
@ -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 %}
|
@ -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 %}
|
@ -1,35 +1,35 @@
|
||||
from atst.domain.applications import Applications
|
||||
from tests.factories import RequestFactory, UserFactory, WorkspaceFactory
|
||||
from atst.domain.workspaces import Workspaces
|
||||
from tests.factories import RequestFactory, UserFactory, PortfolioFactory
|
||||
from atst.domain.portfolios import Portfolios
|
||||
|
||||
|
||||
def test_create_application_with_multiple_environments():
|
||||
request = RequestFactory.create()
|
||||
workspace = Workspaces.create_from_request(request)
|
||||
portfolio = Portfolios.create_from_request(request)
|
||||
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.description == "Test"
|
||||
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()
|
||||
workspace = WorkspaceFactory.create(
|
||||
portfolio = PortfolioFactory.create(
|
||||
owner=owner,
|
||||
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
|
||||
|
||||
|
||||
def test_can_only_update_name_and_description():
|
||||
owner = UserFactory.create()
|
||||
workspace = WorkspaceFactory.create(
|
||||
portfolio = PortfolioFactory.create(
|
||||
owner=owner,
|
||||
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
|
||||
Applications.update(
|
||||
owner,
|
||||
workspace,
|
||||
portfolio,
|
||||
application,
|
||||
{
|
||||
"name": "New Name",
|
||||
|
@ -3,11 +3,11 @@ import pytest
|
||||
from atst.domain.audit_log import AuditLog
|
||||
from atst.domain.exceptions import UnauthorizedError
|
||||
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 (
|
||||
UserFactory,
|
||||
WorkspaceFactory,
|
||||
WorkspaceRoleFactory,
|
||||
PortfolioFactory,
|
||||
PortfolioRoleFactory,
|
||||
ApplicationFactory,
|
||||
)
|
||||
|
||||
@ -42,69 +42,69 @@ def test_paginate_audit_log(ccpo):
|
||||
|
||||
|
||||
def test_ccpo_can_view_ws_audit_log(ccpo):
|
||||
workspace = WorkspaceFactory.create()
|
||||
events = AuditLog.get_workspace_events(ccpo, workspace)
|
||||
portfolio = PortfolioFactory.create()
|
||||
events = AuditLog.get_portfolio_events(ccpo, portfolio)
|
||||
assert len(events) > 0
|
||||
|
||||
|
||||
def test_ws_admin_can_view_ws_audit_log():
|
||||
workspace = WorkspaceFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
admin = UserFactory.create()
|
||||
WorkspaceRoleFactory.create(
|
||||
workspace=workspace,
|
||||
PortfolioRoleFactory.create(
|
||||
portfolio=portfolio,
|
||||
user=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
|
||||
|
||||
|
||||
def test_ws_owner_can_view_ws_audit_log():
|
||||
workspace = WorkspaceFactory.create()
|
||||
events = AuditLog.get_workspace_events(workspace.owner, workspace)
|
||||
portfolio = PortfolioFactory.create()
|
||||
events = AuditLog.get_portfolio_events(portfolio.owner, portfolio)
|
||||
assert len(events) > 0
|
||||
|
||||
|
||||
def test_other_users_cannot_view_ws_audit_log():
|
||||
with pytest.raises(UnauthorizedError):
|
||||
workspace = WorkspaceFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
dev = UserFactory.create()
|
||||
WorkspaceRoleFactory.create(
|
||||
workspace=workspace,
|
||||
PortfolioRoleFactory.create(
|
||||
portfolio=portfolio,
|
||||
user=dev,
|
||||
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():
|
||||
workspace = WorkspaceFactory.create()
|
||||
application = ApplicationFactory.create(workspace=workspace)
|
||||
portfolio = PortfolioFactory.create()
|
||||
application = ApplicationFactory.create(portfolio=portfolio)
|
||||
for _ in range(100):
|
||||
AuditLog.log_system_event(
|
||||
resource=application, action="create", workspace=workspace
|
||||
resource=application, action="create", portfolio=portfolio
|
||||
)
|
||||
|
||||
events = AuditLog.get_workspace_events(
|
||||
workspace.owner, workspace, pagination_opts={"per_page": 25, "page": 2}
|
||||
events = AuditLog.get_portfolio_events(
|
||||
portfolio.owner, portfolio, pagination_opts={"per_page": 25, "page": 2}
|
||||
)
|
||||
assert len(events) == 25
|
||||
|
||||
|
||||
def test_ws_audit_log_only_includes_current_ws_events():
|
||||
owner = UserFactory.create()
|
||||
workspace = WorkspaceFactory.create(owner=owner)
|
||||
other_workspace = WorkspaceFactory.create(owner=owner)
|
||||
portfolio = PortfolioFactory.create(owner=owner)
|
||||
other_portfolio = PortfolioFactory.create(owner=owner)
|
||||
# Add some audit events
|
||||
application_1 = ApplicationFactory.create(workspace=workspace)
|
||||
application_2 = ApplicationFactory.create(workspace=other_workspace)
|
||||
application_1 = ApplicationFactory.create(portfolio=portfolio)
|
||||
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:
|
||||
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 (
|
||||
not event.workspace_id == other_workspace.id
|
||||
or event.resource_id == other_workspace.id
|
||||
not event.portfolio_id == other_portfolio.id
|
||||
or event.resource_id == other_portfolio.id
|
||||
)
|
||||
|
@ -1,8 +1,8 @@
|
||||
from atst.domain.environments import Environments
|
||||
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():
|
||||
@ -16,7 +16,7 @@ def test_create_environment_role_creates_cloud_id(session):
|
||||
owner = UserFactory.create()
|
||||
developer = UserFactory.from_atat_role("developer")
|
||||
|
||||
workspace = WorkspaceFactory.create(
|
||||
portfolio = PortfolioFactory.create(
|
||||
owner=owner,
|
||||
members=[{"user": developer, "role_name": "developer"}],
|
||||
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"}]
|
||||
|
||||
workspace_role = workspace.members[0]
|
||||
assert not workspace_role.user.cloud_id
|
||||
portfolio_role = portfolio.members[0]
|
||||
assert not portfolio_role.user.cloud_id
|
||||
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():
|
||||
owner = UserFactory.create()
|
||||
developer = UserFactory.from_atat_role("developer")
|
||||
|
||||
workspace = WorkspaceFactory.create(
|
||||
portfolio = PortfolioFactory.create(
|
||||
owner=owner,
|
||||
members=[{"user": developer, "role_name": "developer"}],
|
||||
applications=[
|
||||
@ -61,19 +61,19 @@ def test_update_environment_roles():
|
||||
],
|
||||
)
|
||||
|
||||
dev_env = workspace.applications[0].environments[0]
|
||||
staging_env = workspace.applications[0].environments[1]
|
||||
dev_env = portfolio.applications[0].environments[0]
|
||||
staging_env = portfolio.applications[0].environments[1]
|
||||
new_ids_and_roles = [
|
||||
{"id": dev_env.id, "role": "billing_admin"},
|
||||
{"id": staging_env.id, "role": "developer"},
|
||||
]
|
||||
|
||||
workspace_role = workspace.members[0]
|
||||
portfolio_role = portfolio.members[0]
|
||||
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)
|
||||
staging_env_role = EnvironmentRoles.get(workspace_role.user.id, staging_env.id)
|
||||
new_dev_env_role = EnvironmentRoles.get(portfolio_role.user.id, dev_env.id)
|
||||
staging_env_role = EnvironmentRoles.get(portfolio_role.user.id, staging_env.id)
|
||||
|
||||
assert new_dev_env_role.role == "billing_admin"
|
||||
assert staging_env_role.role == "developer"
|
||||
@ -82,7 +82,7 @@ def test_update_environment_roles():
|
||||
def test_remove_environment_role():
|
||||
owner = UserFactory.create()
|
||||
developer = UserFactory.from_atat_role("developer")
|
||||
workspace = WorkspaceFactory.create(
|
||||
portfolio = PortfolioFactory.create(
|
||||
owner=owner,
|
||||
members=[{"user": developer, "role_name": "developer"}],
|
||||
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_none = application.environments[1].id
|
||||
still_fa = application.environments[2].id
|
||||
@ -119,12 +119,12 @@ def test_remove_environment_role():
|
||||
{"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(
|
||||
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_none) is None
|
||||
assert EnvironmentRoles.get(developer.id, still_fa).role == "financial_auditor"
|
||||
@ -134,7 +134,7 @@ def test_no_update_to_environment_roles():
|
||||
owner = UserFactory.create()
|
||||
developer = UserFactory.from_atat_role("developer")
|
||||
|
||||
workspace = WorkspaceFactory.create(
|
||||
portfolio = PortfolioFactory.create(
|
||||
owner=owner,
|
||||
members=[{"user": developer, "role_name": "developer"}],
|
||||
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"}]
|
||||
|
||||
workspace_role = WorkspaceRoles.get(workspace.id, developer.id)
|
||||
portfolio_role = PortfolioRoles.get(portfolio.id, developer.id)
|
||||
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):
|
||||
developer = UserFactory.create()
|
||||
workspace = WorkspaceFactory.create(
|
||||
portfolio = PortfolioFactory.create(
|
||||
members=[{"user": developer, "role_name": "developer"}],
|
||||
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"]
|
||||
|
||||
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"]
|
||||
|
@ -11,8 +11,8 @@ from atst.domain.invitations import (
|
||||
from atst.models.invitation import Status
|
||||
|
||||
from tests.factories import (
|
||||
WorkspaceFactory,
|
||||
WorkspaceRoleFactory,
|
||||
PortfolioFactory,
|
||||
PortfolioRoleFactory,
|
||||
UserFactory,
|
||||
InvitationFactory,
|
||||
)
|
||||
@ -21,22 +21,22 @@ from atst.domain.audit_log import AuditLog
|
||||
|
||||
|
||||
def test_create_invitation():
|
||||
workspace = WorkspaceFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
invite = Invitations.create(workspace.owner, ws_role, user.email)
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
||||
assert invite.user == user
|
||||
assert invite.workspace_role == ws_role
|
||||
assert invite.inviter == workspace.owner
|
||||
assert invite.portfolio_role == ws_role
|
||||
assert invite.inviter == portfolio.owner
|
||||
assert invite.status == Status.PENDING
|
||||
assert re.match(r"^[\w\-_]+$", invite.token)
|
||||
|
||||
|
||||
def test_accept_invitation():
|
||||
workspace = WorkspaceFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
invite = Invitations.create(workspace.owner, ws_role, user.email)
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
||||
assert invite.is_pending
|
||||
accepted_invite = Invitations.accept(user, invite.token)
|
||||
assert accepted_invite.is_accepted
|
||||
@ -44,15 +44,15 @@ def test_accept_invitation():
|
||||
|
||||
def test_accept_expired_invitation():
|
||||
user = UserFactory.create()
|
||||
workspace = WorkspaceFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
portfolio = PortfolioFactory.create()
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
increment = Invitations.EXPIRATION_LIMIT_MINUTES + 1
|
||||
expiration_time = datetime.datetime.now() - datetime.timedelta(minutes=increment)
|
||||
invite = InvitationFactory.create(
|
||||
user=user,
|
||||
expiration_time=expiration_time,
|
||||
status=Status.PENDING,
|
||||
workspace_role=ws_role,
|
||||
portfolio_role=ws_role,
|
||||
)
|
||||
with pytest.raises(ExpiredError):
|
||||
Invitations.accept(user, invite.token)
|
||||
@ -62,10 +62,10 @@ def test_accept_expired_invitation():
|
||||
|
||||
def test_accept_rejected_invite():
|
||||
user = UserFactory.create()
|
||||
workspace = WorkspaceFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
portfolio = PortfolioFactory.create()
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
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):
|
||||
Invitations.accept(user, invite.token)
|
||||
@ -73,10 +73,10 @@ def test_accept_rejected_invite():
|
||||
|
||||
def test_accept_revoked_invite():
|
||||
user = UserFactory.create()
|
||||
workspace = WorkspaceFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
portfolio = PortfolioFactory.create()
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
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):
|
||||
Invitations.accept(user, invite.token)
|
||||
@ -84,20 +84,20 @@ def test_accept_revoked_invite():
|
||||
|
||||
def test_wrong_user_accepts_invitation():
|
||||
user = UserFactory.create()
|
||||
workspace = WorkspaceFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
portfolio = PortfolioFactory.create()
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
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):
|
||||
Invitations.accept(wrong_user, invite.token)
|
||||
|
||||
|
||||
def test_user_cannot_accept_invitation_accepted_by_wrong_user():
|
||||
user = UserFactory.create()
|
||||
workspace = WorkspaceFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
portfolio = PortfolioFactory.create()
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
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):
|
||||
Invitations.accept(wrong_user, invite.token)
|
||||
with pytest.raises(InvitationError):
|
||||
@ -105,40 +105,40 @@ def test_user_cannot_accept_invitation_accepted_by_wrong_user():
|
||||
|
||||
|
||||
def test_accept_invitation_twice():
|
||||
workspace = WorkspaceFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
invite = Invitations.create(workspace.owner, ws_role, user.email)
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
||||
Invitations.accept(user, invite.token)
|
||||
with pytest.raises(InvitationError):
|
||||
Invitations.accept(user, invite.token)
|
||||
|
||||
|
||||
def test_revoke_invitation():
|
||||
workspace = WorkspaceFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
invite = Invitations.create(workspace.owner, ws_role, user.email)
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
||||
assert invite.is_pending
|
||||
Invitations.revoke(invite.token)
|
||||
assert invite.is_revoked
|
||||
|
||||
|
||||
def test_resend_invitation():
|
||||
workspace = WorkspaceFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
invite = Invitations.create(workspace.owner, ws_role, user.email)
|
||||
Invitations.resend(workspace.owner, workspace.id, invite.token)
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
||||
Invitations.resend(portfolio.owner, portfolio.id, invite.token)
|
||||
assert ws_role.invitations[0].is_revoked
|
||||
assert ws_role.invitations[1].is_pending
|
||||
|
||||
|
||||
def test_audit_event_for_accepted_invite():
|
||||
workspace = WorkspaceFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = WorkspaceRoleFactory.create(user=user, workspace=workspace)
|
||||
invite = Invitations.create(workspace.owner, ws_role, user.email)
|
||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
||||
invite = Invitations.accept(user, invite.token)
|
||||
|
||||
accepted_event = AuditLog.get_by_resource(invite.id)[0]
|
||||
|
65
tests/domain/test_portfolio_roles.py
Normal file
65
tests/domain/test_portfolio_roles.py
Normal 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
|
||||
)
|
358
tests/domain/test_portfolios.py
Normal file
358
tests/domain/test_portfolios.py
Normal 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
|
@ -1,19 +1,19 @@
|
||||
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"]
|
||||
|
||||
|
||||
def test_workspace_totals():
|
||||
def test_portfolio_totals():
|
||||
legacy_task_order = LegacyTaskOrderFactory.create()
|
||||
|
||||
for num in CLIN_NUMS:
|
||||
setattr(legacy_task_order, "clin_{}".format(num), 200)
|
||||
|
||||
request = RequestFactory.create(legacy_task_order=legacy_task_order)
|
||||
workspace = WorkspaceFactory.create(request=request)
|
||||
report = Reports.workspace_totals(workspace)
|
||||
portfolio = PortfolioFactory.create(request=request)
|
||||
report = Reports.portfolio_totals(portfolio)
|
||||
total = 200 * len(CLIN_NUMS)
|
||||
assert report == {"budget": total, "spent": 0}
|
||||
|
||||
@ -21,18 +21,18 @@ def test_workspace_totals():
|
||||
# this is sketched in until we do real reporting
|
||||
def test_monthly_totals():
|
||||
request = RequestFactory.create()
|
||||
workspace = WorkspaceFactory.create(request=request)
|
||||
monthly = Reports.monthly_totals(workspace)
|
||||
portfolio = PortfolioFactory.create(request=request)
|
||||
monthly = Reports.monthly_totals(portfolio)
|
||||
|
||||
assert not monthly["environments"]
|
||||
assert not monthly["applications"]
|
||||
assert not monthly["workspace"]
|
||||
assert not monthly["portfolio"]
|
||||
|
||||
|
||||
# this is sketched in until we do real reporting
|
||||
def test_cumulative_budget():
|
||||
request = RequestFactory.create()
|
||||
workspace = WorkspaceFactory.create(request=request)
|
||||
months = Reports.cumulative_budget(workspace)
|
||||
portfolio = PortfolioFactory.create(request=request)
|
||||
months = Reports.cumulative_budget(portfolio)
|
||||
|
||||
assert len(months["months"]) == 12
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user