workspace -> portfolio everywhere
This commit is contained in:
@@ -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.
|
||||
|
Reference in New Issue
Block a user