Merge pull request #1098 from dod-ccpo/add-members-to-app-provisioning-flow
Add members to app provisioning flow
This commit is contained in:
commit
3aa5895c93
@ -3,7 +3,7 @@
|
|||||||
"files": "^.secrets.baseline$",
|
"files": "^.secrets.baseline$",
|
||||||
"lines": null
|
"lines": null
|
||||||
},
|
},
|
||||||
"generated_at": "2019-09-26T13:53:31Z",
|
"generated_at": "2019-09-30T13:51:34Z",
|
||||||
"plugins_used": [
|
"plugins_used": [
|
||||||
{
|
{
|
||||||
"base64_limit": 4.5,
|
"base64_limit": 4.5,
|
||||||
@ -199,5 +199,5 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": "0.12.6"
|
"version": "0.12.5"
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,11 @@ from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm
|
|||||||
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
||||||
from atst.models.permissions import Permissions
|
from atst.models.permissions import Permissions
|
||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
|
from atst.routes.applications.settings import (
|
||||||
|
get_members_data,
|
||||||
|
get_new_member_form,
|
||||||
|
handle_create_member,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_new_application_form(form_data, form_class, application_id=None):
|
def get_new_application_form(form_data, form_class, application_id=None):
|
||||||
@ -24,6 +29,7 @@ def render_new_application_form(
|
|||||||
if application_id:
|
if application_id:
|
||||||
application = Applications.get(application_id)
|
application = Applications.get(application_id)
|
||||||
render_args["form"] = form or form_class(obj=application)
|
render_args["form"] = form or form_class(obj=application)
|
||||||
|
render_args["application"] = application
|
||||||
else:
|
else:
|
||||||
render_args["form"] = form or form_class()
|
render_args["form"] = form or form_class()
|
||||||
|
|
||||||
@ -93,12 +99,19 @@ def create_or_update_new_application_step_1(portfolio_id, application_id=None):
|
|||||||
)
|
)
|
||||||
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
|
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
|
||||||
def view_new_application_step_2(portfolio_id, application_id):
|
def view_new_application_step_2(portfolio_id, application_id):
|
||||||
return render_new_application_form(
|
application = Applications.get(application_id)
|
||||||
"applications/new/step_2.html",
|
render_args = {
|
||||||
EnvironmentsForm,
|
"form": EnvironmentsForm(
|
||||||
portfolio_id=portfolio_id,
|
data={
|
||||||
application_id=application_id,
|
"environment_names": [
|
||||||
)
|
environment.name for environment in application.environments
|
||||||
|
]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"application": application,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template("applications/new/step_2.html", **render_args)
|
||||||
|
|
||||||
|
|
||||||
@applications_bp.route(
|
@applications_bp.route(
|
||||||
@ -112,11 +125,11 @@ def update_new_application_step_2(portfolio_id, application_id):
|
|||||||
if form.validate():
|
if form.validate():
|
||||||
application = Applications.get(application_id)
|
application = Applications.get(application_id)
|
||||||
application = Applications.update(application, form.data)
|
application = Applications.update(application, form.data)
|
||||||
flash("application_created", application_name=application.name)
|
flash("application_environments_updated")
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"applications.portfolio_applications",
|
"applications.update_new_application_step_3",
|
||||||
portfolio_id=application.portfolio_id,
|
application_id=application_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -130,3 +143,32 @@ def update_new_application_step_2(portfolio_id, application_id):
|
|||||||
),
|
),
|
||||||
400,
|
400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@applications_bp.route("/applications/<application_id>/step_3")
|
||||||
|
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
|
||||||
|
def view_new_application_step_3(application_id):
|
||||||
|
application = Applications.get(application_id)
|
||||||
|
members = get_members_data(application)
|
||||||
|
new_member_form = get_new_member_form(application)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"applications/new/step_3.html",
|
||||||
|
application_id=application_id,
|
||||||
|
application=application,
|
||||||
|
members=members,
|
||||||
|
new_member_form=new_member_form,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@applications_bp.route("/applications/<application_id>/step_3", methods=["POST"])
|
||||||
|
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
|
||||||
|
def update_new_application_step_3(application_id):
|
||||||
|
|
||||||
|
handle_create_member(application_id, http_request.form)
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
url_for(
|
||||||
|
"applications.view_new_application_step_3", application_id=application_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -73,8 +73,9 @@ def filter_env_roles_form_data(member, environments):
|
|||||||
"environment_name": env.name,
|
"environment_name": env.name,
|
||||||
"role": NO_ACCESS,
|
"role": NO_ACCESS,
|
||||||
}
|
}
|
||||||
env_role = EnvironmentRoles.get_by_user_and_environment(member.user_id, env.id)
|
env_roles_set = set(env.roles).intersection(set(member.environment_roles))
|
||||||
if env_role:
|
if len(env_roles_set) == 1:
|
||||||
|
(env_role,) = env_roles_set
|
||||||
env_data["role"] = env_role.role
|
env_data["role"] = env_role.role
|
||||||
|
|
||||||
env_roles_form_data.append(env_data)
|
env_roles_form_data.append(env_data)
|
||||||
@ -153,6 +154,41 @@ def send_application_invitation(invitee_email, inviter_name, token):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_create_member(application_id, form_data):
|
||||||
|
application = Applications.get(application_id)
|
||||||
|
form = NewMemberForm(form_data)
|
||||||
|
|
||||||
|
if form.validate():
|
||||||
|
try:
|
||||||
|
invite = Applications.invite(
|
||||||
|
application=application,
|
||||||
|
inviter=g.current_user,
|
||||||
|
user_data=form.user_data.data,
|
||||||
|
permission_sets_names=form.data["permission_sets"],
|
||||||
|
environment_roles_data=form.environment_roles.data,
|
||||||
|
)
|
||||||
|
|
||||||
|
send_application_invitation(
|
||||||
|
invitee_email=invite.email,
|
||||||
|
inviter_name=g.current_user.full_name,
|
||||||
|
token=invite.token,
|
||||||
|
)
|
||||||
|
|
||||||
|
flash(
|
||||||
|
"new_application_member",
|
||||||
|
user_name=invite.user_name,
|
||||||
|
application_name=application.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
except AlreadyExistsError:
|
||||||
|
return render_template(
|
||||||
|
"error.html", message="There was an error processing your request."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
# TODO: flash error message
|
||||||
|
|
||||||
|
|
||||||
@applications_bp.route("/applications/<application_id>/settings")
|
@applications_bp.route("/applications/<application_id>/settings")
|
||||||
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
|
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
|
||||||
def settings(application_id):
|
def settings(application_id):
|
||||||
@ -283,39 +319,7 @@ def delete_environment(environment_id):
|
|||||||
Permissions.CREATE_APPLICATION_MEMBER, message="create new application member"
|
Permissions.CREATE_APPLICATION_MEMBER, message="create new application member"
|
||||||
)
|
)
|
||||||
def create_member(application_id):
|
def create_member(application_id):
|
||||||
application = Applications.get(application_id)
|
handle_create_member(application_id, http_request.form)
|
||||||
form = NewMemberForm(http_request.form)
|
|
||||||
|
|
||||||
if form.validate():
|
|
||||||
try:
|
|
||||||
invite = Applications.invite(
|
|
||||||
application=application,
|
|
||||||
inviter=g.current_user,
|
|
||||||
user_data=form.user_data.data,
|
|
||||||
permission_sets_names=form.data["permission_sets"],
|
|
||||||
environment_roles_data=form.environment_roles.data,
|
|
||||||
)
|
|
||||||
|
|
||||||
send_application_invitation(
|
|
||||||
invitee_email=invite.email,
|
|
||||||
inviter_name=g.current_user.full_name,
|
|
||||||
token=invite.token,
|
|
||||||
)
|
|
||||||
|
|
||||||
flash(
|
|
||||||
"new_application_member",
|
|
||||||
user_name=invite.user_name,
|
|
||||||
application_name=application.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
except AlreadyExistsError:
|
|
||||||
return render_template(
|
|
||||||
"error.html", message="There was an error processing your request."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
# TODO: flash error message
|
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"applications.settings",
|
"applications.settings",
|
||||||
|
@ -12,13 +12,15 @@
|
|||||||
{% set action = url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id, application_id=application_id) %}
|
{% set action = url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id, application_id=application_id) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% block portfolio_header %}
|
||||||
|
{% include "portfolios/header.html" %}
|
||||||
|
{{ StickyCTA(text="Name and Describe New Application") }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block application_content %}
|
{% block application_content %}
|
||||||
|
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
<div class='subheading'>{{ 'portfolios.applications.settings_heading' | translate }}</div>
|
|
||||||
|
|
||||||
<application-name-and-description inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
<application-name-and-description inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
||||||
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit">
|
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
@ -42,7 +44,7 @@
|
|||||||
|
|
||||||
<span class="action-group">
|
<span class="action-group">
|
||||||
{% block next_button %}
|
{% block next_button %}
|
||||||
{{ SaveButton(text=('portfolios.applications.next_button_text' | translate)) }}
|
{{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
|
@ -7,14 +7,17 @@
|
|||||||
|
|
||||||
{% set secondary_breadcrumb = 'portfolios.applications.new_application_title' | translate %}
|
{% set secondary_breadcrumb = 'portfolios.applications.new_application_title' | translate %}
|
||||||
|
|
||||||
|
{% block portfolio_header %}
|
||||||
|
{{ StickyCTA(text=application.name) }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block application_content %}
|
{% block application_content %}
|
||||||
|
|
||||||
{% set modalName = "newApplicationConfirmation" %}
|
{% set modalName = "newApplicationConfirmation" %}
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
<div class='subheading'>{{ 'portfolios.applications.settings_heading' | translate }}</div>
|
|
||||||
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
||||||
<form method="POST" action="{{ url_for('applications.view_new_application_step_2', portfolio_id=portfolio.id, application_id=application_id) }}" v-on:submit="handleSubmit">
|
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
@ -63,7 +66,7 @@
|
|||||||
|
|
||||||
<span class="action-group">
|
<span class="action-group">
|
||||||
{% block next_button %}
|
{% block next_button %}
|
||||||
{{ SaveButton(text=('portfolios.applications.create_button_text' | translate)) }}
|
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
|
26
templates/applications/new/step_3.html
Normal file
26
templates/applications/new/step_3.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
{% extends "applications/base.html" %}
|
||||||
|
|
||||||
|
{% from "fragments/members.html" import MemberManagementTemplate %}
|
||||||
|
{% set secondary_breadcrumb = 'portfolios.applications.new_application_title' | translate %}
|
||||||
|
|
||||||
|
{% block portfolio_header %}
|
||||||
|
{% include "portfolios/header.html" %}
|
||||||
|
{{ StickyCTA(text=application.name) }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block application_content %}
|
||||||
|
{{ MemberManagementTemplate(
|
||||||
|
application,
|
||||||
|
members,
|
||||||
|
new_member_form,
|
||||||
|
"applications.update_new_application_step_3",
|
||||||
|
user_can(permissions.CREATE_APPLICATION_MEMBER)) }}
|
||||||
|
|
||||||
|
<span class="action-group">
|
||||||
|
<a class="usa-button" href="{{ url_for('applications.settings', application_id=application_id) }}">
|
||||||
|
Return to Application Settings
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -5,6 +5,7 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% import "applications/fragments/new_member_modal_content.html" as member_steps %}
|
{% import "applications/fragments/new_member_modal_content.html" as member_steps %}
|
||||||
{% from "applications/fragments/member_perms_form_fields.html" import MemberPermsFields %}
|
{% from "applications/fragments/member_perms_form_fields.html" import MemberPermsFields %}
|
||||||
|
{% from "fragments/members.html" import MemberManagementTemplate %}
|
||||||
{% from "components/modal.html" import Modal %}
|
{% from "components/modal.html" import Modal %}
|
||||||
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||||
{% from "components/pagination.html" import Pagination %}
|
{% from "components/pagination.html" import Pagination %}
|
||||||
@ -73,150 +74,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not application.members %}
|
{{ MemberManagementTemplate(
|
||||||
{% set user_can_invite = user_can(permissions.CREATE_APPLICATION_MEMBER) %}
|
application,
|
||||||
|
members,
|
||||||
<div class='empty-state'>
|
new_member_form,
|
||||||
<p class='empty-state__message'>{{ ("portfolios.applications.team_settings.blank_slate.title" | translate) }}</p>
|
"applications.settings",
|
||||||
|
user_can(permissions.CREATE_APPLICATION_MEMBER)) }}
|
||||||
{{ Icon('avatar') }}
|
|
||||||
|
|
||||||
{% if not user_can_invite %}
|
|
||||||
<p class='empty-state__sub-message'>{{ ("portfolios.applications.team_settings.blank_slate.sub_message" | translate) }}</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user_can_invite %}
|
|
||||||
{% set new_member_modal_name = "add-app-mem" %}
|
|
||||||
<a class="usa-button usa-button-big" v-on:click="openModal('{{ new_member_modal_name }}')">
|
|
||||||
{{ "portfolios.applications.team_settings.blank_slate.action_label" | translate }}
|
|
||||||
</a>
|
|
||||||
{{ MultiStepModalForm(
|
|
||||||
name=new_member_modal_name,
|
|
||||||
form=new_member_form,
|
|
||||||
form_action=url_for("applications.create_member", application_id=application.id),
|
|
||||||
steps=[
|
|
||||||
member_steps.MemberStepOne(new_member_form),
|
|
||||||
member_steps.MemberStepTwo(new_member_form, application)
|
|
||||||
],
|
|
||||||
) }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
<div class='subheading'>
|
|
||||||
{{ 'portfolios.applications.settings.team_members' | translate }}
|
|
||||||
|
|
||||||
{% set new_member_modal_name = "add-app-mem" %}
|
|
||||||
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
|
|
||||||
<a class="icon-link modal-link icon-link__add" v-on:click="openModal('{{ new_member_modal_name }}')">
|
|
||||||
{{ Icon("plus") }}
|
|
||||||
{{ "portfolios.applications.add_member" | translate }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="member-list application-list" id="application-members">
|
|
||||||
<div class='responsive-table-wrapper panel'>
|
|
||||||
{% if g.matchesPath("application-members") %}
|
|
||||||
{% include "fragments/flash.html" %}
|
|
||||||
{% endif %}
|
|
||||||
{% for member in members %}
|
|
||||||
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
|
||||||
{% call Modal(modal_name) %}
|
|
||||||
<div class="modal__form--header">
|
|
||||||
<h1>{{ Icon('avatar') }} {{ member.user_name }}</h1>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<base-form inline-template>
|
|
||||||
<form id='{{ modal_name }}' method="POST" action="{{ url_for('applications.update_member', application_id=application.id, application_role_id=member.role_id) }}">
|
|
||||||
{{ member.form.csrf_token }}
|
|
||||||
{{ MemberPermsFields(form=member.form, member_role_id=member.role_id) }}
|
|
||||||
<div class="action-group">
|
|
||||||
{{ SaveButton(text='Update', element='input', additional_classes='action-group__action') }}
|
|
||||||
<a class='action-group__action usa-button usa-button-secondary' v-on:click="closeModal('{{ modal_name }}')">{{ "common.cancel" | translate }}</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</base-form>
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) and member.role_status == 'pending' %}
|
|
||||||
{% set revoke_invite_modal = "revoke_invite_{}".format(member.role_id) %}
|
|
||||||
{% call Modal(name=revoke_invite_modal, dismissable=True) %}
|
|
||||||
<div class="task-order__modal-cancel">
|
|
||||||
<form method="post" action="{{ url_for('applications.revoke_invite', application_id=application.id, application_role_id=member.role_id) }}">
|
|
||||||
{{ member.form.csrf_token }}
|
|
||||||
<h1>{{ "invites.revoke.modal_heading" | translate({'user_name': member.user_name}) }}</h1>
|
|
||||||
<div class="task-order__modal-cancel_buttons">
|
|
||||||
<button class="usa-button usa-button-primary" type="submit">{{ "invites.revoke.submit" | translate }}</button>
|
|
||||||
<button type='button' v-on:click='closeModal("{{revoke_invite_modal}}")' class="usa-button usa-button-primary">{{ "invites.revoke.cancel" | translate }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endcall %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Member</th>
|
|
||||||
<th>Project Permissions</th>
|
|
||||||
<th>Environment Access</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for member in members %}
|
|
||||||
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{{ member.user_name }}
|
|
||||||
<a class="icon-link" v-on:click="openModal('{{ modal_name }}')">
|
|
||||||
{{ Icon('edit') }}
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
{% if member.role_status == 'pending' %}
|
|
||||||
<span class='label label--purple'>INVITE PENDING</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{% for perm, value in member.permission_sets.items() %}
|
|
||||||
{{ ("portfolios.applications.members.{}.{}".format(perm, value)) | translate }}<br>
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% for env in member.environment_roles %}
|
|
||||||
{{ env.environment_name }}{% if not env == member.environment_roles[-1]%},{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) and member.role_status == 'pending' %}
|
|
||||||
{% set revoke_invite_modal = "revoke_invite_{}".format(member.role_id) %}
|
|
||||||
<a href="#">Resend Invite</a><br>
|
|
||||||
<a v-on:click='openModal("{{ revoke_invite_modal }}")'>{{ 'invites.revoke.button' | translate }}</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
|
|
||||||
{% import "applications/fragments/new_member_modal_content.html" as member_steps %}
|
|
||||||
{{ MultiStepModalForm(
|
|
||||||
name=new_member_modal_name,
|
|
||||||
form=new_member_form,
|
|
||||||
form_action=url_for("applications.create_member", application_id=application.id),
|
|
||||||
steps=[
|
|
||||||
member_steps.MemberStepOne(new_member_form),
|
|
||||||
member_steps.MemberStepTwo(new_member_form, application)
|
|
||||||
],
|
|
||||||
) }}
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class='subheading'>
|
<div class='subheading'>
|
||||||
{{ 'common.resource_names.environments' | translate }}
|
{{ 'common.resource_names.environments' | translate }}
|
||||||
|
143
templates/fragments/members.html
Normal file
143
templates/fragments/members.html
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
{% from "components/alert.html" import Alert %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
{% import "applications/fragments/new_member_modal_content.html" as member_steps %}
|
||||||
|
{% from "applications/fragments/member_perms_form_fields.html" import MemberPermsFields %}
|
||||||
|
{% from "components/modal.html" import Modal %}
|
||||||
|
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||||
|
{% from "components/save_button.html" import SaveButton %}
|
||||||
|
|
||||||
|
{% macro MemberManagementTemplate(
|
||||||
|
application,
|
||||||
|
members,
|
||||||
|
new_member_form,
|
||||||
|
action,
|
||||||
|
user_can_create_app_member=False
|
||||||
|
) %}
|
||||||
|
|
||||||
|
|
||||||
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
|
{% if not application.members %}
|
||||||
|
<div class='empty-state'>
|
||||||
|
<p class='empty-state__message'>{{ ("portfolios.applications.team_settings.blank_slate.title" | translate) }}</p>
|
||||||
|
|
||||||
|
{{ Icon('avatar') }}
|
||||||
|
|
||||||
|
{% if not user_can_create_app_member %}
|
||||||
|
<p class='empty-state__sub-message'>{{ ("portfolios.applications.team_settings.blank_slate.sub_message" | translate) }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_can_create_app_member %}
|
||||||
|
{% set new_member_modal_name = "add-app-mem" %}
|
||||||
|
<a class="usa-button usa-button-big" v-on:click="openModal('{{ new_member_modal_name }}')">
|
||||||
|
{{ "portfolios.applications.team_settings.blank_slate.action_label" | translate }}
|
||||||
|
</a>
|
||||||
|
{{ MultiStepModalForm(
|
||||||
|
name=new_member_modal_name,
|
||||||
|
form=new_member_form,
|
||||||
|
form_action=url_for(action, application_id=application.id),
|
||||||
|
steps=[
|
||||||
|
member_steps.MemberStepOne(new_member_form),
|
||||||
|
member_steps.MemberStepTwo(new_member_form, application)
|
||||||
|
],
|
||||||
|
) }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class='subheading'>
|
||||||
|
{{ 'portfolios.applications.settings.team_members' | translate }}
|
||||||
|
|
||||||
|
{% set new_member_modal_name = "add-app-mem" %}
|
||||||
|
{% if user_can_create_app_member %}
|
||||||
|
<a class="icon-link modal-link icon-link__add" v-on:click="openModal('{{ new_member_modal_name }}')">
|
||||||
|
{{ Icon("plus") }}
|
||||||
|
{{ "portfolios.applications.add_member" | translate }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="member-list application-list" id="application-members">
|
||||||
|
<div class='responsive-table-wrapper panel'>
|
||||||
|
{% for member in members %}
|
||||||
|
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
||||||
|
{% call Modal(modal_name) %}
|
||||||
|
<div class="modal__form--header">
|
||||||
|
<h1>{{ Icon('avatar') }} {{ member.user_name }}</h1>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<base-form inline-template>
|
||||||
|
<form id='{{ modal_name }}' method="POST" action="{{ url_for('applications.update_member', application_id=application.id, application_role_id=member.role_id) }}">
|
||||||
|
{{ member.form.csrf_token }}
|
||||||
|
{{ MemberPermsFields(form=member.form, member_role_id=member.role_id) }}
|
||||||
|
<div class="action-group">
|
||||||
|
{{ SaveButton(text='Update', element='input', additional_classes='action-group__action') }}
|
||||||
|
<a class='action-group__action usa-button usa-button-secondary' v-on:click="closeModal('{{ modal_name }}')">{{ "common.cancel" | translate }}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</base-form>
|
||||||
|
{% endcall %}
|
||||||
|
{% endfor %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Member</th>
|
||||||
|
<th>Project Permissions</th>
|
||||||
|
<th>Environment Access</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for member in members %}
|
||||||
|
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ member.user_name }}
|
||||||
|
<a class="icon-link" v-on:click="openModal('{{ modal_name }}')">
|
||||||
|
{{ Icon('edit') }}
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
{% if member.role_status == 'pending' %}
|
||||||
|
<span class='label label--purple'>INVITE PENDING</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{% for perm, value in member.permission_sets.items() %}
|
||||||
|
{{ ("portfolios.applications.members.{}.{}".format(perm, value)) | translate }}<br>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for env in member.environment_roles %}
|
||||||
|
{{ env.environment_name }}{% if not env == member.environment_roles[-1]%},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if member.role_status == 'pending' %}
|
||||||
|
<a href="#">Resend Invite</a><br>
|
||||||
|
<a href="#">Revoke Invite</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user_can_create_app_member %}
|
||||||
|
{% import "applications/fragments/new_member_modal_content.html" as member_steps %}
|
||||||
|
{{ MultiStepModalForm(
|
||||||
|
name=new_member_modal_name,
|
||||||
|
form=new_member_form,
|
||||||
|
form_action=url_for(action, application_id=application.id),
|
||||||
|
steps=[
|
||||||
|
member_steps.MemberStepOne(new_member_form),
|
||||||
|
member_steps.MemberStepTwo(new_member_form, application)
|
||||||
|
],
|
||||||
|
) }}
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endmacro %}
|
@ -1,7 +1,9 @@
|
|||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from tests.factories import PortfolioFactory, ApplicationFactory
|
from tests.factories import PortfolioFactory, ApplicationFactory, UserFactory
|
||||||
from atst.domain.applications import Applications
|
from unittest.mock import Mock
|
||||||
|
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
|
||||||
|
from atst.models.application_invitation import ApplicationInvitation
|
||||||
|
|
||||||
|
|
||||||
def test_get_name_and_description_form(client, user_session):
|
def test_get_name_and_description_form(client, user_session):
|
||||||
@ -94,3 +96,66 @@ def test_post_environments(client, session, user_session):
|
|||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
session.refresh(application)
|
session.refresh(application)
|
||||||
assert len(application.environments) == 3
|
assert len(application.environments) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_members(client, session, user_session):
|
||||||
|
application = ApplicationFactory.create()
|
||||||
|
user_session(application.portfolio.owner)
|
||||||
|
response = client.get(
|
||||||
|
url_for(
|
||||||
|
"applications.view_new_application_step_3", application_id=application.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_member(monkeypatch, client, user_session, session):
|
||||||
|
job_mock = Mock()
|
||||||
|
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
|
||||||
|
user = UserFactory.create()
|
||||||
|
application = ApplicationFactory.create(
|
||||||
|
environments=[{"name": "Naboo"}, {"name": "Endor"}]
|
||||||
|
)
|
||||||
|
(env, env_1) = application.environments
|
||||||
|
|
||||||
|
user_session(application.portfolio.owner)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
url_for("applications.create_member", application_id=application.id),
|
||||||
|
data={
|
||||||
|
"user_data-first_name": user.first_name,
|
||||||
|
"user_data-last_name": user.last_name,
|
||||||
|
"user_data-dod_id": user.dod_id,
|
||||||
|
"user_data-email": user.email,
|
||||||
|
"environment_roles-0-environment_id": env.id,
|
||||||
|
"environment_roles-0-role": "Basic Access",
|
||||||
|
"environment_roles-0-environment_name": env.name,
|
||||||
|
"environment_roles-1-environment_id": env_1.id,
|
||||||
|
"environment_roles-1-role": NO_ACCESS,
|
||||||
|
"environment_roles-1-environment_name": env_1.name,
|
||||||
|
"perms_env_mgmt": True,
|
||||||
|
"perms_team_mgmt": True,
|
||||||
|
"perms_del_env": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
expected_url = url_for(
|
||||||
|
"applications.settings",
|
||||||
|
application_id=application.id,
|
||||||
|
fragment="application-members",
|
||||||
|
_anchor="application-members",
|
||||||
|
_external=True,
|
||||||
|
)
|
||||||
|
assert response.location == expected_url
|
||||||
|
assert len(application.roles) == 1
|
||||||
|
environment_roles = application.roles[0].environment_roles
|
||||||
|
assert len(environment_roles) == 1
|
||||||
|
assert environment_roles[0].environment == env
|
||||||
|
|
||||||
|
invitation = (
|
||||||
|
session.query(ApplicationInvitation).filter_by(dod_id=user.dod_id).one()
|
||||||
|
)
|
||||||
|
assert invitation.role.application == application
|
||||||
|
|
||||||
|
assert job_mock.called
|
||||||
|
@ -21,6 +21,7 @@ from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
|||||||
from atst.forms.application import EditEnvironmentForm
|
from atst.forms.application import EditEnvironmentForm
|
||||||
from atst.forms.application_member import UpdateMemberForm
|
from atst.forms.application_member import UpdateMemberForm
|
||||||
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
|
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
|
||||||
|
from atst.routes.applications.settings import filter_env_roles_form_data
|
||||||
|
|
||||||
from tests.utils import captured_templates
|
from tests.utils import captured_templates
|
||||||
|
|
||||||
@ -559,3 +560,28 @@ def test_revoke_invite(client, user_session):
|
|||||||
|
|
||||||
assert invite.is_revoked
|
assert invite.is_revoked
|
||||||
assert app_role.status == ApplicationRoleStatus.DISABLED
|
assert app_role.status == ApplicationRoleStatus.DISABLED
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_environment_roles():
|
||||||
|
application_role = ApplicationRoleFactory.create(user=None)
|
||||||
|
application_role2 = ApplicationRoleFactory.create(
|
||||||
|
user=None, application=application_role.application
|
||||||
|
)
|
||||||
|
application_role3 = ApplicationRoleFactory.create(
|
||||||
|
user=None, application=application_role.application
|
||||||
|
)
|
||||||
|
|
||||||
|
environment = EnvironmentFactory.create(application=application_role.application)
|
||||||
|
|
||||||
|
EnvironmentRoleFactory.create(
|
||||||
|
environment=environment, application_role=application_role
|
||||||
|
)
|
||||||
|
EnvironmentRoleFactory.create(
|
||||||
|
environment=environment, application_role=application_role2
|
||||||
|
)
|
||||||
|
|
||||||
|
environment_data = filter_env_roles_form_data(application_role, [environment])
|
||||||
|
assert environment_data[0]["role"] != "No Access"
|
||||||
|
|
||||||
|
environment_data = filter_env_roles_form_data(application_role3, [environment])
|
||||||
|
assert environment_data[0]["role"] == "No Access"
|
||||||
|
@ -302,8 +302,10 @@ portfolios:
|
|||||||
add_member: Add a New Team Member
|
add_member: Add a New Team Member
|
||||||
add_another_environment: Add another environment
|
add_another_environment: Add another environment
|
||||||
app_settings_text: App settings
|
app_settings_text: App settings
|
||||||
create_button_text: Create
|
new:
|
||||||
next_button_text: "Next: Environments"
|
step_1_button_text: "Save and Add Environments"
|
||||||
|
step_2_button_text: "Save and Add Members"
|
||||||
|
step_3_button_text: Save Application
|
||||||
create_new_env: Create a new environment.
|
create_new_env: Create a new environment.
|
||||||
create_new_env_info: Creating an environment gives you access to the Cloud Service Provider. This environment will function within the constraints of the task order, and any costs will be billed against the portfolio.
|
create_new_env_info: Creating an environment gives you access to the Cloud Service Provider. This environment will function within the constraints of the task order, and any costs will be billed against the portfolio.
|
||||||
csp_console_text: CSP console
|
csp_console_text: CSP console
|
||||||
|
Loading…
x
Reference in New Issue
Block a user