project -> application everywhere

This commit is contained in:
dandds 2019-01-10 16:38:00 -05:00
parent 9ad3c45200
commit 3fc323d785
67 changed files with 644 additions and 609 deletions

View File

@ -3,46 +3,50 @@ 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.application import Application
from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole
class Projects(object):
class Applications(object):
@classmethod
def create(cls, user, workspace, name, description, environment_names):
project = Project(workspace=workspace, name=name, description=description)
db.session.add(project)
application = Application(
workspace=workspace, name=name, description=description
)
db.session.add(application)
Environments.create_many(project, environment_names)
Environments.create_many(application, environment_names)
db.session.commit()
return project
return application
@classmethod
def get(cls, user, workspace, project_id):
# TODO: this should check permission for this particular project
def get(cls, user, workspace, application_id):
# TODO: this should check permission for this particular application
Authorization.check_workspace_permission(
user,
workspace,
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
"view project in workspace",
"view application in workspace",
)
try:
project = db.session.query(Project).filter_by(id=project_id).one()
application = (
db.session.query(Application).filter_by(id=application_id).one()
)
except NoResultFound:
raise NotFoundError("project")
raise NotFoundError("application")
return project
return application
@classmethod
def for_user(self, user, workspace):
return (
db.session.query(Project)
db.session.query(Application)
.join(Environment)
.join(EnvironmentRole)
.filter(Project.workspace_id == workspace.id)
.filter(Application.workspace_id == workspace.id)
.filter(EnvironmentRole.user_id == user.id)
.all()
)
@ -53,26 +57,26 @@ class Projects(object):
user,
workspace,
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
"view project in workspace",
"view application in workspace",
)
try:
projects = (
db.session.query(Project).filter_by(workspace_id=workspace.id).all()
applications = (
db.session.query(Application).filter_by(workspace_id=workspace.id).all()
)
except NoResultFound:
raise NotFoundError("projects")
raise NotFoundError("applications")
return projects
return applications
@classmethod
def update(cls, user, workspace, project, new_data):
def update(cls, user, workspace, application, new_data):
if "name" in new_data:
project.name = new_data["name"]
application.name = new_data["name"]
if "description" in new_data:
project.description = new_data["description"]
application.description = new_data["description"]
db.session.add(project)
db.session.add(application)
db.session.commit()
return project
return application

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,8 +194,8 @@ 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()
]
)
@ -210,31 +210,31 @@ class MockReportingProvider(ReportingInterface):
def get_total_spending(self, workspace):
if workspace.name in self.REPORT_FIXTURE_MAP:
return self._sum_monthly_spend(
self.REPORT_FIXTURE_MAP[workspace.name]["projects"]
self.REPORT_FIXTURE_MAP[workspace.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_workspace_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 = {}
for month, spends in groupby(sorted(monthly_spend), lambda m: m[0]):
@ -254,39 +254,39 @@ 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.
"""Return month totals rolled up by environment, application, and workspace.
Data should returned with three top level keys, "workspace", "projects",
Data should returned with three top level keys, "workspace", "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.
For example:
{
"environments": { "X-Wing": { "Prod": { "01/2018": 75.42 } } },
"projects": { "X-Wing": { "01/2018": 75.42 } },
"applications": { "X-Wing": { "01/2018": 75.42 } },
"workspace": { "01/2018": 75.42 },
}
"""
projects = workspace.projects
applications = workspace.applications
if workspace.name in self.REPORT_FIXTURE_MAP:
projects = self.REPORT_FIXTURE_MAP[workspace.name]["projects"]
applications = self.REPORT_FIXTURE_MAP[workspace.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)
workspace_totals = self._rollup_workspace_totals(application_totals)
return {
"environments": environments,
"projects": project_totals,
"applications": application_totals,
"workspace": workspace_totals,
}

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.project_id == application.id)
.all()
)

View File

@ -58,7 +58,7 @@ WORKSPACE_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.",
"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.VIEW_ORIGINAL_JEDI_REQEUST,
@ -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,
@ -125,13 +125,13 @@ 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.",
"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_WORKSPACE],
},
{
"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 workspace.",
"permissions": [
Permissions.VIEW_USAGE_REPORT,
Permissions.VIEW_USAGE_DOLLARS,
@ -140,7 +140,7 @@ WORKSPACE_ROLES = [
},
{
"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,

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
@ -24,25 +24,27 @@ class ScopedResource(object):
class ScopedWorkspace(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)
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(
def applications(self):
can_view_all_applications = Authorization.has_workspace_permission(
self.user, self.resource, Permissions.VIEW_APPLICATION_IN_WORKSPACE
)
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
functionality that it only returns sub-resources (environments)

View File

@ -44,10 +44,10 @@ class Workspaces(object):
return ScopedWorkspace(user, workspace)
@classmethod
def get_for_update_projects(cls, user, workspace_id):
def get_for_update_applications(cls, user, workspace_id):
workspace = WorkspacesQuery.get(workspace_id)
Authorization.check_workspace_permission(
user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE, "add project"
user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE, "add application"
)
return workspace

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

@ -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"),
@ -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 workspace assumes when using the JEDI Cloud.<br/><br/>A member may have different environment roles across different applications. A member can only have one assigned environment role in a given environment.",
}
FUNDING_TYPES = [
@ -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

@ -8,7 +8,7 @@ from .data import WORKSPACE_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(

View File

@ -11,7 +11,7 @@ from .workspace_role import WorkspaceRole
from .pe_number import PENumber
from .legacy_task_order import LegacyTaskOrder
from .workspace import Workspace
from .project import Project
from .application import Application
from .environment import Environment
from .attachment import Attachment
from .request_revision import RequestRevision

View File

@ -6,7 +6,7 @@ from atst.models.types import Id
from atst.models import mixins
class Project(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
__tablename__ = "projects"
id = Id()
@ -15,13 +15,13 @@ class Project(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
workspace_id = Column(ForeignKey("workspaces.id"), nullable=False)
workspace = relationship("Workspace")
environments = relationship("Environment", back_populates="project")
environments = relationship("Environment", back_populates="application")
@property
def displayname(self):
return self.name
def __repr__(self): # pragma: no cover
return "<Project(name='{}', description='{}', workspace='{}', id='{}')>".format(
return "<Application(name='{}', description='{}', workspace='{}', id='{}')>".format(
self.name, self.description, self.workspace.name, self.id
)

View File

@ -13,7 +13,7 @@ class Environment(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
name = Column(String, nullable=False)
project_id = Column(ForeignKey("projects.id"), nullable=False)
project = relationship("Project")
application = relationship("Application")
cloud_id = Column(String)
@ -31,16 +31,16 @@ class Environment(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
@property
def workspace(self):
return self.project.workspace
return self.application.workspace
def auditable_workspace_id(self):
return self.project.workspace_id
return self.application.workspace_id
def __repr__(self):
return "<Environment(name='{}', num_users='{}', project='{}', workspace='{}', id='{}')>".format(
return "<Environment(name='{}', num_users='{}', application='{}', workspace='{}', id='{}')>".format(
self.name,
self.num_users,
self.project.name,
self.project.workspace.name,
self.application.name,
self.application.workspace.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.project_id),
"workspace": self.environment.application.workspace.name,
"workspace_id": str(self.environment.application.workspace.id),
}

View File

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

View File

@ -14,7 +14,7 @@ class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
id = types.Id()
name = Column(String)
request_id = Column(ForeignKey("requests.id"), nullable=True)
projects = relationship("Project", back_populates="workspace")
applications = relationship("Application", back_populates="workspace")
roles = relationship("WorkspaceRole")
task_orders = relationship("TaskOrder")
@ -54,7 +54,7 @@ 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):
return 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
@ -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.workspace)
.filter(Application.workspace_id == self.workspace_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.workspace)
.filter(Application.workspace_id == self.workspace_id)
.filter(EnvironmentRole.user_id == self.user_id)
.all()
)

View File

@ -67,7 +67,7 @@ def home():
)
else:
return redirect(
url_for("workspaces.workspace_projects", workspace_id=workspace_id)
url_for("workspaces.workspace_applications", workspace_id=workspace_id)
)
else:
return redirect(url_for("workspaces.workspaces"))

View File

@ -250,7 +250,9 @@ 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))
return redirect(
url_for("workspaces.new_application", workspace_id=workspace.id)
)
else:
return redirect(url_for("requests.requests_index", modal="pendingCCPOApproval"))

View File

@ -66,7 +66,7 @@ class RequestsIndex(object):
def _workspace_link_for_request(self, request):
if request.is_approved:
return url_for(
"workspaces.workspace_projects", workspace_id=request.workspace.id
"workspaces.workspace_applications", workspace_id=request.workspace.id
)
else:
return None

View File

@ -3,7 +3,7 @@ from flask import Blueprint, request as http_request, g, render_template
workspaces_bp = Blueprint("workspaces", __name__)
from . import index
from . import projects
from . import applications
from . import members
from . import invitations
from . import task_orders

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

View File

@ -32,7 +32,7 @@ def edit_workspace(workspace_id):
if form.validate():
Workspaces.update(workspace, form.data)
return redirect(
url_for("workspaces.workspace_projects", workspace_id=workspace.id)
url_for("workspaces.workspace_applications", workspace_id=workspace.id)
)
else:
return render_template("workspaces/edit.html", form=form, workspace=workspace)
@ -40,7 +40,9 @@ def edit_workspace(workspace_id):
@workspaces_bp.route("/workspaces/<workspace_id>")
def show_workspace(workspace_id):
return redirect(url_for("workspaces.workspace_projects", workspace_id=workspace_id))
return redirect(
url_for("workspaces.workspace_applications", workspace_id=workspace_id)
)
@workspaces_bp.route("/workspaces/<workspace_id>/reports")

View File

@ -4,7 +4,7 @@ from flask import render_template, request as http_request, g, redirect, url_for
from . import workspaces_bp
from atst.domain.exceptions import AlreadyExistsError
from atst.domain.projects import Projects
from atst.domain.applications import Applications
from atst.domain.workspaces import Workspaces
from atst.domain.workspace_roles import WorkspaceRoles, MEMBER_STATUS_CHOICES
from atst.domain.environments import Environments
@ -101,7 +101,7 @@ def view_member(workspace_id, member_id):
"edit this workspace user",
)
member = WorkspaceRoles.get(workspace_id, member_id)
projects = Projects.get_all(g.current_user, member, workspace)
applications = Applications.get_all(g.current_user, member, workspace)
form = EditMemberForm(workspace_role=member.role_name)
editable = g.current_user == member.user
can_revoke_access = Workspaces.can_revoke_access_for(workspace, member)
@ -113,7 +113,7 @@ def view_member(workspace_id, member_id):
"workspaces/members/edit.html",
workspace=workspace,
member=member,
projects=projects,
applications=applications,
form=form,
choices=ENVIRONMENT_ROLES,
env_role_modal_description=ENV_ROLE_MODAL_DESCRIPTION,

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

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

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

View File

@ -5,7 +5,7 @@ export default {
name: 'spend-table',
props: {
projects: Object,
applications: Object,
workspace: Object,
environments: Object,
currentMonthIndex: 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

@ -10,7 +10,7 @@ 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.applications import Applications
from atst.domain.workspace_roles import WorkspaceRoles
from atst.models.invitation import Status as InvitationStatus
from atst.domain.exceptions import AlreadyExistsError
@ -122,7 +122,7 @@ def seed_db():
db.session.commit()
Projects.create(
Applications.create(
user,
workspace=workspace,
name="First Project",

View File

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

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

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

@ -289,8 +289,8 @@
}
}
.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

@ -12,7 +12,7 @@
<br>
in Environment <code>{{ event.event_details["environment_id"] }}</code> ({{ event.event_details["environment"] }})
<br>
in Application <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 Portfolio <code>{{ event.event_details["workspace_id"] }}</code> ({{ event.event_details["workspace"] }})
{% endif %}

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 %}
@ -122,13 +122,13 @@
<hr>
<h2 id='jedi-cloud-projects'>JEDI Cloud Applications</h2>
<h2 id='jedi-cloud-applications'>JEDI Cloud Applications</h2>
<h3>How are applications organized in the JEDI Cloud?</h3>
<h4>Application Structure for JEDI Cloud</h4>
<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 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>
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>

View File

@ -3,14 +3,14 @@
<nav class='sidenav workspace-navigation'>
<ul>
{{ SidenavItem(
("navigation.workspace_navigation.projects" | translate),
href=url_for("workspaces.workspace_projects", workspace_id=workspace.id),
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/projects'),
("navigation.workspace_navigation.applications" | translate),
href=url_for("workspaces.workspace_applications", workspace_id=workspace.id),
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/applications'),
subnav=None if not user_can(permissions.ADD_APPLICATION_IN_WORKSPACE) else [
{
"label": ("navigation.workspace_navigation.add_new_project_label" | translate),
"href": url_for('workspaces.new_project', workspace_id=workspace.id),
"active": g.matchesPath('\/workspaces\/[A-Za-z0-9-]*\/projects'),
"label": ("navigation.workspace_navigation.add_new_application_label" | translate),
"href": url_for('workspaces.new_application', workspace_id=workspace.id),
"active": g.matchesPath('\/workspaces\/[A-Za-z0-9-]*\/applications'),
"icon": "plus"
}
]

View File

@ -17,7 +17,7 @@
<p>The Portfolio Owner is the primary point of contact and technical administrator of the JEDI Cloud Portfolio and will have the
following responsibilities:</p>
<ul>
<li>Organize your cloud-hosted systems into projects and environments</li>
<li>Organize your cloud-hosted systems into applications and environments</li>
<li>Add users to this portfolio and manage members</li>
<li>Manage access to the JEDI Cloud service providers portal</li>
</ul>

View File

@ -4,11 +4,11 @@
{% block workspace_content %}
<form method="POST" action="{{ url_for('workspaces.edit_project', workspace_id=workspace.id, project_id=project.id) }}">
<form method="POST" action="{{ url_for('workspaces.edit_application', workspace_id=workspace.id, application_id=application.id) }}">
{% include "fragments/edit_project_form.html" %}
{% include "fragments/edit_application_form.html" %}
<div class="block-list project-list-item">
<div class="block-list application-list-item">
<header class="block-list__header block-list__header--grow">
<h2 class="block-list__title">Application Environments</h2>
<p>
@ -17,8 +17,8 @@
</header>
<ul>
{% for environment in project.environments %}
<li class="block-list__item project-edit__env-list-item">
{% for environment in application.environments %}
<li class="block-list__item application-edit__env-list-item">
<div class="usa-input">
<label>Environment Name</label>
<input type="text" value="{{ environment.name }}" readonly />

View File

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

View File

@ -7,11 +7,11 @@
{% block workspace_content %}
{% set modalName = "newProjectConfirmation" %}
{% set modalName = "newApplicationConfirmation" %}
{% include "fragments/flash.html" %}
<new-project inline-template v-bind:initial-data='{{ form.data|tojson }}' modal-name='{{ modalName }}'>
<form method="POST" action="{{ url_for('workspaces.create_project', workspace_id=workspace.id) }}" v-on:submit="handleSubmit">
<new-application inline-template v-bind:initial-data='{{ form.data|tojson }}' modal-name='{{ modalName }}'>
<form method="POST" action="{{ url_for('workspaces.create_application', workspace_id=workspace.id) }}" v-on:submit="handleSubmit">
{% call Modal(name=modalName, dismissable=False) %}
<h1>Create application !{ name }</h1>
@ -21,7 +21,7 @@
<span v-for="(environment, index) in environments">
<strong>!{environment.name}</strong><template v-if="index < (environments.length - 1)">, </template>
</span>
will be created as individual cloud resource groups under <strong>!{ name }</strong> project.
will be created as individual cloud resource groups under <strong>!{ name }</strong> application.
</p>
<div class='action-group'>
@ -30,7 +30,7 @@
</div>
{% endcall %}
{% include "fragments/edit_project_form.html" %}
{% include "fragments/edit_application_form.html" %}
<div> {# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #}
<div v-cloak v-for="title in errors" :key="title">
@ -38,7 +38,7 @@
</div>
</div>
<div class="block-list project-list-item">
<div class="block-list application-list-item">
<header class="block-list__header block-list__header--grow">
<h2 class="block-list__title">Application Environments</h2>
<p>
@ -47,13 +47,13 @@
</header>
<ul>
<li v-for="(environment, i) in environments" class="block-list__item project-edit__env-list-item">
<li v-for="(environment, i) in environments" class="block-list__item application-edit__env-list-item">
<div class="usa-input">
<label :for="'environment_names-' + i">Environment Name</label>
<input type="text" :id="'environment_names-' + i" v-model="environment.name" placeholder="e.g. Development, Staging, Production"/>
<input type="hidden" :name="'environment_names-' + i" v-model="environment.name"/>
</div>
<button v-on:click="removeEnvironment(i)" v-if="environments.length > 1" type="button" class='project-edit__env-list-item__remover'>
<button v-on:click="removeEnvironment(i)" v-if="environments.length > 1" type="button" class='application-edit__env-list-item__remover'>
{{ Icon('trash') }}
<span>Remove</span>
</button>
@ -69,6 +69,6 @@
<button class="usa-button usa-button-primary" tabindex="0" type="submit">Create Application</button>
</div>
</form>
</new-project>
</new-application>
{% endblock %}

View File

@ -24,7 +24,7 @@
<div class='action-group'>
<button type="submit" class="usa-button usa-button-big usa-button-primary" tabindex="0">Save</button>
<a href='{{ url_for("workspaces.workspace_projects", workspace_id=workspace.id) }}' class='action-group__action icon-link'>
<a href='{{ url_for("workspaces.workspace_applications", workspace_id=workspace.id) }}' class='action-group__action icon-link'>
{{ Icon('x') }}
<span>Cancel</span>
</a>

View File

@ -14,7 +14,7 @@
{% for workspace in workspaces %}
<tr>
<td>
<a class='icon-link icon-link--large' href="/workspaces/{{ workspace.id }}/projects">{{ workspace.name }}</a><br>
<a class='icon-link icon-link--large' href="/workspaces/{{ workspace.id }}/applications">{{ workspace.name }}</a><br>
</td>
<td>
#{{ workspace.legacy_task_order.number }}

View File

@ -69,24 +69,24 @@
<div class='search-bar'>
<div class='usa-input search-input'>
<label for='project-search'>Search by application name</label>
<input type='search' id='project-search' name='project-search' placeholder="Search by application name"/>
<label for='application-search'>Search by application name</label>
<input type='search' id='application-search' name='application-search' placeholder="Search by application name"/>
<button type="submit">
<span class="hide">Search</span>
</button>
</div>
</div>
{% for project in projects %}
{% set revoke_modal_name = (project.id|string) + 'RevokeModal' %}
<edit-project-roles inline-template name="{{ project.name }}" id="{{ project.id }}">
<div is='toggler' default-visible class='block-list project-list-item'>
{% for application in applications %}
{% set revoke_modal_name = (application.id|string) + 'RevokeModal' %}
<edit-application-roles inline-template name="{{ application.name }}" id="{{ application.id }}">
<div is='toggler' default-visible class='block-list application-list-item'>
<template slot-scope='props'>
<header class='block-list__header'>
<button v-on:click='props.toggle' class='icon-link icon-link--large icon-link--default spend-table__project__toggler'>
<button v-on:click='props.toggle' class='icon-link icon-link--large icon-link--default spend-table__application__toggler'>
<template v-if='props.isVisible'>{{ Icon('caret_down') }}</template>
<template v-else>{{ Icon('caret_right') }}</template>
<h3 class="block-list__title">{{ project.name }}</h3>
<h3 class="block-list__title">{{ application.name }}</h3>
</button>
<span><a v-on:click="openModal('{{ revoke_modal_name }}')" class="icon-link icon-link--danger">revoke all access</a></span>
</header>
@ -94,7 +94,7 @@
<div>
<h1>Revoke Access</h1>
<p>
Confirming will revoke access for {{ member.user.full_name }} to any environments associated with {{ project.name }}.
Confirming will revoke access for {{ member.user.full_name }} to any environments associated with {{ application.name }}.
</p>
<div class='action-group'>
<a v-on:click="doRevoke(); closeModal('{{ revoke_modal_name }}')" class='action-group__action usa-button'>Confirm</a>
@ -103,19 +103,19 @@
</div>
{% endcall %}
<ul v-show='props.isVisible'>
{% for env in project.environments %}
{% for env in application.environments %}
{% set role = EnvironmentRoles.get(member.user_id, env.id).role %}
{% set env_modal_name = (env.id|string) + 'RolesModal' %}
<li class='block-list__item'>
<edit-environment-role inline-template initial-data='{{ role or "" }}' v-bind:choices='{{ choices | tojson }}' v-bind:project-id="'{{ project.id }}'">
<div class='project-list-item__environment'>
<span class='project-list-item__environment__link'>
<edit-environment-role inline-template initial-data='{{ role or "" }}' v-bind:choices='{{ choices | tojson }}' v-bind:application-id="'{{ application.id }}'">
<div class='application-list-item__environment'>
<span class='application-list-item__environment__link'>
{{ env.name }}
</span>
<div class='project-list-item__environment__actions'>
<div class='application-list-item__environment__actions'>
<span v-bind:class="label_class" v-html:on=displayName></span>
<button v-on:click="openModal('{{env_modal_name}}')" type="button" class="icon-link">set role</button>
{% call Modal(name=env_modal_name, dismissable=False) %}
@ -170,7 +170,7 @@
</ul>
</template>
</div>
</edit-project-roles>
</edit-application-roles>
{% endfor %}
<div class='action-group'>

View File

@ -1,55 +0,0 @@
{% from "components/icon.html" import Icon %}
{% from "components/empty_state.html" import EmptyState %}
{% extends "workspaces/base.html" %}
{% block workspace_content %}
{% if not workspace.projects %}
{% set can_create_projects = user_can(permissions.ADD_APPLICATION_IN_WORKSPACE) %}
{{ EmptyState(
'This portfolio doesnt have any applications yet.',
action_label='Add a New Application' if can_create_projects else None,
action_href=url_for('workspaces.new_project', workspace_id=workspace.id) if can_create_projects else None,
icon='cloud',
sub_message=None if can_create_projects else 'Please contact your JEDI Cloud portfolio administrator to set up a new project.'
) }}
{% else %}
{% for project in workspace.projects %}
<div v-cloak class='block-list project-list-item'>
<header class='block-list__header'>
<h2 class='block-list__title'>{{ project.name }} ({{ project.environments|length }} environments)</h2>
{% if user_can(permissions.RENAME_APPLICATION_IN_WORKSPACE) %}
<a class='icon-link' href='{{ url_for("workspaces.edit_project", workspace_id=workspace.id, project_id=project.id) }}'>
{{ Icon('edit') }}
<span>edit</span>
</a>
{% endif %}
</header>
<ul>
{% for environment in project.environments %}
<li class='block-list__item project-list-item__environment'>
<a href='{{ url_for("workspaces.access_environment", workspace_id=workspace.id, environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer' class='project-list-item__environment__link'>
{{ Icon('link') }}
<span>{{ environment.name }}</span>
</a>
<div class='project-list-item__environment__members'>
<div class='label'>{{ environment.num_users }}</div>
<span>members</span>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% endif %}
{% endblock %}

View File

@ -113,18 +113,18 @@
{% set two_months_ago_index = two_months_ago.strftime('%m/%Y') %}
{% set reports_url = url_for("workspaces.workspace_reports", workspace_id=workspace.id) %}
{% if not workspace.projects %}
{% if not workspace.applications %}
{% set can_create_projects = user_can(permissions.ADD_APPLICATION_IN_WORKSPACE) %}
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_WORKSPACE) %}
{% set message = 'This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.'
if can_create_projects
if can_create_applications
else 'This portfolio has no cloud environments set up, so there is no spending data to report. Contact the portfolio owner to set up some cloud environments.'
%}
{{ EmptyState(
'Nothing to report',
action_label='Add a New Application' if can_create_projects else None,
action_href=url_for('workspaces.new_project', workspace_id=workspace.id) if can_create_projects else None,
action_label='Add a New Application' if can_create_applications else None,
action_href=url_for('workspaces.new_application', workspace_id=workspace.id) if can_create_applications else None,
icon='chart',
sub_message=message
) }}
@ -353,7 +353,7 @@
</div>
<spend-table
v-bind:projects='{{ monthly_totals['projects'] | tojson }}'
v-bind:applications='{{ monthly_totals['applications'] | tojson }}'
v-bind:workspace='{{ workspace_totals | tojson }}'
v-bind:environments='{{ monthly_totals['environments'] | tojson }}'
current-month-index='{{ current_month_index }}'
@ -383,40 +383,40 @@
</tr>
</tbody>
<tbody v-for='(project, name) in projectsState' class='spend-table__project'>
<tbody v-for='(application, name) in applicationsState' class='spend-table__application'>
<tr>
<th scope='rowgroup'>
<button v-on:click='toggle($event, name)' class='icon-link icon-link--large spend-table__project__toggler'>
<template v-if='project.isVisible'>{{ Icon('caret_down') }}</template>
<button v-on:click='toggle($event, name)' class='icon-link icon-link--large spend-table__application__toggler'>
<template v-if='application.isVisible'>{{ Icon('caret_down') }}</template>
<template v-else>{{ Icon('caret_right') }}</template>
<span v-html='name'></span>
</button>
</th>
<td class='table-cell--align-right previous-month'>
<span v-html='formatDollars(project[twoMonthsAgoIndex] || 0)'></span>
<span v-html='formatDollars(application[twoMonthsAgoIndex] || 0)'></span>
</td>
<td class='table-cell--align-right previous-month'>
<span v-html='formatDollars(project[prevMonthIndex] || 0)'></span>
<span v-html='formatDollars(application[prevMonthIndex] || 0)'></span>
</td>
<td class='table-cell--align-right current-month'>
<span v-html='formatDollars(project[currentMonthIndex] || 0)'></span>
<span v-html='formatDollars(application[currentMonthIndex] || 0)'></span>
</td>
<td class='table-cell--expand current-month meter-cell'>
<span class='spend-table__meter-value'>
<span v-html='round( 100 * ((project[currentMonthIndex] || 0) / (workspace[currentMonthIndex] || 1) )) + "%"'></span>
<span v-html='round( 100 * ((application[currentMonthIndex] || 0) / (workspace[currentMonthIndex] || 1) )) + "%"'></span>
</span>
<meter v-bind:value='project[currentMonthIndex] || 0' min='0' v-bind:max='workspace[currentMonthIndex] || 1'>
<div class='meter__fallback' v-bind:style='"width:" + round( 100 * ((project[currentMonthIndex] || 0) / (workspace[currentMonthIndex] || 1) )) + "%;"'></div>
<meter v-bind:value='application[currentMonthIndex] || 0' min='0' v-bind:max='workspace[currentMonthIndex] || 1'>
<div class='meter__fallback' v-bind:style='"width:" + round( 100 * ((application[currentMonthIndex] || 0) / (workspace[currentMonthIndex] || 1) )) + "%;"'></div>
</meter>
</td>
</tr>
<tr v-for='(environment, envName) in environments[name]' v-show='project.isVisible' class='spend-table__project__env'>
<tr v-for='(environment, envName) in environments[name]' v-show='application.isVisible' class='spend-table__application__env'>
<th scope='rowgroup'>
<a href='#' class='icon-link spend-table__project__env'>
<a href='#' class='icon-link spend-table__application__env'>
{{ Icon('link') }}
<span v-html='envName'></span>
</a>

View File

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

View File

@ -8,7 +8,7 @@ from tests.factories import (
UserFactory,
WorkspaceFactory,
WorkspaceRoleFactory,
ProjectFactory,
ApplicationFactory,
)
@ -81,10 +81,10 @@ def test_other_users_cannot_view_ws_audit_log():
def test_paginate_ws_audit_log():
workspace = WorkspaceFactory.create()
project = ProjectFactory.create(workspace=workspace)
application = ApplicationFactory.create(workspace=workspace)
for _ in range(100):
AuditLog.log_system_event(
resource=project, action="create", workspace=workspace
resource=application, action="create", workspace=workspace
)
events = AuditLog.get_workspace_events(
@ -98,8 +98,8 @@ def test_ws_audit_log_only_includes_current_ws_events():
workspace = WorkspaceFactory.create(owner=owner)
other_workspace = WorkspaceFactory.create(owner=owner)
# Add some audit events
project_1 = ProjectFactory.create(workspace=workspace)
project_2 = ProjectFactory.create(workspace=other_workspace)
application_1 = ApplicationFactory.create(workspace=workspace)
application_2 = ApplicationFactory.create(workspace=other_workspace)
events = AuditLog.get_workspace_events(workspace.owner, workspace)
for event in events:

View File

@ -2,12 +2,12 @@ from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.workspace_roles import WorkspaceRoles
from tests.factories import ProjectFactory, UserFactory, WorkspaceFactory
from tests.factories import ApplicationFactory, UserFactory, WorkspaceFactory
def test_create_environments():
project = ProjectFactory.create()
environments = Environments.create_many(project, ["Staging", "Production"])
application = ApplicationFactory.create()
environments = Environments.create_many(application, ["Staging", "Production"])
for env in environments:
assert env.cloud_id is not None
@ -19,10 +19,12 @@ def test_create_environment_role_creates_cloud_id(session):
workspace = WorkspaceFactory.create(
owner=owner,
members=[{"user": developer, "role_name": "developer"}],
projects=[{"name": "project1", "environments": [{"name": "project1 prod"}]}],
applications=[
{"name": "application1", "environments": [{"name": "application1 prod"}]}
],
)
env = workspace.projects[0].environments[0]
env = workspace.applications[0].environments[0]
new_role = [{"id": env.id, "role": "developer"}]
workspace_role = workspace.members[0]
@ -41,26 +43,26 @@ def test_update_environment_roles():
workspace = WorkspaceFactory.create(
owner=owner,
members=[{"user": developer, "role_name": "developer"}],
projects=[
applications=[
{
"name": "project1",
"name": "application1",
"environments": [
{
"name": "project1 dev",
"name": "application1 dev",
"members": [{"user": developer, "role_name": "devlops"}],
},
{
"name": "project1 staging",
"name": "application1 staging",
"members": [{"user": developer, "role_name": "developer"}],
},
{"name": "project1 prod"},
{"name": "application1 prod"},
],
}
],
)
dev_env = workspace.projects[0].environments[0]
staging_env = workspace.projects[0].environments[1]
dev_env = workspace.applications[0].environments[0]
staging_env = workspace.applications[0].environments[1]
new_ids_and_roles = [
{"id": dev_env.id, "role": "billing_admin"},
{"id": staging_env.id, "role": "developer"},
@ -83,34 +85,34 @@ def test_remove_environment_role():
workspace = WorkspaceFactory.create(
owner=owner,
members=[{"user": developer, "role_name": "developer"}],
projects=[
applications=[
{
"name": "project1",
"name": "application1",
"environments": [
{
"name": "project1 dev",
"name": "application1 dev",
"members": [{"user": developer, "role_name": "devops"}],
},
{
"name": "project1 staging",
"name": "application1 staging",
"members": [{"user": developer, "role_name": "developer"}],
},
{
"name": "project1 uat",
"name": "application1 uat",
"members": [
{"user": developer, "role_name": "financial_auditor"}
],
},
{"name": "project1 prod"},
{"name": "application1 prod"},
],
}
],
)
project = workspace.projects[0]
now_ba = project.environments[0].id
now_none = project.environments[1].id
still_fa = project.environments[2].id
application = workspace.applications[0]
now_ba = application.environments[0].id
now_none = application.environments[1].id
still_fa = application.environments[2].id
new_environment_roles = [
{"id": now_ba, "role": "billing_auditor"},
@ -135,12 +137,12 @@ def test_no_update_to_environment_roles():
workspace = WorkspaceFactory.create(
owner=owner,
members=[{"user": developer, "role_name": "developer"}],
projects=[
applications=[
{
"name": "project1",
"name": "application1",
"environments": [
{
"name": "project1 dev",
"name": "application1 dev",
"members": [{"user": developer, "role_name": "devops"}],
}
],
@ -148,7 +150,7 @@ def test_no_update_to_environment_roles():
],
)
dev_env = workspace.projects[0].environments[0]
dev_env = workspace.applications[0].environments[0]
new_ids_and_roles = [{"id": dev_env.id, "role": "devops"}]
workspace_role = WorkspaceRoles.get(workspace.id, developer.id)
@ -161,34 +163,34 @@ def test_get_scoped_environments(db):
developer = UserFactory.create()
workspace = WorkspaceFactory.create(
members=[{"user": developer, "role_name": "developer"}],
projects=[
applications=[
{
"name": "project1",
"name": "application1",
"environments": [
{
"name": "project1 dev",
"name": "application1 dev",
"members": [{"user": developer, "role_name": "developer"}],
},
{"name": "project1 staging"},
{"name": "project1 prod"},
{"name": "application1 staging"},
{"name": "application1 prod"},
],
},
{
"name": "project2",
"name": "application2",
"environments": [
{"name": "project2 dev"},
{"name": "application2 dev"},
{
"name": "project2 staging",
"name": "application2 staging",
"members": [{"user": developer, "role_name": "developer"}],
},
{"name": "project2 prod"},
{"name": "application2 prod"},
],
},
],
)
project1_envs = Environments.for_user(developer, workspace.projects[0])
assert [env.name for env in project1_envs] == ["project1 dev"]
application1_envs = Environments.for_user(developer, workspace.applications[0])
assert [env.name for env in application1_envs] == ["application1 dev"]
project2_envs = Environments.for_user(developer, workspace.projects[1])
assert [env.name for env in project2_envs] == ["project2 staging"]
application2_envs = Environments.for_user(developer, workspace.applications[1])
assert [env.name for env in application2_envs] == ["application2 staging"]

View File

@ -1,57 +0,0 @@
from atst.domain.projects import Projects
from tests.factories import RequestFactory, UserFactory, WorkspaceFactory
from atst.domain.workspaces import Workspaces
def test_create_project_with_multiple_environments():
request = RequestFactory.create()
workspace = Workspaces.create_from_request(request)
project = Projects.create(
workspace.owner, workspace, "My Test Project", "Test", ["dev", "prod"]
)
assert project.workspace == workspace
assert project.name == "My Test Project"
assert project.description == "Test"
assert sorted(e.name for e in project.environments) == ["dev", "prod"]
def test_workspace_owner_can_view_environments():
owner = UserFactory.create()
workspace = WorkspaceFactory.create(
owner=owner, projects=[{"environments": [{"name": "dev"}, {"name": "prod"}]}]
)
project = Projects.get(owner, workspace, workspace.projects[0].id)
assert len(project.environments) == 2
def test_can_only_update_name_and_description():
owner = UserFactory.create()
workspace = WorkspaceFactory.create(
owner=owner,
projects=[
{
"name": "Project 1",
"description": "a project",
"environments": [{"name": "dev"}],
}
],
)
project = Projects.get(owner, workspace, workspace.projects[0].id)
env_name = project.environments[0].name
Projects.update(
owner,
workspace,
project,
{
"name": "New Name",
"description": "a new project",
"environment_name": "prod",
},
)
assert project.name == "New Name"
assert project.description == "a new project"
assert len(project.environments) == 1
assert project.environments[0].name == env_name

View File

@ -25,7 +25,7 @@ def test_monthly_totals():
monthly = Reports.monthly_totals(workspace)
assert not monthly["environments"]
assert not monthly["projects"]
assert not monthly["applications"]
assert not monthly["workspace"]

View File

@ -4,7 +4,7 @@ from uuid import uuid4
from atst.domain.exceptions import NotFoundError, UnauthorizedError
from atst.domain.workspaces import Workspaces, WorkspaceError
from atst.domain.workspace_roles import WorkspaceRoles
from atst.domain.projects import Projects
from atst.domain.applications import Applications
from atst.domain.environments import Environments
from atst.models.workspace_role import Status as WorkspaceRoleStatus
@ -69,16 +69,16 @@ def test_workspaces_get_ensures_user_is_in_workspace(workspace, workspace_owner)
Workspaces.get(outside_user, workspace.id)
def test_get_for_update_projects_allows_owner(workspace, workspace_owner):
Workspaces.get_for_update_projects(workspace_owner, workspace.id)
def test_get_for_update_applications_allows_owner(workspace, workspace_owner):
Workspaces.get_for_update_applications(workspace_owner, workspace.id)
def test_get_for_update_projects_blocks_developer(workspace):
def test_get_for_update_applications_blocks_developer(workspace):
developer = UserFactory.create()
WorkspaceRoles.add(developer, workspace.id, "developer")
with pytest.raises(UnauthorizedError):
Workspaces.get_for_update_projects(developer, workspace.id)
Workspaces.get_for_update_applications(developer, workspace.id)
def test_can_create_workspace_role(workspace, workspace_owner):
@ -183,45 +183,45 @@ def test_random_user_cannot_view_workspace_members(workspace):
workspace = Workspaces.get_with_members(developer, workspace.id)
def test_scoped_workspace_only_returns_a_users_projects_and_environments(
def test_scoped_workspace_only_returns_a_users_applications_and_environments(
workspace, workspace_owner
):
new_project = Projects.create(
new_application = Applications.create(
workspace_owner,
workspace,
"My Project",
"My project",
"My Application",
"My application",
["dev", "staging", "prod"],
)
Projects.create(
Applications.create(
workspace_owner,
workspace,
"My Project 2",
"My project 2",
"My Application 2",
"My application 2",
["dev", "staging", "prod"],
)
developer = UserFactory.from_atat_role("developer")
dev_environment = Environments.add_member(
new_project.environments[0], developer, "developer"
new_application.environments[0], developer, "developer"
)
scoped_workspace = Workspaces.get(developer, workspace.id)
# Should only return the project and environment in which the user has an
# Should only return the application and environment in which the user has an
# environment role.
assert scoped_workspace.projects == [new_project]
assert scoped_workspace.projects[0].environments == [dev_environment]
assert scoped_workspace.applications == [new_application]
assert scoped_workspace.applications[0].environments == [dev_environment]
def test_scoped_workspace_returns_all_projects_for_workspace_admin(
def test_scoped_workspace_returns_all_applications_for_workspace_admin(
workspace, workspace_owner
):
for _ in range(5):
Projects.create(
Applications.create(
workspace_owner,
workspace,
"My Project",
"My project",
"My Application",
"My application",
["dev", "staging", "prod"],
)
@ -231,26 +231,26 @@ def test_scoped_workspace_returns_all_projects_for_workspace_admin(
)
scoped_workspace = Workspaces.get(admin, workspace.id)
assert len(scoped_workspace.projects) == 5
assert len(scoped_workspace.projects[0].environments) == 3
assert len(scoped_workspace.applications) == 5
assert len(scoped_workspace.applications[0].environments) == 3
def test_scoped_workspace_returns_all_projects_for_workspace_owner(
def test_scoped_workspace_returns_all_applications_for_workspace_owner(
workspace, workspace_owner
):
for _ in range(5):
Projects.create(
Applications.create(
workspace_owner,
workspace,
"My Project",
"My project",
"My Application",
"My application",
["dev", "staging", "prod"],
)
scoped_workspace = Workspaces.get(workspace_owner, workspace.id)
assert len(scoped_workspace.projects) == 5
assert len(scoped_workspace.projects[0].environments) == 3
assert len(scoped_workspace.applications) == 5
assert len(scoped_workspace.applications[0].environments) == 3
def test_for_user_returns_active_workspaces_for_user(workspace, workspace_owner):

View File

@ -12,7 +12,7 @@ from atst.models.request_revision import RequestRevision
from atst.models.request_review import RequestReview
from atst.models.request_status_event import RequestStatusEvent, RequestStatus
from atst.models.pe_number import PENumber
from atst.models.project import Project
from atst.models.application import Application
from atst.models.legacy_task_order import LegacyTaskOrder, Source, FundingType
from atst.models.task_order import TaskOrder
from atst.models.user import User
@ -260,14 +260,15 @@ class WorkspaceFactory(Base):
@classmethod
def _create(cls, model_class, *args, **kwargs):
with_projects = kwargs.pop("projects", [])
with_applications = kwargs.pop("applications", [])
owner = kwargs.pop("owner", UserFactory.create())
members = kwargs.pop("members", [])
workspace = super()._create(model_class, *args, **kwargs)
projects = [
ProjectFactory.create(workspace=workspace, **p) for p in with_projects
applications = [
ApplicationFactory.create(workspace=workspace, **p)
for p in with_applications
]
workspace.request.creator = owner
@ -288,29 +289,30 @@ class WorkspaceFactory(Base):
status=WorkspaceRoleStatus.ACTIVE,
)
workspace.projects = projects
workspace.applications = applications
return workspace
class ProjectFactory(Base):
class ApplicationFactory(Base):
class Meta:
model = Project
model = Application
workspace = factory.SubFactory(WorkspaceFactory)
name = factory.Faker("name")
description = "A test project"
description = "A test application"
@classmethod
def _create(cls, model_class, *args, **kwargs):
with_environments = kwargs.pop("environments", [])
project = super()._create(model_class, *args, **kwargs)
application = super()._create(model_class, *args, **kwargs)
environments = [
EnvironmentFactory.create(project=project, **e) for e in with_environments
EnvironmentFactory.create(application=application, **e)
for e in with_environments
]
project.environments = environments
return project
application.environments = environments
return application
class EnvironmentFactory(Base):

View File

@ -72,7 +72,7 @@ class TestDetailsOfUseForm:
request_form = self._make_form(data)
assert request_form.validate()
def test_sessions_required_for_large_projects(self):
def test_sessions_required_for_large_applications(self):
data = {**self.form_data, **self.migration_data}
data["estimated_monthly_spend"] = "9999999"
del data["number_user_sessions"]

View File

@ -1,6 +1,6 @@
from atst.domain.environments import Environments
from atst.domain.workspaces import Workspaces
from atst.domain.projects import Projects
from atst.domain.applications import Applications
from tests.factories import RequestFactory, UserFactory
@ -9,10 +9,14 @@ def test_add_user_to_environment():
developer = UserFactory.from_atat_role("developer")
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
project = Projects.create(
owner, workspace, "my test project", "It's mine.", ["dev", "staging", "prod"]
application = Applications.create(
owner,
workspace,
"my test application",
"It's mine.",
["dev", "staging", "prod"],
)
dev_environment = project.environments[0]
dev_environment = application.environments[0]
dev_environment = Environments.add_member(dev_environment, developer, "developer")
assert developer in dev_environment.users

View File

@ -2,7 +2,7 @@ import datetime
from atst.domain.environments import Environments
from atst.domain.workspaces import Workspaces
from atst.domain.projects import Projects
from atst.domain.applications import Applications
from atst.models.workspace_role import Status
from atst.models.role import Role
from atst.models.invitation import Status as InvitationStatus
@ -15,7 +15,7 @@ from tests.factories import (
WorkspaceRoleFactory,
EnvironmentFactory,
EnvironmentRoleFactory,
ProjectFactory,
ApplicationFactory,
WorkspaceFactory,
)
from atst.domain.workspace_roles import WorkspaceRoles
@ -90,8 +90,10 @@ def test_has_no_env_role_history(session):
owner = UserFactory.create()
user = UserFactory.create()
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
project = ProjectFactory.create(workspace=workspace)
environment = EnvironmentFactory.create(project=project, name="new environment!")
application = ApplicationFactory.create(workspace=workspace)
environment = EnvironmentFactory.create(
application=application, name="new environment!"
)
env_role = EnvironmentRoleFactory.create(
user=user, environment=environment, role="developer"
@ -110,8 +112,10 @@ def test_has_env_role_history(session):
user = UserFactory.create()
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
workspace_role = WorkspaceRoleFactory.create(workspace=workspace, user=user)
project = ProjectFactory.create(workspace=workspace)
environment = EnvironmentFactory.create(project=project, name="new environment!")
application = ApplicationFactory.create(workspace=workspace)
environment = EnvironmentFactory.create(
application=application, name="new environment!"
)
env_role = EnvironmentRoleFactory.create(
user=user, environment=environment, role="developer"
@ -168,10 +172,16 @@ def test_has_environment_roles():
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
workspace_role = Workspaces.create_member(owner, workspace, developer_data)
project = Projects.create(
owner, workspace, "my test project", "It's mine.", ["dev", "staging", "prod"]
application = Applications.create(
owner,
workspace,
"my test application",
"It's mine.",
["dev", "staging", "prod"],
)
Environments.add_member(
application.environments[0], workspace_role.user, "developer"
)
Environments.add_member(project.environments[0], workspace_role.user, "developer")
assert workspace_role.has_environment_roles
@ -260,9 +270,9 @@ def test_can_resend_invitation_if_expired():
def test_can_list_all_environments():
workspace = WorkspaceFactory.create(
projects=[
applications=[
{
"name": "project1",
"name": "application1",
"environments": [
{"name": "dev"},
{"name": "staging"},
@ -270,7 +280,7 @@ def test_can_list_all_environments():
],
},
{
"name": "project2",
"name": "application2",
"environments": [
{"name": "dev"},
{"name": "staging"},
@ -278,7 +288,7 @@ def test_can_list_all_environments():
],
},
{
"name": "project3",
"name": "application3",
"environments": [
{"name": "dev"},
{"name": "staging"},

View File

@ -61,7 +61,7 @@ def test_non_owner_user_with_no_workspaces_redirected_to_requests(client, user_s
assert "/requests" in response.location
def test_non_owner_user_with_one_workspace_redirected_to_workspace_projects(
def test_non_owner_user_with_one_workspace_redirected_to_workspace_applications(
client, user_session
):
user = UserFactory.create()
@ -71,7 +71,7 @@ def test_non_owner_user_with_one_workspace_redirected_to_workspace_projects(
user_session(user)
response = client.get("/home", follow_redirects=False)
assert "/workspaces/{}/projects".format(workspace.id) in response.location
assert "/workspaces/{}/applications".format(workspace.id) in response.location
def test_non_owner_user_with_mulitple_workspaces_redirected_to_workspaces(

View File

@ -6,10 +6,10 @@ from tests.factories import (
WorkspaceRoleFactory,
EnvironmentRoleFactory,
EnvironmentFactory,
ProjectFactory,
ApplicationFactory,
)
from atst.domain.projects import Projects
from atst.domain.applications import Applications
from atst.domain.workspaces import Workspaces
from atst.domain.roles import Roles
from atst.models.workspace_role import Status as WorkspaceRoleStatus
@ -18,7 +18,7 @@ from atst.models.workspace_role import Status as WorkspaceRoleStatus
def test_user_with_permission_has_budget_report_link(client, user_session):
workspace = WorkspaceFactory.create()
user_session(workspace.owner)
response = client.get("/workspaces/{}/projects".format(workspace.id))
response = client.get("/workspaces/{}/applications".format(workspace.id))
assert (
'href="/workspaces/{}/reports"'.format(workspace.id).encode() in response.data
)
@ -31,7 +31,7 @@ def test_user_without_permission_has_no_budget_report_link(client, user_session)
user, workspace, "developer", status=WorkspaceRoleStatus.ACTIVE
)
user_session(user)
response = client.get("/workspaces/{}/projects".format(workspace.id))
response = client.get("/workspaces/{}/applications".format(workspace.id))
assert (
'href="/workspaces/{}/reports"'.format(workspace.id).encode()
not in response.data
@ -50,20 +50,20 @@ def test_user_with_permission_has_activity_log_link(client, user_session):
)
user_session(workspace.owner)
response = client.get("/workspaces/{}/projects".format(workspace.id))
response = client.get("/workspaces/{}/applications".format(workspace.id))
assert (
'href="/workspaces/{}/activity"'.format(workspace.id).encode() in response.data
)
# logs out previous user before creating a new session
user_session(admin)
response = client.get("/workspaces/{}/projects".format(workspace.id))
response = client.get("/workspaces/{}/applications".format(workspace.id))
assert (
'href="/workspaces/{}/activity"'.format(workspace.id).encode() in response.data
)
user_session(ccpo)
response = client.get("/workspaces/{}/projects".format(workspace.id))
response = client.get("/workspaces/{}/applications".format(workspace.id))
assert (
'href="/workspaces/{}/activity"'.format(workspace.id).encode() in response.data
)
@ -80,116 +80,119 @@ def test_user_without_permission_has_no_activity_log_link(client, user_session):
)
user_session(developer)
response = client.get("/workspaces/{}/projects".format(workspace.id))
response = client.get("/workspaces/{}/applications".format(workspace.id))
assert (
'href="/workspaces/{}/activity"'.format(workspace.id).encode()
not in response.data
)
def test_user_with_permission_has_add_project_link(client, user_session):
def test_user_with_permission_has_add_application_link(client, user_session):
workspace = WorkspaceFactory.create()
user_session(workspace.owner)
response = client.get("/workspaces/{}/projects".format(workspace.id))
response = client.get("/workspaces/{}/applications".format(workspace.id))
assert (
'href="/workspaces/{}/projects/new"'.format(workspace.id).encode()
'href="/workspaces/{}/applications/new"'.format(workspace.id).encode()
in response.data
)
def test_user_without_permission_has_no_add_project_link(client, user_session):
def test_user_without_permission_has_no_add_application_link(client, user_session):
user = UserFactory.create()
workspace = WorkspaceFactory.create()
Workspaces._create_workspace_role(user, workspace, "developer")
user_session(user)
response = client.get("/workspaces/{}/projects".format(workspace.id))
response = client.get("/workspaces/{}/applications".format(workspace.id))
assert (
'href="/workspaces/{}/projects/new"'.format(workspace.id).encode()
'href="/workspaces/{}/applications/new"'.format(workspace.id).encode()
not in response.data
)
def test_view_edit_project(client, user_session):
def test_view_edit_application(client, user_session):
workspace = WorkspaceFactory.create()
project = Projects.create(
application = Applications.create(
workspace.owner,
workspace,
"Snazzy Project",
"A new project for me and my friends",
"Snazzy Application",
"A new application for me and my friends",
{"env1", "env2"},
)
user_session(workspace.owner)
response = client.get(
"/workspaces/{}/projects/{}/edit".format(workspace.id, project.id)
"/workspaces/{}/applications/{}/edit".format(workspace.id, application.id)
)
assert response.status_code == 200
def test_user_with_permission_can_update_project(client, user_session):
def test_user_with_permission_can_update_application(client, user_session):
owner = UserFactory.create()
workspace = WorkspaceFactory.create(
owner=owner,
projects=[
applications=[
{
"name": "Awesome Project",
"name": "Awesome Application",
"description": "It's really awesome!",
"environments": [{"name": "dev"}, {"name": "prod"}],
}
],
)
project = workspace.projects[0]
application = workspace.applications[0]
user_session(owner)
response = client.post(
url_for(
"workspaces.update_project",
"workspaces.update_application",
workspace_id=workspace.id,
project_id=project.id,
application_id=application.id,
),
data={"name": "Really Cool Project", "description": "A very cool project."},
data={
"name": "Really Cool Application",
"description": "A very cool application.",
},
follow_redirects=True,
)
assert response.status_code == 200
assert project.name == "Really Cool Project"
assert project.description == "A very cool project."
assert application.name == "Really Cool Application"
assert application.description == "A very cool application."
def test_user_without_permission_cannot_update_project(client, user_session):
def test_user_without_permission_cannot_update_application(client, user_session):
dev = UserFactory.create()
owner = UserFactory.create()
workspace = WorkspaceFactory.create(
owner=owner,
members=[{"user": dev, "role_name": "developer"}],
projects=[
applications=[
{
"name": "Great Project",
"name": "Great Application",
"description": "Cool stuff happening here!",
"environments": [{"name": "dev"}, {"name": "prod"}],
}
],
)
project = workspace.projects[0]
application = workspace.applications[0]
user_session(dev)
response = client.post(
url_for(
"workspaces.update_project",
"workspaces.update_application",
workspace_id=workspace.id,
project_id=project.id,
application_id=application.id,
),
data={"name": "New Name", "description": "A new description."},
follow_redirects=True,
)
assert response.status_code == 404
assert project.name == "Great Project"
assert project.description == "Cool stuff happening here!"
assert application.name == "Great Application"
assert application.description == "Cool stuff happening here!"
def create_environment(user):
workspace = WorkspaceFactory.create()
workspace_role = WorkspaceRoleFactory.create(workspace=workspace, user=user)
project = ProjectFactory.create(workspace=workspace)
return EnvironmentFactory.create(project=project, name="new environment!")
application = ApplicationFactory.create(workspace=workspace)
return EnvironmentFactory.create(application=application, name="new environment!")
def test_environment_access_with_env_role(client, user_session):

View File

@ -99,7 +99,7 @@ def test_user_who_has_not_accepted_workspace_invite_cannot_view(client, user_ses
# user tries to view workspace before accepting invitation
user_session(user)
response = client.get("/workspaces/{}/projects".format(workspace.id))
response = client.get("/workspaces/{}/applications".format(workspace.id))
assert response.status_code == 404

View File

@ -8,7 +8,7 @@ from tests.factories import (
)
from atst.domain.workspaces import Workspaces
from atst.domain.workspace_roles import WorkspaceRoles
from atst.domain.projects import Projects
from atst.domain.applications import Applications
from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles
from atst.queue import queue
@ -144,16 +144,16 @@ def test_update_member_environment_role(client, user_session):
workspace = WorkspaceFactory.create()
user = UserFactory.create()
member = WorkspaceRoles.add(user, workspace.id, "developer")
project = Projects.create(
application = Applications.create(
workspace.owner,
workspace,
"Snazzy Project",
"A new project for me and my friends",
"Snazzy Application",
"A new application for me and my friends",
{"env1", "env2"},
)
env1_id = project.environments[0].id
env2_id = project.environments[1].id
for env in project.environments:
env1_id = application.environments[0].id
env2_id = application.environments[1].id
for env in application.environments:
Environments.add_member(env, user, "developer")
user_session(workspace.owner)
response = client.post(
@ -178,15 +178,15 @@ def test_update_member_environment_role_with_no_data(client, user_session):
workspace = WorkspaceFactory.create()
user = UserFactory.create()
member = WorkspaceRoles.add(user, workspace.id, "developer")
project = Projects.create(
application = Applications.create(
workspace.owner,
workspace,
"Snazzy Project",
"A new project for me and my friends",
"Snazzy Application",
"A new application for me and my friends",
{"env1"},
)
env1_id = project.environments[0].id
for env in project.environments:
env1_id = application.environments[0].id
for env in application.environments:
Environments.add_member(env, user, "developer")
user_session(workspace.owner)
response = client.post(
@ -207,11 +207,11 @@ def test_revoke_active_member_access(client, user_session):
member = WorkspaceRoleFactory.create(
workspace=workspace, user=user, status=WorkspaceRoleStatus.ACTIVE
)
Projects.create(
Applications.create(
workspace.owner,
workspace,
"Snazzy Project",
"A new project for me and my friends",
"Snazzy Application",
"A new application for me and my friends",
{"env1"},
)
user_session(workspace.owner)

View File

@ -75,7 +75,7 @@ forms:
exceptions:
message: Form validation failed.
financial:
ba_code_description: 'BA Code is used to identify the purposes, projects, or types of activities financed by the appropriation fund. <br/><em>It should be two digits, followed by an optional letter.</em>'
ba_code_description: 'BA Code is used to identify the purposes, applications, or types of activities financed by the appropriation fund. <br/><em>It should be two digits, followed by an optional letter.</em>'
ba_code_label: Program Budget Activity (BA) Code
clin_0001_description: 'Review your task order document, the amounts for each CLIN must match exactly here'
clin_0001_label: <dl><dt>CLIN 0001</dt> - <dd>Unclassified IaaS and PaaS Amount</dd></dl>
@ -119,7 +119,7 @@ forms:
email_label: Email Address
first_name_label: First Name
last_name_label: Last Name
workspace_role_description: 'The portfolio role controls whether a member is permitted to organize a portfolio into projects and environments, add members to this portfolio, and view billing information.'
workspace_role_description: 'The portfolio role controls whether a member is permitted to organize a portfolio into applications and environments, add members to this portfolio, and view billing information.'
workspace_role_label: Portfolio Role
new_request:
am_poc_label: I am the Portfolio Owner
@ -157,12 +157,12 @@ forms:
start_date_date_range_validation_message: Must be a date in the future.
start_date_label: When do you expect to start using the JEDI Cloud (not for billing purposes)?
technical_support_team_description: Are you working with a technical support team experienced in cloud migrations?
project:
application:
description_label: Description
environment_names_label: Environment Name
environment_names_required_validation_message: Provide at least one environment name.
environment_names_unique_validation_message: Environment names must be unique.
name_label: Project Name
name_label: Application Name
task_order:
portfolio_name_label: Organization Portfolio Name
portfolio_name_description: The name of your office or organization. You can add multiple applications to your portfolio. Your task orders are used to pay for these applications and their environments.
@ -212,10 +212,10 @@ forms:
name_label: Portfolio Name
name_length_validation_message: Portfolio names must be at least 4 and not more than 50 characters
fragments:
edit_project_form:
existing_project_title: 'Edit {project_name} project'
explain: 'AT-AT allows you to organize your portfolio into multiple projects, each of which may have environments.'
new_project_title: Add a new project
edit_application_form:
existing_application_title: 'Edit {application_name} application'
explain: 'AT-AT allows you to organize your portfolio into multiple applications, each of which may have environments.'
new_application_title: Add a new application
edit_user_form:
date_last_training_tooltip: When was the last time you completed the IA training? <br> Information Assurance (IA) training is an important step in cyber awareness.
save_details_button: Save Details
@ -225,7 +225,7 @@ fragments:
paragraph_2: 'While your request is being reviewed, your next step is to create a Task Order (TO) associated with the JEDI Cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.'
pending_ccpo_approval_modal:
paragraph_1: The CCPO will review and respond to your Financial Verification submission in 3 business days. You will be notified via email or phone.
paragraph_2: Once the financial verification is approved you will be invited to create your JEDI Portfolio and set-up your projects. Click here for more details.
paragraph_2: Once the financial verification is approved you will be invited to create your JEDI Portfolio and set-up your applications. Click here for more details.
pending_financial_verification:
learn_more_link_text: Learn more about the JEDI Cloud Task Order and the Financial Verification process.
paragraph_1: 'The next step is to create a Task Order associated with JEDI Cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.'
@ -249,11 +249,11 @@ navigation:
request_workspace_link_text: Request a new JEDI Portfolio
workspace_navigation:
add_new_member_label: Add New Member
add_new_project_label: Add New Project
add_new_application_label: Add New Application
budget_report: Budget Report
activity_log: Activity Log
members: Members
projects: Projects
applications: Applications
task_orders: Task Orders
workspace_settings: Portfolio Settings
requests:
@ -309,7 +309,7 @@ requests:
no_requests_found: No requests found.
no_workspaces_action_label: Create a new JEDI Cloud Request
no_workspaces_label: You currently have no JEDI Cloud portfolios.
no_workspaces_sub_message: A JEDI Cloud Portfolio is where you manage your projects and control user access to those projects.
no_workspaces_sub_message: A JEDI Cloud Portfolio is where you manage your applications and control user access to those applications.
pending_ccpo_action: Pending CCPO Action
request_submitted_title: Request submitted!
requests_in_progress: Requests in progress