Merge pull request #536 from dod-ccpo/rename-stuff-#163022978
Rename stuff #163022978
This commit is contained in:
commit
6efd304075
@ -125,8 +125,8 @@ that will automatically log you in as Amanda.
|
||||
|
||||
### Seeding the database
|
||||
|
||||
We have a helper script that will seed the database with requests, workspaces and
|
||||
projects for all of the test users:
|
||||
We have a helper script that will seed the database with requests, portfolios and
|
||||
applications for all of the test users:
|
||||
|
||||
`pipenv run python script/seed_sample.py`
|
||||
|
||||
|
@ -0,0 +1,28 @@
|
||||
"""change workspace and project tables
|
||||
|
||||
Revision ID: a6837632686c
|
||||
Revises: acd0c11be93a
|
||||
Create Date: 2019-01-11 10:36:55.030308
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a6837632686c'
|
||||
down_revision = 'acd0c11be93a'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.rename_table("workspaces", "portfolios")
|
||||
op.rename_table("projects", "applications")
|
||||
op.rename_table("workspace_roles", "portfolio_roles")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.rename_table("portfolios", "workspaces")
|
||||
op.rename_table("applications", "projects")
|
||||
op.rename_table("portfolio_roles", "workspace_roles")
|
@ -0,0 +1,34 @@
|
||||
"""change workspace and project
|
||||
|
||||
Revision ID: acd0c11be93a
|
||||
Revises: 7f2040715b0d
|
||||
Create Date: 2019-01-11 10:01:07.667126
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'acd0c11be93a'
|
||||
down_revision = '7f2040715b0d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column('audit_events', "workspace_id", new_column_name="portfolio_id")
|
||||
op.alter_column('environments', "project_id", new_column_name="application_id")
|
||||
op.alter_column('projects', "workspace_id", new_column_name="portfolio_id")
|
||||
op.alter_column('task_orders', "workspace_id", new_column_name="portfolio_id")
|
||||
op.alter_column('workspace_roles', "workspace_id", new_column_name="portfolio_id")
|
||||
op.alter_column('invitations', "workspace_role_id", new_column_name="portfolio_role_id")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.alter_column('audit_events', "portfolio_id", new_column_name="workspace_id")
|
||||
op.alter_column('environments', "application_id", new_column_name="project_id")
|
||||
op.alter_column('projects', "portfolio_id", new_column_name="workspace_id")
|
||||
op.alter_column('task_orders', "portfolio_id", new_column_name="workspace_id")
|
||||
op.alter_column('workspace_roles', "portfolio_id", new_column_name="workspace_id")
|
||||
op.alter_column('invitations', "portfolio_role_id", new_column_name="workspace_role_id")
|
@ -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)
|
||||
|
82
atst/domain/applications.py
Normal file
82
atst/domain/applications.py
Normal file
@ -0,0 +1,82 @@
|
||||
from atst.database import db
|
||||
from atst.domain.authz import Authorization
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
from atst.models.permissions import Permissions
|
||||
from atst.models.application import Application
|
||||
from atst.models.environment import Environment
|
||||
from atst.models.environment_role import EnvironmentRole
|
||||
|
||||
|
||||
class Applications(object):
|
||||
@classmethod
|
||||
def create(cls, user, portfolio, name, description, environment_names):
|
||||
application = Application(
|
||||
portfolio=portfolio, name=name, description=description
|
||||
)
|
||||
db.session.add(application)
|
||||
|
||||
Environments.create_many(application, environment_names)
|
||||
|
||||
db.session.commit()
|
||||
return application
|
||||
|
||||
@classmethod
|
||||
def get(cls, user, portfolio, application_id):
|
||||
# TODO: this should check permission for this particular application
|
||||
Authorization.check_portfolio_permission(
|
||||
user,
|
||||
portfolio,
|
||||
Permissions.VIEW_APPLICATION_IN_PORTFOLIO,
|
||||
"view application in portfolio",
|
||||
)
|
||||
|
||||
try:
|
||||
application = (
|
||||
db.session.query(Application).filter_by(id=application_id).one()
|
||||
)
|
||||
except NoResultFound:
|
||||
raise NotFoundError("application")
|
||||
|
||||
return application
|
||||
|
||||
@classmethod
|
||||
def for_user(self, user, portfolio):
|
||||
return (
|
||||
db.session.query(Application)
|
||||
.join(Environment)
|
||||
.join(EnvironmentRole)
|
||||
.filter(Application.portfolio_id == portfolio.id)
|
||||
.filter(EnvironmentRole.user_id == user.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, user, portfolio_role, portfolio):
|
||||
Authorization.check_portfolio_permission(
|
||||
user,
|
||||
portfolio,
|
||||
Permissions.VIEW_APPLICATION_IN_PORTFOLIO,
|
||||
"view application in portfolio",
|
||||
)
|
||||
|
||||
try:
|
||||
applications = (
|
||||
db.session.query(Application).filter_by(portfolio_id=portfolio.id).all()
|
||||
)
|
||||
except NoResultFound:
|
||||
raise NotFoundError("applications")
|
||||
|
||||
return applications
|
||||
|
||||
@classmethod
|
||||
def update(cls, user, portfolio, application, new_data):
|
||||
if "name" in new_data:
|
||||
application.name = new_data["name"]
|
||||
if "description" in new_data:
|
||||
application.description = new_data["description"]
|
||||
|
||||
db.session.add(application)
|
||||
db.session.commit()
|
||||
|
||||
return application
|
@ -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,
|
||||
Permissions.VIEW_WORKSPACE_AUDIT_LOG,
|
||||
"view workspace audit log",
|
||||
portfolio,
|
||||
Permissions.VIEW_PORTFOLIO_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
|
||||
|
@ -22,12 +22,12 @@ class MockEnvironment:
|
||||
self.name = env_name
|
||||
|
||||
|
||||
class MockProject:
|
||||
def __init__(self, project_name, envs):
|
||||
class MockApplication:
|
||||
def __init__(self, application_name, envs):
|
||||
def make_env(name):
|
||||
return MockEnvironment("{}_{}".format(project_name, name), name)
|
||||
return MockEnvironment("{}_{}".format(application_name, name), name)
|
||||
|
||||
self.name = project_name
|
||||
self.name = application_name
|
||||
self.environments = [make_env(env_name) for env_name in envs]
|
||||
|
||||
|
||||
@ -161,13 +161,13 @@ class MockReportingProvider(ReportingInterface):
|
||||
REPORT_FIXTURE_MAP = {
|
||||
"Aardvark": {
|
||||
"cumulative": CUMULATIVE_BUDGET_AARDVARK,
|
||||
"projects": [
|
||||
MockProject("LC04", ["Integ", "PreProd", "Prod"]),
|
||||
MockProject("SF18", ["Integ", "PreProd", "Prod"]),
|
||||
MockProject("Canton", ["Prod"]),
|
||||
MockProject("BD04", ["Integ", "PreProd"]),
|
||||
MockProject("SCV18", ["Dev"]),
|
||||
MockProject(
|
||||
"applications": [
|
||||
MockApplication("LC04", ["Integ", "PreProd", "Prod"]),
|
||||
MockApplication("SF18", ["Integ", "PreProd", "Prod"]),
|
||||
MockApplication("Canton", ["Prod"]),
|
||||
MockApplication("BD04", ["Integ", "PreProd"]),
|
||||
MockApplication("SCV18", ["Dev"]),
|
||||
MockApplication(
|
||||
"Crown",
|
||||
[
|
||||
"CR Portal Dev",
|
||||
@ -182,9 +182,9 @@ class MockReportingProvider(ReportingInterface):
|
||||
},
|
||||
"Beluga": {
|
||||
"cumulative": CUMULATIVE_BUDGET_BELUGA,
|
||||
"projects": [
|
||||
MockProject("NP02", ["Integ", "PreProd", "NP02_Prod"]),
|
||||
MockProject("FM", ["Integ", "Prod"]),
|
||||
"applications": [
|
||||
MockApplication("NP02", ["Integ", "PreProd", "NP02_Prod"]),
|
||||
MockApplication("FM", ["Integ", "Prod"]),
|
||||
],
|
||||
"budget": 70000,
|
||||
},
|
||||
@ -194,53 +194,53 @@ class MockReportingProvider(ReportingInterface):
|
||||
return sum(
|
||||
[
|
||||
spend
|
||||
for project in data
|
||||
for env in project.environments
|
||||
for application in data
|
||||
for env in application.environments
|
||||
for spend in self.MONTHLY_SPEND_BY_ENVIRONMENT[env.id].values()
|
||||
]
|
||||
)
|
||||
|
||||
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]["projects"]
|
||||
self.REPORT_FIXTURE_MAP[portfolio.name]["applications"]
|
||||
)
|
||||
return 0
|
||||
|
||||
def _rollup_project_totals(self, data):
|
||||
project_totals = {}
|
||||
for project, environments in data.items():
|
||||
project_spend = [
|
||||
def _rollup_application_totals(self, data):
|
||||
application_totals = {}
|
||||
for application, environments in data.items():
|
||||
application_spend = [
|
||||
(month, spend)
|
||||
for env in environments.values()
|
||||
if env
|
||||
for month, spend in env.items()
|
||||
]
|
||||
project_totals[project] = {
|
||||
application_totals[application] = {
|
||||
month: sum([spend[1] for spend in spends])
|
||||
for month, spends in groupby(sorted(project_spend), lambda x: x[0])
|
||||
for month, spends in groupby(sorted(application_spend), lambda x: x[0])
|
||||
}
|
||||
|
||||
return project_totals
|
||||
return application_totals
|
||||
|
||||
def _rollup_workspace_totals(self, project_totals):
|
||||
def _rollup_portfolio_totals(self, application_totals):
|
||||
monthly_spend = [
|
||||
(month, spend)
|
||||
for project in project_totals.values()
|
||||
for month, spend in project.items()
|
||||
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,46 +253,46 @@ class MockReportingProvider(ReportingInterface):
|
||||
"""
|
||||
return self.MONTHLY_SPEND_BY_ENVIRONMENT.get(environment_id, {})
|
||||
|
||||
def monthly_totals(self, workspace):
|
||||
"""Return month totals rolled up by environment, project, 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", "projects",
|
||||
Data should returned with three top level keys, "portfolio", "applications",
|
||||
and "environments".
|
||||
The "projects" key will have budget data per month for each project,
|
||||
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 } } },
|
||||
"projects": { "X-Wing": { "01/2018": 75.42 } },
|
||||
"workspace": { "01/2018": 75.42 },
|
||||
"applications": { "X-Wing": { "01/2018": 75.42 } },
|
||||
"portfolio": { "01/2018": 75.42 },
|
||||
}
|
||||
|
||||
"""
|
||||
projects = workspace.projects
|
||||
if workspace.name in self.REPORT_FIXTURE_MAP:
|
||||
projects = self.REPORT_FIXTURE_MAP[workspace.name]["projects"]
|
||||
applications = portfolio.applications
|
||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
||||
applications = self.REPORT_FIXTURE_MAP[portfolio.name]["applications"]
|
||||
environments = {
|
||||
project.name: {
|
||||
application.name: {
|
||||
env.name: self.monthly_totals_for_environment(env.id)
|
||||
for env in project.environments
|
||||
for env in application.environments
|
||||
}
|
||||
for project in projects
|
||||
for application in applications
|
||||
}
|
||||
|
||||
project_totals = self._rollup_project_totals(environments)
|
||||
workspace_totals = self._rollup_workspace_totals(project_totals)
|
||||
application_totals = self._rollup_application_totals(environments)
|
||||
portfolio_totals = self._rollup_portfolio_totals(application_totals)
|
||||
|
||||
return {
|
||||
"environments": environments,
|
||||
"projects": project_totals,
|
||||
"workspace": workspace_totals,
|
||||
"applications": application_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 = {}
|
||||
|
||||
|
@ -4,7 +4,7 @@ from sqlalchemy.orm.exc import NoResultFound
|
||||
from atst.database import db
|
||||
from atst.models.environment import Environment
|
||||
from atst.models.environment_role import EnvironmentRole
|
||||
from atst.models.project import Project
|
||||
from atst.models.application import Application
|
||||
from atst.models.permissions import Permissions
|
||||
from atst.domain.authz import Authorization
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
@ -14,18 +14,18 @@ from .exceptions import NotFoundError
|
||||
|
||||
class Environments(object):
|
||||
@classmethod
|
||||
def create(cls, project, name):
|
||||
environment = Environment(project=project, name=name)
|
||||
def create(cls, application, name):
|
||||
environment = Environment(application=application, name=name)
|
||||
environment.cloud_id = app.csp.cloud.create_application(environment.name)
|
||||
db.session.add(environment)
|
||||
db.session.commit()
|
||||
return environment
|
||||
|
||||
@classmethod
|
||||
def create_many(cls, project, names):
|
||||
def create_many(cls, application, names):
|
||||
environments = []
|
||||
for name in names:
|
||||
environment = Environments.create(project, name)
|
||||
environment = Environments.create(application, name)
|
||||
environments.append(environment)
|
||||
|
||||
db.session.add_all(environments)
|
||||
@ -40,13 +40,13 @@ class Environments(object):
|
||||
return environment
|
||||
|
||||
@classmethod
|
||||
def for_user(cls, user, project):
|
||||
def for_user(cls, user, application):
|
||||
return (
|
||||
db.session.query(Environment)
|
||||
.join(EnvironmentRole)
|
||||
.join(Project)
|
||||
.join(Application)
|
||||
.filter(EnvironmentRole.user_id == user.id)
|
||||
.filter(Environment.project_id == project.id)
|
||||
.filter(Environment.application_id == application.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
@ -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.portfolio_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_PORTFOLIO, "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_PORTFOLIO, "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_PORTFOLIO_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_PORTFOLIO_MEMBERS,
|
||||
"view portfolio members",
|
||||
)
|
||||
|
||||
return portfolio
|
||||
|
||||
@classmethod
|
||||
def for_user(cls, user):
|
||||
if Authorization.has_atat_permission(user, Permissions.VIEW_PORTFOLIO):
|
||||
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)
|
@ -1,6 +1,6 @@
|
||||
from atst.domain.authz import Authorization
|
||||
from atst.models.permissions import Permissions
|
||||
from atst.domain.projects import Projects
|
||||
from atst.domain.applications import Applications
|
||||
from atst.domain.environments import Environments
|
||||
|
||||
|
||||
@ -21,39 +21,41 @@ 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
|
||||
functionality that it only returns sub-resources (projects and environments)
|
||||
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 projects(self):
|
||||
can_view_all_projects = Authorization.has_workspace_permission(
|
||||
self.user, self.resource, Permissions.VIEW_APPLICATION_IN_WORKSPACE
|
||||
def applications(self):
|
||||
can_view_all_applications = Authorization.has_portfolio_permission(
|
||||
self.user, self.resource, Permissions.VIEW_APPLICATION_IN_PORTFOLIO
|
||||
)
|
||||
|
||||
if can_view_all_projects:
|
||||
projects = self.resource.projects
|
||||
if can_view_all_applications:
|
||||
applications = self.resource.applications
|
||||
else:
|
||||
projects = Projects.for_user(self.user, self.resource)
|
||||
applications = Applications.for_user(self.user, self.resource)
|
||||
|
||||
return [ScopedProject(self.user, project) for project in projects]
|
||||
return [
|
||||
ScopedApplication(self.user, application) for application in applications
|
||||
]
|
||||
|
||||
|
||||
class ScopedProject(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,
|
||||
)
|
||||
|
@ -1,78 +0,0 @@
|
||||
from atst.database import db
|
||||
from atst.domain.authz import Authorization
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
from atst.models.permissions import Permissions
|
||||
from atst.models.project import Project
|
||||
from atst.models.environment import Environment
|
||||
from atst.models.environment_role import EnvironmentRole
|
||||
|
||||
|
||||
class Projects(object):
|
||||
@classmethod
|
||||
def create(cls, user, workspace, name, description, environment_names):
|
||||
project = Project(workspace=workspace, name=name, description=description)
|
||||
db.session.add(project)
|
||||
|
||||
Environments.create_many(project, environment_names)
|
||||
|
||||
db.session.commit()
|
||||
return project
|
||||
|
||||
@classmethod
|
||||
def get(cls, user, workspace, project_id):
|
||||
# TODO: this should check permission for this particular project
|
||||
Authorization.check_workspace_permission(
|
||||
user,
|
||||
workspace,
|
||||
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
|
||||
"view project in workspace",
|
||||
)
|
||||
|
||||
try:
|
||||
project = db.session.query(Project).filter_by(id=project_id).one()
|
||||
except NoResultFound:
|
||||
raise NotFoundError("project")
|
||||
|
||||
return project
|
||||
|
||||
@classmethod
|
||||
def for_user(self, user, workspace):
|
||||
return (
|
||||
db.session.query(Project)
|
||||
.join(Environment)
|
||||
.join(EnvironmentRole)
|
||||
.filter(Project.workspace_id == workspace.id)
|
||||
.filter(EnvironmentRole.user_id == user.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, user, workspace_role, workspace):
|
||||
Authorization.check_workspace_permission(
|
||||
user,
|
||||
workspace,
|
||||
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
|
||||
"view project in workspace",
|
||||
)
|
||||
|
||||
try:
|
||||
projects = (
|
||||
db.session.query(Project).filter_by(workspace_id=workspace.id).all()
|
||||
)
|
||||
except NoResultFound:
|
||||
raise NotFoundError("projects")
|
||||
|
||||
return projects
|
||||
|
||||
@classmethod
|
||||
def update(cls, user, workspace, project, new_data):
|
||||
if "name" in new_data:
|
||||
project.name = new_data["name"]
|
||||
if "description" in new_data:
|
||||
project.description = new_data["description"]
|
||||
|
||||
db.session.add(project)
|
||||
db.session.commit()
|
||||
|
||||
return project
|
@ -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)
|
||||
|
@ -12,7 +12,7 @@ class RequestsAuthorization(object):
|
||||
def can_view(self):
|
||||
return (
|
||||
Authorization.has_atat_permission(
|
||||
self.user, Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST
|
||||
self.user, Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST
|
||||
)
|
||||
or self.request.creator == self.user
|
||||
)
|
||||
@ -24,6 +24,6 @@ class RequestsAuthorization(object):
|
||||
def check_can_approve(self):
|
||||
return Authorization.check_atat_permission(
|
||||
self.user,
|
||||
Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST,
|
||||
Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST,
|
||||
"cannot review and approve requests",
|
||||
)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -12,7 +12,7 @@ ATAT_ROLES = [
|
||||
"description": "",
|
||||
"permissions": [
|
||||
Permissions.VIEW_ORIGINAL_JEDI_REQEUST,
|
||||
Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST,
|
||||
Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST,
|
||||
Permissions.MODIFY_ATAT_ROLE_PERMISSIONS,
|
||||
Permissions.CREATE_CSP_ROLE,
|
||||
Permissions.DELETE_CSP_ROLE,
|
||||
@ -26,41 +26,41 @@ ATAT_ROLES = [
|
||||
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
|
||||
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
|
||||
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
|
||||
Permissions.DEACTIVATE_WORKSPACE,
|
||||
Permissions.DEACTIVATE_PORTFOLIO,
|
||||
Permissions.VIEW_ATAT_PERMISSIONS,
|
||||
Permissions.TRANSFER_OWNERSHIP_OF_WORKSPACE,
|
||||
Permissions.VIEW_WORKSPACE,
|
||||
Permissions.VIEW_WORKSPACE_MEMBERS,
|
||||
Permissions.ADD_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.DELETE_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.DEACTIVATE_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.RENAME_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.TRANSFER_OWNERSHIP_OF_PORTFOLIO,
|
||||
Permissions.VIEW_PORTFOLIO,
|
||||
Permissions.VIEW_PORTFOLIO_MEMBERS,
|
||||
Permissions.ADD_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.DELETE_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.DEACTIVATE_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.VIEW_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.RENAME_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.ADD_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.DELETE_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.DEACTIVATE_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.RENAME_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.ADD_TAG_TO_WORKSPACE,
|
||||
Permissions.REMOVE_TAG_FROM_WORKSPACE,
|
||||
Permissions.ADD_TAG_TO_PORTFOLIO,
|
||||
Permissions.REMOVE_TAG_FROM_PORTFOLIO,
|
||||
Permissions.VIEW_AUDIT_LOG,
|
||||
Permissions.VIEW_WORKSPACE_AUDIT_LOG,
|
||||
Permissions.VIEW_PORTFOLIO_AUDIT_LOG,
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "default",
|
||||
"display_name": "Default",
|
||||
"description": "",
|
||||
"permissions": [Permissions.REQUEST_JEDI_WORKSPACE],
|
||||
"permissions": [Permissions.REQUEST_JEDI_PORTFOLIO],
|
||||
},
|
||||
]
|
||||
WORKSPACE_ROLES = [
|
||||
PORTFOLIO_ROLES = [
|
||||
{
|
||||
"name": "owner",
|
||||
"display_name": "Workspace Owner",
|
||||
"description": "Adds, edits, deactivates access to all projects, environments, and members. Views budget reports. Initiates and edits JEDI Cloud requests.",
|
||||
"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,
|
||||
Permissions.REQUEST_JEDI_PORTFOLIO,
|
||||
Permissions.VIEW_ORIGINAL_JEDI_REQEUST,
|
||||
Permissions.VIEW_USAGE_REPORT,
|
||||
Permissions.VIEW_USAGE_DOLLARS,
|
||||
@ -70,22 +70,22 @@ WORKSPACE_ROLES = [
|
||||
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
|
||||
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
|
||||
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
|
||||
Permissions.DEACTIVATE_WORKSPACE,
|
||||
Permissions.DEACTIVATE_PORTFOLIO,
|
||||
Permissions.VIEW_ATAT_PERMISSIONS,
|
||||
Permissions.VIEW_WORKSPACE,
|
||||
Permissions.VIEW_WORKSPACE_MEMBERS,
|
||||
Permissions.EDIT_WORKSPACE_INFORMATION,
|
||||
Permissions.ADD_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.DELETE_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.DEACTIVATE_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.RENAME_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.VIEW_PORTFOLIO,
|
||||
Permissions.VIEW_PORTFOLIO_MEMBERS,
|
||||
Permissions.EDIT_PORTFOLIO_INFORMATION,
|
||||
Permissions.ADD_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.DELETE_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.DEACTIVATE_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.VIEW_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.RENAME_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.ADD_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.DELETE_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.DEACTIVATE_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.RENAME_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.VIEW_WORKSPACE_AUDIT_LOG,
|
||||
Permissions.VIEW_PORTFOLIO_AUDIT_LOG,
|
||||
Permissions.VIEW_TASK_ORDER,
|
||||
Permissions.UPDATE_TASK_ORDER,
|
||||
Permissions.ADD_TASK_ORDER_OFFICER,
|
||||
@ -94,7 +94,7 @@ WORKSPACE_ROLES = [
|
||||
{
|
||||
"name": "admin",
|
||||
"display_name": "Administrator",
|
||||
"description": "Adds and edits projects, environments, members, but cannot deactivate. Cannot view budget reports or JEDI Cloud requests.",
|
||||
"description": "Adds and edits applications, environments, members, but cannot deactivate. Cannot view budget reports or JEDI Cloud requests.",
|
||||
"permissions": [
|
||||
Permissions.VIEW_USAGE_REPORT,
|
||||
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
|
||||
@ -103,20 +103,20 @@ WORKSPACE_ROLES = [
|
||||
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
|
||||
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
|
||||
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
|
||||
Permissions.VIEW_WORKSPACE,
|
||||
Permissions.VIEW_WORKSPACE_MEMBERS,
|
||||
Permissions.EDIT_WORKSPACE_INFORMATION,
|
||||
Permissions.ADD_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.DELETE_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.DEACTIVATE_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.RENAME_APPLICATION_IN_WORKSPACE,
|
||||
Permissions.VIEW_PORTFOLIO,
|
||||
Permissions.VIEW_PORTFOLIO_MEMBERS,
|
||||
Permissions.EDIT_PORTFOLIO_INFORMATION,
|
||||
Permissions.ADD_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.DELETE_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.DEACTIVATE_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.VIEW_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.RENAME_APPLICATION_IN_PORTFOLIO,
|
||||
Permissions.ADD_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.DELETE_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.DEACTIVATE_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.RENAME_ENVIRONMENT_IN_APPLICATION,
|
||||
Permissions.VIEW_WORKSPACE_AUDIT_LOG,
|
||||
Permissions.VIEW_PORTFOLIO_AUDIT_LOG,
|
||||
Permissions.VIEW_TASK_ORDER,
|
||||
Permissions.UPDATE_TASK_ORDER,
|
||||
Permissions.ADD_TASK_ORDER_OFFICER,
|
||||
@ -125,28 +125,28 @@ WORKSPACE_ROLES = [
|
||||
{
|
||||
"name": "developer",
|
||||
"display_name": "Developer",
|
||||
"description": "Views only the projects and environments they are granted access to. Can also view members associated with each environment.",
|
||||
"permissions": [Permissions.VIEW_USAGE_REPORT, Permissions.VIEW_WORKSPACE],
|
||||
"description": "Views only the applications and environments they are granted access to. Can also view members associated with each environment.",
|
||||
"permissions": [Permissions.VIEW_USAGE_REPORT, Permissions.VIEW_PORTFOLIO],
|
||||
},
|
||||
{
|
||||
"name": "billing_auditor",
|
||||
"display_name": "Billing Auditor",
|
||||
"description": "Views only the projects 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,
|
||||
Permissions.VIEW_WORKSPACE,
|
||||
Permissions.VIEW_PORTFOLIO,
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "security_auditor",
|
||||
"description": "Views only the projects and environments they are granted access to. Can also view activity logs.",
|
||||
"description": "Views only the applications and environments they are granted access to. Can also view activity logs.",
|
||||
"display_name": "Security Auditor",
|
||||
"permissions": [
|
||||
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
|
||||
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
|
||||
Permissions.VIEW_ATAT_PERMISSIONS,
|
||||
Permissions.VIEW_WORKSPACE,
|
||||
Permissions.VIEW_PORTFOLIO,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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_projects(cls, user, workspace_id):
|
||||
workspace = WorkspacesQuery.get(workspace_id)
|
||||
Authorization.check_workspace_permission(
|
||||
user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE, "add project"
|
||||
)
|
||||
|
||||
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
|
@ -5,29 +5,29 @@ from atst.forms.validators import ListItemRequired, ListItemsUnique
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
class ProjectForm(FlaskForm):
|
||||
class ApplicationForm(FlaskForm):
|
||||
name = StringField(
|
||||
label=translate("forms.project.name_label"), validators=[Required()]
|
||||
label=translate("forms.application.name_label"), validators=[Required()]
|
||||
)
|
||||
description = TextAreaField(
|
||||
label=translate("forms.project.description_label"), validators=[Required()]
|
||||
label=translate("forms.application.description_label"), validators=[Required()]
|
||||
)
|
||||
|
||||
|
||||
class NewProjectForm(ProjectForm):
|
||||
class NewApplicationForm(ApplicationForm):
|
||||
EMPTY_ENVIRONMENT_NAMES = ["", None]
|
||||
|
||||
environment_names = FieldList(
|
||||
StringField(label=translate("forms.project.environment_names_label")),
|
||||
StringField(label=translate("forms.application.environment_names_label")),
|
||||
validators=[
|
||||
ListItemRequired(
|
||||
message=translate(
|
||||
"forms.project.environment_names_required_validation_message"
|
||||
"forms.application.environment_names_required_validation_message"
|
||||
)
|
||||
),
|
||||
ListItemsUnique(
|
||||
message=translate(
|
||||
"forms.project.environment_names_unique_validation_message"
|
||||
"forms.application.environment_names_unique_validation_message"
|
||||
)
|
||||
),
|
||||
],
|
@ -1,4 +1,4 @@
|
||||
from atst.domain.roles import WORKSPACE_ROLES as WORKSPACE_ROLE_DEFINITIONS
|
||||
from atst.domain.roles import PORTFOLIO_ROLES as PORTFOLIO_ROLE_DEFINITIONS
|
||||
|
||||
SERVICE_BRANCHES = [
|
||||
("", "Select an option"),
|
||||
@ -6,8 +6,8 @@ SERVICE_BRANCHES = [
|
||||
("Army and Air Force Exchange Service", "Army and Air Force Exchange Service"),
|
||||
("Army, Department of the", "Army, Department of the"),
|
||||
(
|
||||
"Defense Advanced Research Projects Agency",
|
||||
"Defense Advanced Research Projects Agency",
|
||||
"Defense Advanced Research Applications Agency",
|
||||
"Defense Advanced Research Applications Agency",
|
||||
),
|
||||
("Defense Commissary Agency", "Defense Commissary Agency"),
|
||||
("Defense Contract Audit Agency", "Defense Contract Audit Agency"),
|
||||
@ -105,9 +105,9 @@ COMPLETION_DATE_RANGES = [
|
||||
("Above 12 months", "Above 12 months"),
|
||||
]
|
||||
|
||||
WORKSPACE_ROLES = [
|
||||
PORTFOLIO_ROLES = [
|
||||
(role["name"], {"name": role["display_name"], "description": role["description"]})
|
||||
for role in WORKSPACE_ROLE_DEFINITIONS
|
||||
for role in PORTFOLIO_ROLE_DEFINITIONS
|
||||
if role["name"] is not "officer"
|
||||
]
|
||||
|
||||
@ -137,7 +137,7 @@ ENVIRONMENT_ROLES = [
|
||||
"billing_administrator",
|
||||
{
|
||||
"name": "Billing Administrator",
|
||||
"description": "Views cloud resource usage, budget reports, and invoices; Tracks budgets, including spend reports, cost planning and projections, and sets limits based on cloud service usage.",
|
||||
"description": "Views cloud resource usage, budget reports, and invoices; Tracks budgets, including spend reports, cost planning and applicationions, and sets limits based on cloud service usage.",
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -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 projects. 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 = [
|
||||
@ -186,7 +186,7 @@ APP_MIGRATION = [
|
||||
("not_sure", "Not Sure"),
|
||||
]
|
||||
|
||||
PROJECT_COMPLEXITY = [
|
||||
APPLICATION_COMPLEXITY = [
|
||||
("storage", "Storage "),
|
||||
("data_analytics", "Data Analytics "),
|
||||
("conus", "CONUS Access "),
|
||||
@ -210,7 +210,7 @@ TEAM_EXPERIENCE = [
|
||||
("built_3", "Built or Migrated 3-5 applications"),
|
||||
(
|
||||
"built_many",
|
||||
"Built or migrated many applications, or consulted on several such projects",
|
||||
"Built or migrated many applications, or consulted on several such applications",
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -4,15 +4,15 @@ from wtforms.validators import Required
|
||||
from atst.forms.fields import SelectField
|
||||
from atst.utils.localization import translate
|
||||
|
||||
from .data import WORKSPACE_ROLES
|
||||
from .data import PORTFOLIO_ROLES
|
||||
|
||||
|
||||
class EditMemberForm(FlaskForm):
|
||||
# This form also accepts a field for each environment in each project
|
||||
# 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"),
|
||||
choices=WORKSPACE_ROLES,
|
||||
portfolio_role = SelectField(
|
||||
translate("forms.edit_member.portfolio_role_label"),
|
||||
choices=PORTFOLIO_ROLES,
|
||||
validators=[Required()],
|
||||
)
|
||||
|
@ -7,7 +7,7 @@ from atst.forms.validators import IsNumber
|
||||
from atst.forms.fields import SelectField
|
||||
from atst.utils.localization import translate
|
||||
|
||||
from .data import WORKSPACE_ROLES
|
||||
from .data import PORTFOLIO_ROLES
|
||||
|
||||
|
||||
class NewMemberForm(FlaskForm):
|
||||
@ -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"),
|
||||
choices=WORKSPACE_ROLES,
|
||||
portfolio_role = SelectField(
|
||||
translate("forms.new_member.portfolio_role_label"),
|
||||
choices=PORTFOLIO_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"),
|
||||
)
|
||||
],
|
||||
)
|
@ -18,7 +18,7 @@ from .forms import CacheableForm
|
||||
from .data import (
|
||||
SERVICE_BRANCHES,
|
||||
APP_MIGRATION,
|
||||
PROJECT_COMPLEXITY,
|
||||
APPLICATION_COMPLEXITY,
|
||||
DEV_TEAM,
|
||||
TEAM_EXPERIENCE,
|
||||
PERIOD_OF_PERFORMANCE_LENGTH,
|
||||
@ -52,7 +52,7 @@ class AppInfoForm(CacheableForm):
|
||||
complexity = SelectMultipleField(
|
||||
translate("forms.task_order.complexity_label"),
|
||||
description=translate("forms.task_order.complexity_description"),
|
||||
choices=PROJECT_COMPLEXITY,
|
||||
choices=APPLICATION_COMPLEXITY,
|
||||
default="",
|
||||
widget=ListWidget(prefix_label=False),
|
||||
option_widget=CheckboxInput(),
|
||||
|
@ -7,11 +7,11 @@ 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 .project import Project
|
||||
from .portfolio import Portfolio
|
||||
from .application import Application
|
||||
from .environment import Environment
|
||||
from .attachment import Attachment
|
||||
from .request_revision import RequestRevision
|
||||
|
27
atst/models/application.py
Normal file
27
atst/models/application.py
Normal file
@ -0,0 +1,27 @@
|
||||
from sqlalchemy import Column, ForeignKey, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from atst.models import Base
|
||||
from atst.models.types import Id
|
||||
from atst.models import mixins
|
||||
|
||||
|
||||
class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
__tablename__ = "applications"
|
||||
|
||||
id = Id()
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String, nullable=False)
|
||||
|
||||
portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False)
|
||||
portfolio = relationship("Portfolio")
|
||||
environments = relationship("Environment", back_populates="application")
|
||||
|
||||
@property
|
||||
def displayname(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self): # pragma: no cover
|
||||
return "<Application(name='{}', description='{}', portfolio='{}', id='{}')>".format(
|
||||
self.name, self.description, self.portfolio.name, self.id
|
||||
)
|
@ -14,8 +14,8 @@ class AuditEvent(Base, TimestampsMixin):
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
||||
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_id = Column(UUID(as_uuid=True), ForeignKey("portfolios.id"), index=True)
|
||||
portfolio = relationship("Portfolio", backref="audit_events")
|
||||
|
||||
request_id = Column(UUID(as_uuid=True), ForeignKey("requests.id"), index=True)
|
||||
request = relationship("Request", backref="audit_events")
|
||||
|
@ -12,8 +12,8 @@ class Environment(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
id = Id()
|
||||
name = Column(String, nullable=False)
|
||||
|
||||
project_id = Column(ForeignKey("projects.id"), nullable=False)
|
||||
project = relationship("Project")
|
||||
application_id = Column(ForeignKey("applications.id"), nullable=False)
|
||||
application = relationship("Application")
|
||||
|
||||
cloud_id = Column(String)
|
||||
|
||||
@ -30,17 +30,17 @@ class Environment(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def workspace(self):
|
||||
return self.project.workspace
|
||||
def portfolio(self):
|
||||
return self.application.portfolio
|
||||
|
||||
def auditable_workspace_id(self):
|
||||
return self.project.workspace_id
|
||||
def auditable_portfolio_id(self):
|
||||
return self.application.portfolio_id
|
||||
|
||||
def __repr__(self):
|
||||
return "<Environment(name='{}', num_users='{}', project='{}', workspace='{}', id='{}')>".format(
|
||||
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
|
||||
self.name,
|
||||
self.num_users,
|
||||
self.project.name,
|
||||
self.project.workspace.name,
|
||||
self.application.name,
|
||||
self.application.portfolio.name,
|
||||
self.id,
|
||||
)
|
||||
|
@ -45,10 +45,10 @@ class EnvironmentRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
"role": self.role,
|
||||
"environment": self.environment.displayname,
|
||||
"environment_id": str(self.environment_id),
|
||||
"project": self.environment.project.name,
|
||||
"project_id": str(self.environment.project_id),
|
||||
"workspace": self.environment.project.workspace.name,
|
||||
"workspace_id": str(self.environment.project.workspace.id),
|
||||
"application": self.environment.application.name,
|
||||
"application_id": str(self.environment.application_id),
|
||||
"portfolio": self.environment.application.portfolio.name,
|
||||
"portfolio_id": str(self.environment.application.portfolio.id),
|
||||
}
|
||||
|
||||
|
||||
|
@ -27,11 +27,11 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
||||
user = relationship("User", backref="invitations", foreign_keys=[user_id])
|
||||
|
||||
workspace_role_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("workspace_roles.id"), index=True
|
||||
portfolio_role_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("portfolio_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.portfolio_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.portfolio_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,
|
||||
portfolio_id=portfolio_id,
|
||||
request_id=request_id,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource.id,
|
||||
@ -88,7 +88,7 @@ class AuditableMixin(object):
|
||||
return camel_to_snake(type(self).__name__)
|
||||
|
||||
@property
|
||||
def workspace_id(self):
|
||||
def portfolio_id(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
|
@ -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_PORTFOLIO_AUDIT_LOG = "view_portfolio_audit_log"
|
||||
REQUEST_JEDI_PORTFOLIO = "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 = (
|
||||
"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_PORTFOLIO_INFORMATION = "edit_portfolio_information"
|
||||
DEACTIVATE_PORTFOLIO = "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_PORTFOLIO = "transfer_ownership_of_portfolio"
|
||||
VIEW_PORTFOLIO_MEMBERS = "view_portfolio_members"
|
||||
VIEW_PORTFOLIO = "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_PORTFOLIO = "add_application_in_portfolio"
|
||||
DELETE_APPLICATION_IN_PORTFOLIO = "delete_application_in_portfolio"
|
||||
DEACTIVATE_APPLICATION_IN_PORTFOLIO = "deactivate_application_in_portfolio"
|
||||
VIEW_APPLICATION_IN_PORTFOLIO = "view_application_in_portfolio"
|
||||
RENAME_APPLICATION_IN_PORTFOLIO = "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_PORTFOLIO = "add_tag_to_portfolio"
|
||||
REMOVE_TAG_FROM_PORTFOLIO = "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):
|
||||
__tablename__ = "workspaces"
|
||||
class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
__tablename__ = "portfolios"
|
||||
|
||||
id = types.Id()
|
||||
name = Column(String)
|
||||
request_id = Column(ForeignKey("requests.id"), nullable=True)
|
||||
projects = relationship("Project", 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()
|
||||
)
|
||||
|
||||
@ -54,12 +54,12 @@ class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
|
||||
@property
|
||||
def all_environments(self):
|
||||
return list(chain.from_iterable(p.environments for p in self.projects))
|
||||
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
|
||||
)
|
@ -8,7 +8,7 @@ from .types import Id
|
||||
|
||||
from atst.database import db
|
||||
from atst.models.environment_role import EnvironmentRole
|
||||
from atst.models.project import Project
|
||||
from atst.models.application import Application
|
||||
from atst.models.environment import Environment
|
||||
from atst.models.role import Role
|
||||
|
||||
@ -30,14 +30,14 @@ class Status(Enum):
|
||||
PENDING = "pending"
|
||||
|
||||
|
||||
class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
__tablename__ = "workspace_roles"
|
||||
class PortfolioRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
__tablename__ = "portfolio_roles"
|
||||
|
||||
id = Id()
|
||||
workspace_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("workspaces.id"), index=True, nullable=False
|
||||
portfolio_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("portfolios.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
|
||||
@ -126,9 +126,9 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
return (
|
||||
db.session.query(EnvironmentRole)
|
||||
.join(EnvironmentRole.environment)
|
||||
.join(Environment.project)
|
||||
.join(Project.workspace)
|
||||
.filter(Project.workspace_id == self.workspace_id)
|
||||
.join(Environment.application)
|
||||
.join(Application.portfolio)
|
||||
.filter(Application.portfolio_id == self.portfolio_id)
|
||||
.filter(EnvironmentRole.user_id == self.user_id)
|
||||
.count()
|
||||
)
|
||||
@ -138,9 +138,9 @@ class WorkspaceRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
return (
|
||||
db.session.query(EnvironmentRole)
|
||||
.join(EnvironmentRole.environment)
|
||||
.join(Environment.project)
|
||||
.join(Project.workspace)
|
||||
.filter(Project.workspace_id == self.workspace_id)
|
||||
.join(Environment.application)
|
||||
.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.portfolio_id,
|
||||
unique=True,
|
||||
)
|
@ -1,27 +0,0 @@
|
||||
from sqlalchemy import Column, ForeignKey, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from atst.models import Base
|
||||
from atst.models.types import Id
|
||||
from atst.models import mixins
|
||||
|
||||
|
||||
class Project(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
__tablename__ = "projects"
|
||||
|
||||
id = Id()
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String, nullable=False)
|
||||
|
||||
workspace_id = Column(ForeignKey("workspaces.id"), nullable=False)
|
||||
workspace = relationship("Workspace")
|
||||
environments = relationship("Environment", back_populates="project")
|
||||
|
||||
@property
|
||||
def displayname(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self): # pragma: no cover
|
||||
return "<Project(name='{}', description='{}', workspace='{}', id='{}')>".format(
|
||||
self.name, self.description, self.workspace.name, self.id
|
||||
)
|
@ -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")
|
||||
|
@ -24,8 +24,8 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
|
||||
id = types.Id()
|
||||
|
||||
workspace_id = Column(ForeignKey("workspaces.id"))
|
||||
workspace = relationship("Workspace")
|
||||
portfolio_id = Column(ForeignKey("portfolios.id"))
|
||||
portfolio = relationship("Portfolio")
|
||||
|
||||
user_id = Column(ForeignKey("users.id"))
|
||||
creator = relationship("User", foreign_keys="TaskOrder.user_id")
|
||||
@ -47,7 +47,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
defense_component = Column(String) # Department of Defense Component
|
||||
app_migration = Column(String) # App Migration
|
||||
native_apps = Column(String) # Native Apps
|
||||
complexity = Column(ARRAY(String)) # Project Complexity
|
||||
complexity = Column(ARRAY(String)) # Application Complexity
|
||||
complexity_other = Column(String)
|
||||
dev_team = Column(ARRAY(String)) # Development Team
|
||||
dev_team_other = Column(String)
|
||||
@ -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
|
||||
Permissions.VIEW_PORTFOLIO in self.atat_role.permissions
|
||||
) 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_projects", 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.projects import Projects
|
||||
from atst.domain.workspaces import Workspaces
|
||||
from atst.domain.workspace_roles import WorkspaceRoles, MEMBER_STATUS_CHOICES
|
||||
from atst.domain.applications import Applications
|
||||
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
|
||||
@ -15,7 +15,7 @@ from atst.forms.edit_member import EditMemberForm
|
||||
from atst.forms.data import (
|
||||
ENVIRONMENT_ROLES,
|
||||
ENV_ROLE_MODAL_DESCRIPTION,
|
||||
WORKSPACE_ROLE_DEFINITIONS,
|
||||
PORTFOLIO_ROLE_DEFINITIONS,
|
||||
)
|
||||
from atst.domain.authz import Authorization
|
||||
from atst.models.permissions import Permissions
|
||||
@ -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,48 +38,48 @@ 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,
|
||||
role_choices=WORKSPACE_ROLE_DEFINITIONS,
|
||||
"portfolios/members/index.html",
|
||||
portfolio=portfolio,
|
||||
role_choices=PORTFOLIO_ROLE_DEFINITIONS,
|
||||
status_choices=MEMBER_STATUS_CHOICES,
|
||||
members=members_list,
|
||||
new_member=new_member,
|
||||
)
|
||||
|
||||
|
||||
@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,33 +87,33 @@ 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)
|
||||
projects = Projects.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,
|
||||
projects=projects,
|
||||
applications=applications,
|
||||
form=form,
|
||||
choices=ENVIRONMENT_ROLES,
|
||||
env_role_modal_description=ENV_ROLE_MODAL_DESCRIPTION,
|
||||
@ -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(g.current_user, task_order_id)
|
||||
return render_template(
|
||||
"portfolios/task_orders/show.html", portfolio=portfolio, task_order=task_order
|
||||
)
|
@ -248,9 +248,11 @@ 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")
|
||||
return redirect(url_for("workspaces.new_project", workspace_id=workspace.id))
|
||||
portfolio = Requests.auto_approve_and_create_portfolio(updated_request)
|
||||
flash("new_portfolio")
|
||||
return redirect(
|
||||
url_for("portfolios.new_application", portfolio_id=portfolio.id)
|
||||
)
|
||||
else:
|
||||
return redirect(url_for("requests.requests_index", modal="pendingCCPOApproval"))
|
||||
|
||||
|
@ -14,7 +14,7 @@ class RequestsIndex(object):
|
||||
|
||||
def execute(self):
|
||||
if (
|
||||
Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST
|
||||
Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST
|
||||
in self.user.atat_permissions
|
||||
):
|
||||
context = self._ccpo_view(self.user)
|
||||
@ -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_projects", 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 projects
|
||||
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,100 +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_projects", 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_projects", 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,
|
||||
)
|
@ -1,99 +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.projects import Projects
|
||||
from atst.domain.workspaces import Workspaces
|
||||
from atst.forms.project import NewProjectForm, ProjectForm
|
||||
|
||||
|
||||
@workspaces_bp.route("/workspaces/<workspace_id>/projects")
|
||||
def workspace_projects(workspace_id):
|
||||
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||
return render_template("workspaces/projects/index.html", workspace=workspace)
|
||||
|
||||
|
||||
@workspaces_bp.route("/workspaces/<workspace_id>/projects/new")
|
||||
def new_project(workspace_id):
|
||||
workspace = Workspaces.get_for_update_projects(g.current_user, workspace_id)
|
||||
form = NewProjectForm()
|
||||
return render_template(
|
||||
"workspaces/projects/new.html", workspace=workspace, form=form
|
||||
)
|
||||
|
||||
|
||||
@workspaces_bp.route("/workspaces/<workspace_id>/projects/new", methods=["POST"])
|
||||
def create_project(workspace_id):
|
||||
workspace = Workspaces.get_for_update_projects(g.current_user, workspace_id)
|
||||
form = NewProjectForm(http_request.form)
|
||||
|
||||
if form.validate():
|
||||
project_data = form.data
|
||||
Projects.create(
|
||||
g.current_user,
|
||||
workspace,
|
||||
project_data["name"],
|
||||
project_data["description"],
|
||||
project_data["environment_names"],
|
||||
)
|
||||
return redirect(
|
||||
url_for("workspaces.workspace_projects", workspace_id=workspace.id)
|
||||
)
|
||||
else:
|
||||
return render_template(
|
||||
"workspaces/projects/new.html", workspace=workspace, form=form
|
||||
)
|
||||
|
||||
|
||||
@workspaces_bp.route("/workspaces/<workspace_id>/projects/<project_id>/edit")
|
||||
def edit_project(workspace_id, project_id):
|
||||
workspace = Workspaces.get_for_update_projects(g.current_user, workspace_id)
|
||||
project = Projects.get(g.current_user, workspace, project_id)
|
||||
form = ProjectForm(name=project.name, description=project.description)
|
||||
|
||||
return render_template(
|
||||
"workspaces/projects/edit.html", workspace=workspace, project=project, form=form
|
||||
)
|
||||
|
||||
|
||||
@workspaces_bp.route(
|
||||
"/workspaces/<workspace_id>/projects/<project_id>/edit", methods=["POST"]
|
||||
)
|
||||
def update_project(workspace_id, project_id):
|
||||
workspace = Workspaces.get_for_update_projects(g.current_user, workspace_id)
|
||||
project = Projects.get(g.current_user, workspace, project_id)
|
||||
form = ProjectForm(http_request.form)
|
||||
if form.validate():
|
||||
project_data = form.data
|
||||
Projects.update(g.current_user, workspace, project, project_data)
|
||||
|
||||
return redirect(
|
||||
url_for("workspaces.workspace_projects", workspace_id=workspace.id)
|
||||
)
|
||||
else:
|
||||
return render_template(
|
||||
"workspaces/projects/edit.html",
|
||||
workspace=workspace,
|
||||
project=project,
|
||||
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,20 +0,0 @@
|
||||
from flask import g, render_template
|
||||
|
||||
from . import workspaces_bp
|
||||
from atst.domain.task_orders import TaskOrders
|
||||
from atst.domain.workspaces import Workspaces
|
||||
|
||||
|
||||
@workspaces_bp.route("/workspaces/<workspace_id>/task_orders")
|
||||
def workspace_task_orders(workspace_id):
|
||||
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||
return render_template("workspaces/task_orders/index.html", workspace=workspace)
|
||||
|
||||
|
||||
@workspaces_bp.route("/workspaces/<workspace_id>/task_order/<task_order_id>")
|
||||
def view_task_order(workspace_id, task_order_id):
|
||||
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||
return render_template(
|
||||
"workspaces/task_orders/show.html", workspace=workspace, task_order=task_order
|
||||
)
|
@ -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,30 +1,30 @@
|
||||
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>Workspace access successfully removed from {{ member_name }}.</p>
|
||||
<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": {
|
||||
"title_template": "Workspace role updated successfully",
|
||||
"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": {
|
||||
"title_template": "Workspace created!",
|
||||
"new_portfolio": {
|
||||
"title_template": "Portfolio created!",
|
||||
"message_template": """
|
||||
<p>You are now ready to create projects and environments within the JEDI Cloud.</p>
|
||||
<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.
|
||||
|
@ -4,7 +4,7 @@ import toggler from '../toggler'
|
||||
import EditEnvironmentRole from './edit_environment_role'
|
||||
|
||||
export default {
|
||||
name: 'edit-project-roles',
|
||||
name: 'edit-application-roles',
|
||||
|
||||
mixins: [FormMixin, Modal],
|
||||
|
@ -20,7 +20,7 @@ export default {
|
||||
props: {
|
||||
choices: Array,
|
||||
initialData: String,
|
||||
projectId: String
|
||||
applicationId: String
|
||||
},
|
||||
|
||||
data: function () {
|
||||
@ -30,7 +30,7 @@ export default {
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
this.$root.$on('revoke-' + this.projectId, this.revoke)
|
||||
this.$root.$on('revoke-' + this.applicationId, this.revoke)
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -4,7 +4,7 @@ import textinput from '../text_input'
|
||||
const createEnvironment = (name) => ({ name })
|
||||
|
||||
export default {
|
||||
name: 'new-project',
|
||||
name: 'new-application',
|
||||
|
||||
mixins: [FormMixin],
|
||||
|
@ -84,7 +84,7 @@ export default {
|
||||
sortFunc: alphabeticalSort
|
||||
},
|
||||
{
|
||||
displayName: 'Workspace Role',
|
||||
displayName: 'Portfolio Role',
|
||||
attr: 'role',
|
||||
sortFunc: alphabeticalSort,
|
||||
},
|
||||
|
@ -60,7 +60,7 @@ export default {
|
||||
sortFunc: defaultSort,
|
||||
},
|
||||
{
|
||||
displayName: 'Projected Annual Usage ($)',
|
||||
displayName: 'Applicationed Annual Usage ($)',
|
||||
attr: 'annual_usage',
|
||||
sortFunc: defaultSort,
|
||||
},
|
||||
|
@ -5,8 +5,8 @@ export default {
|
||||
name: 'spend-table',
|
||||
|
||||
props: {
|
||||
projects: Object,
|
||||
workspace: Object,
|
||||
applications: Object,
|
||||
portfolio: Object,
|
||||
environments: Object,
|
||||
currentMonthIndex: String,
|
||||
prevMonthIndex: String,
|
||||
@ -15,21 +15,21 @@ export default {
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
projectsState: this.projects
|
||||
applicationsState: this.applications
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
Object.keys(this.projects).forEach(project => {
|
||||
set(this.projectsState[project], 'isVisible', false)
|
||||
Object.keys(this.applications).forEach(application => {
|
||||
set(this.applicationsState[application], 'isVisible', false)
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle: function (e, projectName) {
|
||||
this.projectsState = Object.assign(this.projectsState, {
|
||||
[projectName]: Object.assign(this.projectsState[projectName],{
|
||||
isVisible: !this.projectsState[projectName].isVisible
|
||||
toggle: function (e, applicationName) {
|
||||
this.applicationsState = Object.assign(this.applicationsState, {
|
||||
[applicationName]: Object.assign(this.applicationsState[applicationName],{
|
||||
isVisible: !this.applicationsState[applicationName].isVisible
|
||||
})
|
||||
})
|
||||
},
|
||||
|
@ -13,9 +13,9 @@ import DetailsOfUse from './components/forms/details_of_use'
|
||||
import poc from './components/forms/poc'
|
||||
import financial from './components/forms/financial'
|
||||
import toggler from './components/toggler'
|
||||
import NewProject from './components/forms/new_project'
|
||||
import NewApplication from './components/forms/new_application'
|
||||
import EditEnvironmentRole from './components/forms/edit_environment_role'
|
||||
import EditProjectRoles from './components/forms/edit_project_roles'
|
||||
import EditApplicationRoles from './components/forms/edit_application_roles'
|
||||
import funding from './components/forms/funding'
|
||||
import Modal from './mixins/modal'
|
||||
import selector from './components/selector'
|
||||
@ -44,7 +44,7 @@ const app = new Vue({
|
||||
DetailsOfUse,
|
||||
poc,
|
||||
financial,
|
||||
NewProject,
|
||||
NewApplication,
|
||||
selector,
|
||||
BudgetChart,
|
||||
SpendTable,
|
||||
@ -52,7 +52,7 @@ const app = new Vue({
|
||||
MembersList,
|
||||
LocalDatetime,
|
||||
EditEnvironmentRole,
|
||||
EditProjectRoles,
|
||||
EditApplicationRoles,
|
||||
RequestsList,
|
||||
ConfirmationPopover,
|
||||
funding,
|
||||
|
@ -83,10 +83,10 @@ export default {
|
||||
unmask: [],
|
||||
validationError: 'Please enter a valid BA Code. Note that it should be two digits, followed by an optional letter.'
|
||||
},
|
||||
workspaceName: {
|
||||
portfolioName: {
|
||||
mask: false,
|
||||
match: /^.{4,100}$/,
|
||||
unmask: [],
|
||||
validationError: 'Workspace and request names must be at least 4 and not more than 100 characters'
|
||||
validationError: 'Portfolio and request names must be at least 4 and not more than 100 characters'
|
||||
},
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Add root project dir to the python path
|
||||
# Add root application dir to the python path
|
||||
import os
|
||||
import sys
|
||||
|
||||
@ -15,14 +15,14 @@ from atst.app import make_config, make_app
|
||||
from atst.models.audit_event import AuditEvent
|
||||
from atst.models.environment import Environment
|
||||
from atst.models.environment_role import EnvironmentRole
|
||||
from atst.models.project import Project
|
||||
from atst.models.application import Application
|
||||
from atst.models.request import Request
|
||||
from atst.models.request_revision import RequestRevision
|
||||
from atst.models.request_status_event import RequestStatus, RequestStatusEvent
|
||||
from atst.models.role import Role
|
||||
from atst.models.user import User
|
||||
from atst.models.workspace_role import WorkspaceRole
|
||||
from atst.models.workspace import Workspace
|
||||
from atst.models.portfolio_role import PortfolioRole
|
||||
from atst.models.portfolio import Portfolio
|
||||
from atst.models.mixins import AuditableMixin
|
||||
|
||||
from atst.domain.environments import Environments
|
||||
@ -30,7 +30,7 @@ from atst.domain.exceptions import NotFoundError
|
||||
from atst.domain.csp.reports import MockReportingProvider
|
||||
from atst.domain.requests import Requests
|
||||
from atst.domain.users import Users
|
||||
from atst.domain.workspaces import Workspaces
|
||||
from atst.domain.portfolios import portfolios
|
||||
from tests.factories import RequestFactory, LegacyTaskOrderFactory
|
||||
|
||||
|
||||
@ -48,29 +48,29 @@ dod_ids = [
|
||||
]
|
||||
|
||||
|
||||
def create_demo_workspace(name, data):
|
||||
def create_demo_portfolio(name, data):
|
||||
try:
|
||||
workspace_owner = Users.get_by_dod_id("678678678") # Other
|
||||
portfolio_owner = Users.get_by_dod_id("678678678") # Other
|
||||
auditor = Users.get_by_dod_id("3453453453") # Sally
|
||||
except NotFoundError:
|
||||
print("Could not find demo users; will not create demo workspace {}".format(name))
|
||||
print("Could not find demo users; will not create demo portfolio {}".format(name))
|
||||
return
|
||||
|
||||
request = RequestFactory.build(creator=workspace_owner)
|
||||
request = RequestFactory.build(creator=portfolio_owner)
|
||||
request.legacy_task_order = LegacyTaskOrderFactory.build()
|
||||
request = Requests.update(
|
||||
request.id, {"financial_verification": RequestFactory.mock_financial_data()}
|
||||
)
|
||||
approved_request = Requests.set_status(request, RequestStatus.APPROVED)
|
||||
|
||||
workspace = Requests.approve_and_create_workspace(request)
|
||||
Workspaces.update(workspace, { "name": name })
|
||||
portfolio = Requests.approve_and_create_portfolio(request)
|
||||
portfolios.update(portfolio, { "name": name })
|
||||
|
||||
for mock_project in data["projects"]:
|
||||
project = Project(workspace=workspace, name=mock_project.name, description='')
|
||||
env_names = [env.name for env in mock_project.environments]
|
||||
envs = Environments.create_many(project, env_names)
|
||||
db.session.add(project)
|
||||
for mock_application in data["applications"]:
|
||||
application = application(portfolio=portfolio, name=mock_application.name, description='')
|
||||
env_names = [env.name for env in mock_application.environments]
|
||||
envs = Environments.create_many(application, env_names)
|
||||
db.session.add(application)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@ -103,18 +103,18 @@ def remove_sample_data(all_users=False):
|
||||
)
|
||||
events = [ev for r in requests for ev in r.status_events]
|
||||
revisions = [rev for r in requests for rev in r.revisions]
|
||||
workspaces = [r.workspace for r in requests if r.workspace]
|
||||
portfolios = [r.portfolio for r in requests if r.portfolio]
|
||||
ws_audit = (
|
||||
db.session.query(AuditEvent)
|
||||
.filter(AuditEvent.workspace_id.in_([w.id for w in workspaces]))
|
||||
.filter(AuditEvent.portfolio_id.in_([w.id for w in portfolios]))
|
||||
.all()
|
||||
)
|
||||
workspace_roles = [role for workspace in workspaces for role in workspace.roles]
|
||||
invites = [invite for role in workspace_roles for invite in role.invitations]
|
||||
projects = [p for workspace in workspaces for p in workspace.projects]
|
||||
portfolio_roles = [role for portfolio in portfolios for role in portfolio.roles]
|
||||
invites = [invite for role in portfolio_roles for invite in role.invitations]
|
||||
applications = [p for portfolio in portfolios for p in portfolio.applications]
|
||||
environments = (
|
||||
db.session.query(Environment)
|
||||
.filter(Environment.project_id.in_([p.id for p in projects]))
|
||||
.filter(Environment.application_id.in_([p.id for p in applications]))
|
||||
.all()
|
||||
)
|
||||
roles = [role for env in environments for role in env.roles]
|
||||
@ -122,9 +122,9 @@ def remove_sample_data(all_users=False):
|
||||
for set_of_things in [
|
||||
roles,
|
||||
environments,
|
||||
projects,
|
||||
applications,
|
||||
invites,
|
||||
workspace_roles,
|
||||
portfolio_roles,
|
||||
ws_audit,
|
||||
events,
|
||||
revisions,
|
||||
@ -135,9 +135,9 @@ def remove_sample_data(all_users=False):
|
||||
|
||||
db.session.commit()
|
||||
|
||||
query = "DELETE FROM workspaces WHERE workspaces.id = ANY(:ids);"
|
||||
query = "DELETE FROM portfolios WHERE portfolios.id = ANY(:ids);"
|
||||
db.session.connection().execute(
|
||||
sqlalchemy.text(query), ids=[w.id for w in workspaces]
|
||||
sqlalchemy.text(query), ids=[w.id for w in portfolios]
|
||||
)
|
||||
|
||||
query = "DELETE FROM requests WHERE requests.id = ANY(:ids);"
|
||||
@ -153,5 +153,5 @@ if __name__ == "__main__":
|
||||
app = make_app(config)
|
||||
with app.app_context():
|
||||
remove_sample_data()
|
||||
create_demo_workspace('Aardvark', MockReportingProvider.REPORT_FIXTURE_MAP["Aardvark"])
|
||||
create_demo_workspace('Beluga', MockReportingProvider.REPORT_FIXTURE_MAP["Beluga"])
|
||||
create_demo_portfolio('Aardvark', MockReportingProvider.REPORT_FIXTURE_MAP["Aardvark"])
|
||||
create_demo_portfolio('Beluga', MockReportingProvider.REPORT_FIXTURE_MAP["Beluga"])
|
||||
|
@ -10,11 +10,11 @@ from sqlalchemy.orm.exc import NoResultFound
|
||||
from atst.app import make_config, make_app
|
||||
from atst.database import db
|
||||
from atst.models import Role, Permissions
|
||||
from atst.domain.roles import ATAT_ROLES, WORKSPACE_ROLES
|
||||
from atst.domain.roles import ATAT_ROLES, PORTFOLIO_ROLES
|
||||
|
||||
|
||||
def seed_roles():
|
||||
for role_info in ATAT_ROLES + WORKSPACE_ROLES:
|
||||
for role_info in ATAT_ROLES + PORTFOLIO_ROLES:
|
||||
role = Role(**role_info)
|
||||
try:
|
||||
existing_role = db.session.query(Role).filter_by(name=role.name).one()
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Add root project dir to the python path
|
||||
# Add root application dir to the python path
|
||||
import os
|
||||
import sys
|
||||
|
||||
@ -9,44 +9,44 @@ from atst.database import db
|
||||
from atst.app import make_config, make_app
|
||||
from atst.domain.users import Users
|
||||
from atst.domain.requests import Requests
|
||||
from atst.domain.workspaces import Workspaces
|
||||
from atst.domain.projects import Projects
|
||||
from atst.domain.workspace_roles import WorkspaceRoles
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.domain.applications import Applications
|
||||
from atst.domain.portfolio_roles import PortfolioRoles
|
||||
from atst.models.invitation import Status as InvitationStatus
|
||||
from atst.domain.exceptions import AlreadyExistsError
|
||||
from tests.factories import RequestFactory, LegacyTaskOrderFactory, InvitationFactory
|
||||
from atst.routes.dev import _DEV_USERS as DEV_USERS
|
||||
|
||||
WORKSPACE_USERS = [
|
||||
portfolio_USERS = [
|
||||
{
|
||||
"first_name": "Danny",
|
||||
"last_name": "Knight",
|
||||
"email": "knight@mil.gov",
|
||||
"workspace_role": "developer",
|
||||
"portfolio_role": "developer",
|
||||
"dod_id": "0000000001",
|
||||
},
|
||||
{
|
||||
"first_name": "Mario",
|
||||
"last_name": "Hudson",
|
||||
"email": "hudson@mil.gov",
|
||||
"workspace_role": "billing_auditor",
|
||||
"portfolio_role": "billing_auditor",
|
||||
"dod_id": "0000000002",
|
||||
},
|
||||
{
|
||||
"first_name": "Louise",
|
||||
"last_name": "Greer",
|
||||
"email": "greer@mil.gov",
|
||||
"workspace_role": "admin",
|
||||
"portfolio_role": "admin",
|
||||
"dod_id": "0000000003",
|
||||
},
|
||||
]
|
||||
|
||||
WORKSPACE_INVITED_USERS = [
|
||||
PORTFOLIO_INVITED_USERS = [
|
||||
{
|
||||
"first_name": "Frederick",
|
||||
"last_name": "Fitzgerald",
|
||||
"email": "frederick@mil.gov",
|
||||
"workspace_role": "developer",
|
||||
"portfolio_role": "developer",
|
||||
"dod_id": "0000000004",
|
||||
"status": InvitationStatus.REJECTED_WRONG_USER
|
||||
},
|
||||
@ -54,7 +54,7 @@ WORKSPACE_INVITED_USERS = [
|
||||
"first_name": "Gina",
|
||||
"last_name": "Guzman",
|
||||
"email": "gina@mil.gov",
|
||||
"workspace_role": "developer",
|
||||
"portfolio_role": "developer",
|
||||
"dod_id": "0000000005",
|
||||
"status": InvitationStatus.REJECTED_EXPIRED
|
||||
},
|
||||
@ -62,7 +62,7 @@ WORKSPACE_INVITED_USERS = [
|
||||
"first_name": "Hector",
|
||||
"last_name": "Harper",
|
||||
"email": "hector@mil.gov",
|
||||
"workspace_role": "developer",
|
||||
"portfolio_role": "developer",
|
||||
"dod_id": "0000000006",
|
||||
"status": InvitationStatus.REVOKED
|
||||
},
|
||||
@ -70,7 +70,7 @@ WORKSPACE_INVITED_USERS = [
|
||||
"first_name": "Isabella",
|
||||
"last_name": "Ingram",
|
||||
"email": "isabella@mil.gov",
|
||||
"workspace_role": "developer",
|
||||
"portfolio_role": "developer",
|
||||
"dod_id": "0000000007",
|
||||
"status": InvitationStatus.PENDING
|
||||
},
|
||||
@ -107,26 +107,26 @@ def seed_db():
|
||||
request.id, {"financial_verification": RequestFactory.mock_financial_data()}
|
||||
)
|
||||
|
||||
workspace = Workspaces.create(
|
||||
user, name="{}'s workspace".format(user.first_name)
|
||||
portfolio = Portfolios.create(
|
||||
user, name="{}'s portfolio".format(user.first_name)
|
||||
)
|
||||
for workspace_role in WORKSPACE_USERS:
|
||||
ws_role = Workspaces.create_member(user, workspace, workspace_role)
|
||||
for portfolio_role in portfolio_USERS:
|
||||
ws_role = Portfolios.create_member(user, portfolio, portfolio_role)
|
||||
db.session.refresh(ws_role)
|
||||
WorkspaceRoles.enable(ws_role)
|
||||
PortfolioRoles.enable(ws_role)
|
||||
|
||||
for workspace_role in WORKSPACE_INVITED_USERS:
|
||||
ws_role = Workspaces.create_member(user, workspace, workspace_role)
|
||||
invitation = InvitationFactory.build(workspace_role=ws_role, status=workspace_role["status"])
|
||||
for portfolio_role in PORTFOLIO_INVITED_USERS:
|
||||
ws_role = Portfolios.create_member(user, portfolio, portfolio_role)
|
||||
invitation = InvitationFactory.build(portfolio_role=ws_role, status=portfolio_role["status"])
|
||||
db.session.add(invitation)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
Projects.create(
|
||||
Applications.create(
|
||||
user,
|
||||
workspace=workspace,
|
||||
name="First Project",
|
||||
description="This is our first project.",
|
||||
portfolio=portfolio,
|
||||
name="First Application",
|
||||
description="This is our first application.",
|
||||
environment_names=["dev", "staging", "prod"],
|
||||
)
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
@import 'components/topbar';
|
||||
@import 'components/global_layout';
|
||||
@import 'components/global_navigation';
|
||||
@import 'components/workspace_layout';
|
||||
@import 'components/portfolio_layout';
|
||||
@import 'components/site_action';
|
||||
@import 'components/empty_state';
|
||||
@import 'components/alerts';
|
||||
@ -42,8 +42,8 @@
|
||||
@import 'sections/login';
|
||||
@import 'sections/home';
|
||||
@import 'sections/request_approval';
|
||||
@import 'sections/projects_list';
|
||||
@import 'sections/project_edit';
|
||||
@import 'sections/application_list';
|
||||
@import 'sections/application_edit';
|
||||
@import 'sections/member_edit';
|
||||
@import 'sections/reports';
|
||||
@import 'sections/task_order';
|
||||
|
@ -18,7 +18,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.global-navigation__context--workspace {
|
||||
&.global-navigation__context--portfolio {
|
||||
.sidenav__link {
|
||||
padding-right: $gap;
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
.workspace-panel-container {
|
||||
.portfolio-panel-container {
|
||||
@include media($large-screen) {
|
||||
@include grid-row;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-navigation {
|
||||
.portfolio-navigation {
|
||||
@include panel-margin;
|
||||
margin-bottom: $gap * 4;
|
||||
|
@ -12,4 +12,4 @@
|
||||
color: $color-primary !important;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -59,11 +59,11 @@
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
|
||||
.topbar__workspace-menu {
|
||||
.topbar__portfolio-menu {
|
||||
margin-right: auto;
|
||||
position: relative;
|
||||
|
||||
.topbar__workspace-menu__toggle {
|
||||
.topbar__portfolio-menu__toggle {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
|
||||
@ -89,12 +89,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.topbar__workspace-menu__panel {
|
||||
.topbar__portfolio-menu__panel {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&.topbar__context--workspace {
|
||||
&.topbar__context--portfolio {
|
||||
background-color: $color-primary;
|
||||
-ms-flex-pack: start;
|
||||
|
||||
|
@ -207,7 +207,7 @@
|
||||
&--validation {
|
||||
|
||||
&--anything,
|
||||
&--workspaceName,
|
||||
&--portfolioName,
|
||||
&--requiredField,
|
||||
&--email {
|
||||
input {
|
||||
|
@ -22,4 +22,4 @@
|
||||
padding-bottom: $gap / 2;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
.project-edit__env-list-item {
|
||||
.application-edit__env-list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
@ -8,7 +8,7 @@
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.project-edit__env-list-item__remover {
|
||||
.application-edit__env-list-item__remover {
|
||||
@include icon-link;
|
||||
@include icon-link-vertical;
|
||||
@include icon-link-color($color-red, $color-red-lightest);
|
@ -1,5 +1,5 @@
|
||||
.project-list-item {
|
||||
.project-list-item__environment {
|
||||
.application-list-item {
|
||||
.application-list-item__environment {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
@ -8,12 +8,12 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.project-list-item__environment__link {
|
||||
.application-list-item__environment__link {
|
||||
@include icon-link;
|
||||
@include icon-link-large;
|
||||
}
|
||||
|
||||
.project-list-item__environment__members {
|
||||
.application-list-item__environment__members {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
@ -67,4 +67,4 @@
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -283,14 +283,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.spend-table__workspace {
|
||||
.spend-table__portfolio {
|
||||
th, td {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.spend-table__project {
|
||||
.spend-table__project__toggler {
|
||||
.spend-table__application {
|
||||
.spend-table__application__toggler {
|
||||
@include icon-link-color($color-black-light, $color-gray-lightest);
|
||||
margin-left: -$gap;
|
||||
|
||||
@ -300,7 +300,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.spend-table__project__env {
|
||||
.spend-table__application__env {
|
||||
margin-left: $gap;
|
||||
|
||||
&:last-child {
|
||||
|
@ -38,8 +38,8 @@
|
||||
<p>After your Task Order is approved you must add that information to your JEDI Cloud Access Request for financial verification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="h4">Designate Workspace Users</h3>
|
||||
<p>Once your JEDI Cloud Access Request is approved by the CCPO the workspace owner will need to set up projects, environments, and users. The workspace owner is the technical POC you originally designated on the request.</p>
|
||||
<h3 class="h4">Designate Portfolio Users</h3>
|
||||
<p>Once your JEDI Cloud Access Request is approved by the CCPO the portfolio owner will need to set up applications, environments, and users. The portfolio owner is the technical POC you originally designated on the request.</p>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="h4">Use JEDI Cloud</h3>
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "audit_log/events/_base.html" %}
|
||||
|
||||
{% block content %}
|
||||
in Workspace <code>{{ event.workspace_id }}</code> ({{ event.workspace.name }})
|
||||
in Portfolio <code>{{ event.portfolio_id }}</code> ({{ event.portfolio.name }})
|
||||
{% endblock %}
|
||||
|
@ -12,8 +12,8 @@
|
||||
<br>
|
||||
in Environment <code>{{ event.event_details["environment_id"] }}</code> ({{ event.event_details["environment"] }})
|
||||
<br>
|
||||
in Project <code>{{ event.event_details["project_id"] }}</code> ({{ event.event_details["project"] }})
|
||||
in Application <code>{{ event.event_details["application_id"] }}</code> ({{ event.event_details["application"] }})
|
||||
<br>
|
||||
in Workspace <code>{{ event.event_details["workspace_id"] }}</code> ({{ event.event_details["workspace"] }})
|
||||
in Portfolio <code>{{ event.event_details["portfolio_id"] }}</code> ({{ event.event_details["portfolio"] }})
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -10,5 +10,5 @@
|
||||
invited {{ event.event_details.email }} (DOD <code>{{ event.event_details.dod_id }}</code>)
|
||||
<br>
|
||||
{% endif %}
|
||||
in Workspace <code>{{ event.workspace_id }}</code> ({{ event.workspace.name }})
|
||||
in Portfolio <code>{{ event.portfolio_id }}</code> ({{ event.portfolio.name }})
|
||||
{% endblock %}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% block content %}
|
||||
for User <code>{{ event.event_details.updated_user_id }}</code> ({{ event.event_details.updated_user_name }})
|
||||
in Workspace <code>{{ event.workspace_id }}</code> ({{ event.workspace.name }})
|
||||
in Portfolio <code>{{ event.portfolio_id }}</code> ({{ event.portfolio.name }})
|
||||
|
||||
{% if event.changed_state.status %}
|
||||
from status "{{ event.changed_state.status[0] }}" to "{{ event.changed_state.status[1] }}"
|
@ -1,4 +1,4 @@
|
||||
{% macro Page(pagination, route, i, label=None, disabled=False, workspace_id=None) -%}
|
||||
{% macro Page(pagination, route, i, label=None, disabled=False, portfolio_id=None) -%}
|
||||
{% set label = label or i %}
|
||||
|
||||
{% set button_class = "page usa-button " %}
|
||||
@ -11,42 +11,42 @@
|
||||
{% set button_class = button_class + "usa-button-secondary" %}
|
||||
{% endif %}
|
||||
|
||||
<a id="{{ label }}" type="button" class="{{ button_class }}" href="{{ url_for(route, workspace_id=workspace_id, page=i, perPage=pagination.per_page) if not disabled else 'null' }}">{{ label }}</a>
|
||||
<a id="{{ label }}" type="button" class="{{ button_class }}" href="{{ url_for(route, portfolio_id=portfolio_id, page=i, perPage=pagination.per_page) if not disabled else 'null' }}">{{ label }}</a>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro Pagination(pagination, route, workspace_id=None) -%}
|
||||
{% macro Pagination(pagination, route, portfolio_id=None) -%}
|
||||
<div class="pagination">
|
||||
|
||||
{% if pagination.page == 1 %}
|
||||
{{ Page(pagination, route, 1, label="first", disabled=True, workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.page - 1, label="prev", disabled=True, workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, 1, label="first", disabled=True, portfolio_id=portfolio_id) }}
|
||||
{{ Page(pagination, route, pagination.page - 1, label="prev", disabled=True, portfolio_id=portfolio_id) }}
|
||||
{% else %}
|
||||
{{ Page(pagination, route, 1, label="first", workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.page - 1, label="prev", workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, 1, label="first", portfolio_id=portfolio_id) }}
|
||||
{{ Page(pagination, route, pagination.page - 1, label="prev", portfolio_id=portfolio_id) }}
|
||||
{% endif %}
|
||||
|
||||
{% if pagination.page == 1 %}
|
||||
{% set max_page = [pagination.pages, 5] | min %}
|
||||
{% for i in range(1, max_page + 1) %}
|
||||
{{ Page(pagination, route, i, workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, i, portfolio_id=portfolio_id) }}
|
||||
{% endfor %}
|
||||
{% elif pagination.page == pagination.pages %}
|
||||
{% for i in range(pagination.pages - 4, pagination.pages + 1) %}
|
||||
{{ Page(pagination, route, i, workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, i, portfolio_id=portfolio_id) }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% set window = pagination | pageWindow %}
|
||||
{% for i in range(window.0, window.1 + 1) %}
|
||||
{{ Page(pagination, route, i, workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, i, portfolio_id=portfolio_id) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if pagination.page == pagination.pages %}
|
||||
{{ Page(pagination, route, pagination.page + 1, label="next", disabled=True, workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.pages, label="last", disabled=True, workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.page + 1, label="next", disabled=True, portfolio_id=portfolio_id) }}
|
||||
{{ Page(pagination, route, pagination.pages, label="last", disabled=True, portfolio_id=portfolio_id) }}
|
||||
{% else %}
|
||||
{{ Page(pagination, route, pagination.page + 1, label="next", workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.pages, label="last", workspace_id=workspace_id) }}
|
||||
{{ Page(pagination, route, pagination.page + 1, label="next", portfolio_id=portfolio_id) }}
|
||||
{{ Page(pagination, route, pagination.pages, label="last", portfolio_id=portfolio_id) }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
@ -1,12 +1,12 @@
|
||||
Join this JEDI Cloud Workspace
|
||||
{{ owner }} has invited you to join a JEDI Cloud Workspace. Login now to view or use your JEDI Cloud resources.
|
||||
Join this JEDI Cloud Portfolio
|
||||
{{ owner }} has invited you to join a JEDI Cloud Portfolio. Login now to view or use your JEDI Cloud resources.
|
||||
|
||||
{{ url_for("workspaces.accept_invitation", token=token, _external=True) }}
|
||||
{{ url_for("portfolios.accept_invitation", token=token, _external=True) }}
|
||||
|
||||
What is JEDI Cloud?
|
||||
JEDI Cloud is a DoD enterprise-wide solution for commercial cloud services.
|
||||
|
||||
What is a JEDI Cloud Workspace?
|
||||
A JEDI Cloud Workspace is where you may access and manage the cloud resources associated with your projects and environments.
|
||||
What is a JEDI Cloud Portfolio?
|
||||
A JEDI Cloud Portfolio is where you may access and manage the cloud resources associated with your applications and environments.
|
||||
|
||||
JEDI Cloud is managed by the Cloud Computing Program Office. Learn more at jedi.cloud.
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
|
||||
{% set title_text = ('fragments.edit_project_form.existing_project_title' | translate({ "project_name": project.name })) if project else ('fragments.edit_project_form.new_project_title' | translate) %}
|
||||
{% set title_text = ('fragments.edit_application_form.existing_application_title' | translate({ "application_name": application.name })) if application else ('fragments.edit_application_form.new_application_title' | translate) %}
|
||||
|
||||
{{ form.csrf_token }}
|
||||
<div class="panel">
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
<div class="panel__content">
|
||||
<p>
|
||||
{{ "fragments.edit_project_form.explain" | translate }}
|
||||
{{ "fragments.edit_application_form.explain" | translate }}
|
||||
</p>
|
||||
{{ TextInput(form.name) }}
|
||||
{{ TextInput(form.description, paragraph=True) }}
|
@ -3,7 +3,7 @@
|
||||
{% set subnav = [
|
||||
{"label":"Financial Verification", "href":"#financial-verification"},
|
||||
{"label":"ID/IQ CLINs", "href":"#idiq-clins"},
|
||||
{"label":"JEDI Cloud Projects", "href":"#jedi-cloud-projects"},
|
||||
{"label":"JEDI Cloud Applications", "href":"#jedi-cloud-applications"},
|
||||
] %}
|
||||
|
||||
{% block doc_content %}
|
||||
@ -63,7 +63,7 @@
|
||||
<tr>
|
||||
<td>Program BA Code</td>
|
||||
<td><em>Example: <br>02</em></td>
|
||||
<td>The Budget Activity Code (or BA Code) is a two digit number that is the category within each appropriation and fund account used to identify the purposes, projects, or types of activities financed by the appropriation fund.</td>
|
||||
<td>The Budget Activity Code (or BA Code) is a two digit number that is the category within each appropriation and fund account used to identify the purposes, applications, or types of activities financed by the appropriation fund.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -122,15 +122,15 @@
|
||||
|
||||
<hr>
|
||||
|
||||
<h2 id='jedi-cloud-projects'>JEDI Cloud Projects</h2>
|
||||
<h2 id='jedi-cloud-applications'>JEDI Cloud Applications</h2>
|
||||
|
||||
<h3>How are projects organized in the JEDI Cloud?</h3>
|
||||
<h3>How are applications organized in the JEDI Cloud?</h3>
|
||||
|
||||
<h4>Project Structure for JEDI Cloud</h4>
|
||||
<h4>Application Structure for JEDI Cloud</h4>
|
||||
|
||||
<p>Separate your workspace into projects and environments; this allows your team to manage user access to systems more securely and track expenditures for each project.</p>
|
||||
<p>Separate your portfolio into applications and environments; this allows your team to manage user access to systems more securely and track expenditures for each application.</p>
|
||||
<p>Here’s an example:<br>
|
||||
Project A has a development environment, production environment, and sandbox environment. The cloud resources in the development environment are grouped and accessed separately from the production environment and sandbox environment.</p>
|
||||
Application A has a development environment, production environment, and sandbox environment. The cloud resources in the development environment are grouped and accessed separately from the production environment and sandbox environment.</p>
|
||||
|
||||
<img src='/static/img/at-at_faqs_content.svg' alt='AT-AT FAQs Content'>
|
||||
|
||||
|
@ -13,4 +13,4 @@
|
||||
<a class="sidenav__link" href="/">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% from "components/sidenav_item.html" import SidenavItem %}
|
||||
|
||||
<div class="global-navigation sidenav {% if workspace %}global-navigation__context--workspace{% endif %}">
|
||||
<div class="global-navigation sidenav {% if portfolio %}global-navigation__context--portfolio{% endif %}">
|
||||
<ul>
|
||||
{{ SidenavItem("New Task Order",
|
||||
href=url_for("task_orders.get_started"),
|
||||
@ -8,8 +8,8 @@
|
||||
active=g.matchesPath('/task_orders/new'),
|
||||
) }}
|
||||
|
||||
{% if g.current_user.has_workspaces %}
|
||||
{{ SidenavItem("Workspaces", href="/workspaces", icon="cloud", active=g.matchesPath('/workspaces')) }}
|
||||
{% if g.current_user.has_portfolios %}
|
||||
{{ SidenavItem("Portfolios", href="/portfolios", icon="cloud", active=g.matchesPath('/portfolios')) }}
|
||||
{% endif %}
|
||||
|
||||
{% if g.Authorization.has_atat_permission(g.current_user, g.Permissions.VIEW_AUDIT_LOG) %}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user