Rename stuff #163022978
This commit is contained in:
dandds 2019-01-14 16:08:14 -05:00 committed by GitHub
commit 6efd304075
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
157 changed files with 3052 additions and 2955 deletions

View File

@ -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`

View File

@ -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")

View File

@ -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")

View File

@ -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)

View 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

View File

@ -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,

View File

@ -1,24 +1,24 @@
from atst.domain.workspace_roles import WorkspaceRoles
from atst.domain.portfolio_roles import PortfolioRoles
from atst.models.permissions import Permissions
from atst.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

View File

@ -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 = {}

View File

@ -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",
)

View File

@ -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
)

View File

@ -0,0 +1,167 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models.portfolio_role import (
PortfolioRole,
Status as PortfolioRoleStatus,
MEMBER_STATUSES,
)
from atst.models.user import User
from .roles import Roles
from .users import Users
from .exceptions import NotFoundError
MEMBER_STATUS_CHOICES = [
dict(name=key, display_name=value) for key, value in MEMBER_STATUSES.items()
]
class PortfolioRoles(object):
@classmethod
def get(cls, portfolio_id, user_id):
try:
portfolio_role = (
db.session.query(PortfolioRole)
.join(User)
.filter(User.id == user_id, PortfolioRole.portfolio_id == portfolio_id)
.one()
)
except NoResultFound:
raise NotFoundError("portfolio_role")
return portfolio_role
@classmethod
def get_by_id(cls, id_):
try:
return db.session.query(PortfolioRole).filter(PortfolioRole.id == id_).one()
except NoResultFound:
raise NotFoundError("portfolio_role")
@classmethod
def _get_active_portfolio_role(cls, portfolio_id, user_id):
try:
return (
db.session.query(PortfolioRole)
.join(User)
.filter(User.id == user_id, PortfolioRole.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()

View File

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

View File

@ -0,0 +1,182 @@
from atst.domain.roles import Roles
from atst.domain.authz import Authorization
from atst.models.permissions import Permissions
from atst.domain.users import Users
from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.environments import Environments
from atst.models.portfolio_role import Status as PortfolioRoleStatus
from .query import PortfoliosQuery
from .scopes import ScopedPortfolio
class PortfolioError(Exception):
pass
class Portfolios(object):
@classmethod
def create(cls, user, name):
portfolio = PortfoliosQuery.create(name=name)
Portfolios._create_portfolio_role(
user, portfolio, "owner", status=PortfolioRoleStatus.ACTIVE
)
PortfoliosQuery.add_and_commit(portfolio)
return portfolio
@classmethod
def create_from_request(cls, request, name=None):
name = name or request.displayname
portfolio = PortfoliosQuery.create(request=request, name=name)
Portfolios._create_portfolio_role(
request.creator, portfolio, "owner", status=PortfolioRoleStatus.ACTIVE
)
PortfoliosQuery.add_and_commit(portfolio)
return portfolio
@classmethod
def get(cls, user, portfolio_id):
portfolio = PortfoliosQuery.get(portfolio_id)
Authorization.check_portfolio_permission(
user, portfolio, Permissions.VIEW_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

View File

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

View File

@ -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,
)

View File

@ -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

View File

@ -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)

View File

@ -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",
)

View File

@ -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)

View File

@ -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,
],
},
{

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -1,182 +0,0 @@
from atst.domain.roles import Roles
from atst.domain.authz import Authorization
from atst.models.permissions import Permissions
from atst.domain.users import Users
from atst.domain.workspace_roles import WorkspaceRoles
from atst.domain.environments import Environments
from atst.models.workspace_role import Status as WorkspaceRoleStatus
from .query import WorkspacesQuery
from .scopes import ScopedWorkspace
class WorkspaceError(Exception):
pass
class Workspaces(object):
@classmethod
def create(cls, user, name):
workspace = WorkspacesQuery.create(name=name)
Workspaces._create_workspace_role(
user, workspace, "owner", status=WorkspaceRoleStatus.ACTIVE
)
WorkspacesQuery.add_and_commit(workspace)
return workspace
@classmethod
def create_from_request(cls, request, name=None):
name = name or request.displayname
workspace = WorkspacesQuery.create(request=request, name=name)
Workspaces._create_workspace_role(
request.creator, workspace, "owner", status=WorkspaceRoleStatus.ACTIVE
)
WorkspacesQuery.add_and_commit(workspace)
return workspace
@classmethod
def get(cls, user, workspace_id):
workspace = WorkspacesQuery.get(workspace_id)
Authorization.check_workspace_permission(
user, workspace, Permissions.VIEW_WORKSPACE, "get workspace"
)
return ScopedWorkspace(user, workspace)
@classmethod
def get_for_update_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

View File

@ -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"
)
),
],

View File

@ -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",
),
]

View File

@ -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()],
)

View File

@ -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"),
)

View File

@ -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

View File

@ -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"),
)
],
)

View File

@ -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(),

View File

@ -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

View 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
)

View File

@ -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")

View File

@ -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,
)

View File

@ -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),
}

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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
)

View File

@ -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,
)

View File

@ -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
)

View File

@ -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")

View File

@ -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):

View File

@ -14,7 +14,7 @@ class User(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
atat_role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"))
atat_role = 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,
)

View File

@ -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")

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
from flask import g, redirect, url_for, render_template
from . 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))

View File

@ -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))

View File

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

View File

@ -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"))

View File

@ -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),
}

View File

@ -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",

View File

@ -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)
)

View File

@ -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,

View File

@ -1,41 +0,0 @@
from flask import Blueprint, request as http_request, g, render_template
workspaces_bp = Blueprint("workspaces", __name__)
from . import index
from . import 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,
}

View File

@ -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,
)

View File

@ -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))

View File

@ -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
)

View File

@ -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

View File

@ -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.

View File

@ -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],

View File

@ -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: {

View File

@ -4,7 +4,7 @@ import textinput from '../text_input'
const createEnvironment = (name) => ({ name })
export default {
name: 'new-project',
name: 'new-application',
mixins: [FormMixin],

View File

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

View File

@ -60,7 +60,7 @@ export default {
sortFunc: defaultSort,
},
{
displayName: 'Projected Annual Usage ($)',
displayName: 'Applicationed Annual Usage ($)',
attr: 'annual_usage',
sortFunc: defaultSort,
},

View File

@ -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
})
})
},

View File

@ -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,

View File

@ -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'
},
}

View File

@ -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"])

View File

@ -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()

View File

@ -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"],
)

View File

@ -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';

View File

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

View File

@ -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;

View File

@ -12,4 +12,4 @@
color: $color-primary !important;
}
}
}

View File

@ -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;

View File

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

View File

@ -22,4 +22,4 @@
padding-bottom: $gap / 2;
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -67,4 +67,4 @@
}
}
}
}

View File

@ -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 {

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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] }}"

View File

@ -1,4 +1,4 @@
{% macro Page(pagination, route, i, label=None, disabled=False, workspace_id=None) -%}
{% macro Page(pagination, route, i, label=None, disabled=False, portfolio_id=None) -%}
{% set label = label or i %}
{% set 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>

View File

@ -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.

View File

@ -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) }}

View File

@ -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>Heres 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'>

View File

@ -13,4 +13,4 @@
<a class="sidenav__link" href="/">Logout</a>
</li>
</ul>
</div>
</div>

View File

@ -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