Merge pull request #801 from dod-ccpo/app-team-permissions
Application Team Table Permissions
This commit is contained in:
commit
fb7efc6057
@ -1,6 +1,9 @@
|
|||||||
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models import ApplicationRole, ApplicationRoleStatus
|
from atst.models import ApplicationRole, ApplicationRoleStatus
|
||||||
from atst.domain.permission_sets import PermissionSets
|
from .permission_sets import PermissionSets
|
||||||
|
from .exceptions import NotFoundError
|
||||||
|
|
||||||
|
|
||||||
class ApplicationRoles(object):
|
class ApplicationRoles(object):
|
||||||
@ -28,3 +31,27 @@ class ApplicationRoles(object):
|
|||||||
|
|
||||||
db.session.add(role)
|
db.session.add(role)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, user_id, application_id):
|
||||||
|
try:
|
||||||
|
app_role = (
|
||||||
|
db.session.query(ApplicationRole)
|
||||||
|
.filter_by(user_id=user_id, application_id=application_id)
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
except NoResultFound:
|
||||||
|
raise NotFoundError("application_role")
|
||||||
|
|
||||||
|
return app_role
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_permission_sets(cls, application_role, new_perm_sets_names):
|
||||||
|
application_role.permission_sets = ApplicationRoles._permission_sets_for_names(
|
||||||
|
new_perm_sets_names
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(application_role)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return application_role
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models import EnvironmentRole
|
from atst.models import EnvironmentRole, Application, Environment
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentRoles(object):
|
class EnvironmentRoles(object):
|
||||||
@ -35,3 +35,15 @@ class EnvironmentRoles(object):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_application_and_user(cls, user_id, application_id):
|
||||||
|
return (
|
||||||
|
db.session.query(EnvironmentRole)
|
||||||
|
.join(Environment)
|
||||||
|
.join(Application, Environment.application_id == Application.id)
|
||||||
|
.filter(EnvironmentRole.user_id == user_id)
|
||||||
|
.filter(Application.id == application_id)
|
||||||
|
.filter(EnvironmentRole.environment_id == Environment.id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
54
atst/forms/team.py
Normal file
54
atst/forms/team.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms.fields import FormField, FieldList, HiddenField, StringField
|
||||||
|
from wtforms.validators import Required
|
||||||
|
|
||||||
|
from .application_member import EnvironmentForm
|
||||||
|
from .forms import BaseForm
|
||||||
|
from atst.forms.fields import SelectField
|
||||||
|
from atst.domain.permission_sets import PermissionSets
|
||||||
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsForm(FlaskForm):
|
||||||
|
perms_team_mgmt = SelectField(
|
||||||
|
translate("portfolios.applications.members.new.manage_team"),
|
||||||
|
choices=[
|
||||||
|
(PermissionSets.VIEW_APPLICATION, "View only"),
|
||||||
|
(PermissionSets.EDIT_APPLICATION_TEAM, "Edit access"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
perms_env_mgmt = SelectField(
|
||||||
|
translate("portfolios.applications.members.new.manage_envs"),
|
||||||
|
choices=[
|
||||||
|
(PermissionSets.VIEW_APPLICATION, "View only"),
|
||||||
|
(PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, "Edit access"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
perms_del_env = SelectField(
|
||||||
|
choices=[
|
||||||
|
(PermissionSets.VIEW_APPLICATION, "No"),
|
||||||
|
(PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, "Yes"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
_data = super().data
|
||||||
|
_data.pop("csrf_token", None)
|
||||||
|
permission_sets = []
|
||||||
|
for field in _data:
|
||||||
|
if _data[field] is not None:
|
||||||
|
permission_sets.append(_data[field])
|
||||||
|
|
||||||
|
return permission_sets
|
||||||
|
|
||||||
|
|
||||||
|
class MemberForm(FlaskForm):
|
||||||
|
user_id = HiddenField(validators=[Required()])
|
||||||
|
user_name = StringField()
|
||||||
|
environment_roles = FieldList(FormField(EnvironmentForm))
|
||||||
|
permission_sets = FormField(PermissionsForm)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamForm(BaseForm):
|
||||||
|
members = FieldList(FormField(MemberForm))
|
@ -28,6 +28,7 @@ class Application(
|
|||||||
back_populates="application",
|
back_populates="application",
|
||||||
primaryjoin="and_(Environment.application_id==Application.id, Environment.deleted==False)",
|
primaryjoin="and_(Environment.application_id==Application.id, Environment.deleted==False)",
|
||||||
)
|
)
|
||||||
|
# TODO: filter condition on this relationship?
|
||||||
roles = relationship("ApplicationRole")
|
roles = relationship("ApplicationRole")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -2,62 +2,120 @@ from flask import render_template, request as http_request, g, url_for, redirect
|
|||||||
|
|
||||||
|
|
||||||
from . import applications_bp
|
from . import applications_bp
|
||||||
from atst.domain.environments import Environments
|
|
||||||
from atst.domain.applications import Applications
|
from atst.domain.applications import Applications
|
||||||
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
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.domain.permission_sets import PermissionSets
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
from atst.domain.exceptions import AlreadyExistsError
|
from atst.domain.exceptions import AlreadyExistsError
|
||||||
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from atst.forms.application_member import NewForm as NewMemberForm
|
from atst.forms.application_member import NewForm as NewMemberForm
|
||||||
|
from atst.forms.team import TeamForm
|
||||||
from atst.models import Permissions
|
from atst.models import Permissions
|
||||||
from atst.services.invitation import Invitation as InvitationService
|
from atst.services.invitation import Invitation as InvitationService
|
||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
from atst.utils.localization import translate
|
|
||||||
|
|
||||||
|
|
||||||
def permission_str(member, edit_perm_set):
|
def get_form_permission_value(member, edit_perm_set):
|
||||||
if member.has_permission_set(edit_perm_set):
|
if member.has_permission_set(edit_perm_set):
|
||||||
return translate("portfolios.members.permissions.edit_access")
|
return edit_perm_set
|
||||||
else:
|
else:
|
||||||
return translate("portfolios.members.permissions.view_only")
|
return PermissionSets.VIEW_APPLICATION
|
||||||
|
|
||||||
|
|
||||||
|
def get_team_form(application):
|
||||||
|
team_data = []
|
||||||
|
for member in application.members:
|
||||||
|
user_id = member.user.id
|
||||||
|
user_name = member.user.full_name
|
||||||
|
permission_sets = {
|
||||||
|
"perms_team_mgmt": get_form_permission_value(
|
||||||
|
member, PermissionSets.EDIT_APPLICATION_TEAM
|
||||||
|
),
|
||||||
|
"perms_env_mgmt": get_form_permission_value(
|
||||||
|
member, PermissionSets.EDIT_APPLICATION_ENVIRONMENTS
|
||||||
|
),
|
||||||
|
"perms_del_env": get_form_permission_value(
|
||||||
|
member, PermissionSets.DELETE_APPLICATION_ENVIRONMENTS
|
||||||
|
),
|
||||||
|
}
|
||||||
|
roles = EnvironmentRoles.get_for_application_and_user(
|
||||||
|
member.user.id, application.id
|
||||||
|
)
|
||||||
|
environment_roles = [
|
||||||
|
{
|
||||||
|
"environment_id": str(role.environment.id),
|
||||||
|
"environment_name": role.environment.name,
|
||||||
|
"role": role.role,
|
||||||
|
}
|
||||||
|
for role in roles
|
||||||
|
]
|
||||||
|
team_data.append(
|
||||||
|
{
|
||||||
|
"user_id": str(user_id),
|
||||||
|
"user_name": user_name,
|
||||||
|
"permission_sets": permission_sets,
|
||||||
|
"environment_roles": environment_roles,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return TeamForm(data={"members": team_data})
|
||||||
|
|
||||||
|
|
||||||
|
def get_new_member_form(application):
|
||||||
|
env_roles = [
|
||||||
|
{"environment_id": e.id, "environment_name": e.name}
|
||||||
|
for e in application.environments
|
||||||
|
]
|
||||||
|
|
||||||
|
return NewMemberForm(data={"environment_roles": env_roles})
|
||||||
|
|
||||||
|
|
||||||
|
def render_team_page(application):
|
||||||
|
team_form = get_team_form(application)
|
||||||
|
new_member_form = get_new_member_form(application)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"portfolios/applications/team.html",
|
||||||
|
application=application,
|
||||||
|
team_form=team_form,
|
||||||
|
new_member_form=new_member_form,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@applications_bp.route("/applications/<application_id>/team")
|
@applications_bp.route("/applications/<application_id>/team")
|
||||||
@user_can(Permissions.VIEW_APPLICATION, message="view portfolio applications")
|
@user_can(Permissions.VIEW_APPLICATION, message="view portfolio applications")
|
||||||
def team(application_id):
|
def team(application_id):
|
||||||
application = Applications.get(resource_id=application_id)
|
application = Applications.get(resource_id=application_id)
|
||||||
|
return render_team_page(application)
|
||||||
|
|
||||||
environment_users = {}
|
|
||||||
for member in application.members:
|
|
||||||
user_id = member.user.id
|
|
||||||
environment_users[user_id] = {
|
|
||||||
"permissions": {
|
|
||||||
"delete_access": permission_str(
|
|
||||||
member, PermissionSets.DELETE_APPLICATION_ENVIRONMENTS
|
|
||||||
),
|
|
||||||
"environment_management": permission_str(
|
|
||||||
member, PermissionSets.EDIT_APPLICATION_ENVIRONMENTS
|
|
||||||
),
|
|
||||||
"team_management": permission_str(
|
|
||||||
member, PermissionSets.EDIT_APPLICATION_TEAM
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"environments": Environments.for_user(
|
|
||||||
user=member.user, application=application
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
env_roles = [
|
@applications_bp.route("/application/<application_id>/team", methods=["POST"])
|
||||||
{"environment_id": e.id, "environment_name": e.name}
|
@user_can(Permissions.EDIT_APPLICATION_MEMBER, message="update application member")
|
||||||
for e in application.environments
|
def update_team(application_id):
|
||||||
|
application = Applications.get(application_id)
|
||||||
|
form = TeamForm(http_request.form)
|
||||||
|
|
||||||
|
if form.validate():
|
||||||
|
for member in form.members:
|
||||||
|
app_role = ApplicationRoles.get(member.data["user_id"], application.id)
|
||||||
|
new_perms = [
|
||||||
|
perm
|
||||||
|
for perm in member.data["permission_sets"]
|
||||||
|
if perm != PermissionSets.VIEW_APPLICATION
|
||||||
]
|
]
|
||||||
member_form = NewMemberForm(data={"environment_roles": env_roles})
|
ApplicationRoles.update_permission_sets(app_role, new_perms)
|
||||||
|
flash("updated_application_members_permissions")
|
||||||
|
|
||||||
return render_template(
|
return redirect(
|
||||||
"portfolios/applications/team.html",
|
url_for(
|
||||||
application=application,
|
"applications.team",
|
||||||
environment_users=environment_users,
|
application_id=application_id,
|
||||||
member_form=member_form,
|
fragment="application-members",
|
||||||
|
_anchor="application-members",
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (render_team_page(application), 400)
|
||||||
|
|
||||||
|
|
||||||
@applications_bp.route("/application/<application_id>/members/new", methods=["POST"])
|
@applications_bp.route("/application/<application_id>/members/new", methods=["POST"])
|
||||||
|
@ -173,6 +173,13 @@ MESSAGES = {
|
|||||||
""",
|
""",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
|
"updated_application_members_permissions": {
|
||||||
|
"title_template": translate("flash.success"),
|
||||||
|
"message_template": """
|
||||||
|
<p>{{ "flash.updated_application_members_permissions" | translate }}</p>
|
||||||
|
""",
|
||||||
|
"category": "success",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,26 +1,28 @@
|
|||||||
import ally from 'ally.js'
|
import ally from 'ally.js'
|
||||||
|
|
||||||
import FormMixin from '../../mixins/form'
|
|
||||||
import textinput from '../text_input'
|
|
||||||
import optionsinput from '../options_input'
|
|
||||||
import DateSelector from '../date_selector'
|
|
||||||
import MultiStepModalForm from './multi_step_modal_form'
|
|
||||||
import multicheckboxinput from '../multi_checkbox_input'
|
|
||||||
import checkboxinput from '../checkbox_input'
|
import checkboxinput from '../checkbox_input'
|
||||||
|
import DateSelector from '../date_selector'
|
||||||
|
import FormMixin from '../../mixins/form'
|
||||||
import levelofwarrant from '../levelofwarrant'
|
import levelofwarrant from '../levelofwarrant'
|
||||||
import Modal from '../../mixins/modal'
|
import Modal from '../../mixins/modal'
|
||||||
|
import multicheckboxinput from '../multi_checkbox_input'
|
||||||
|
import MultiStepModalForm from './multi_step_modal_form'
|
||||||
|
import optionsinput from '../options_input'
|
||||||
|
import textinput from '../text_input'
|
||||||
|
import toggler from '../toggler'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'base-form',
|
name: 'base-form',
|
||||||
components: {
|
components: {
|
||||||
textinput,
|
|
||||||
optionsinput,
|
|
||||||
DateSelector,
|
|
||||||
MultiStepModalForm,
|
|
||||||
multicheckboxinput,
|
|
||||||
checkboxinput,
|
checkboxinput,
|
||||||
|
DateSelector,
|
||||||
levelofwarrant,
|
levelofwarrant,
|
||||||
Modal,
|
Modal,
|
||||||
|
multicheckboxinput,
|
||||||
|
MultiStepModalForm,
|
||||||
|
optionsinput,
|
||||||
|
textinput,
|
||||||
|
toggler,
|
||||||
},
|
},
|
||||||
mixins: [FormMixin],
|
mixins: [FormMixin],
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { emitEvent } from '../lib/emitters'
|
import { emitEvent } from '../lib/emitters'
|
||||||
|
import FormMixin from '../mixins/form'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'optionsinput',
|
name: 'optionsinput',
|
||||||
|
|
||||||
|
mixins: [FormMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
name: String,
|
name: String,
|
||||||
initialErrors: {
|
initialErrors: {
|
||||||
@ -10,6 +13,10 @@ export default {
|
|||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
initialValue: String,
|
initialValue: String,
|
||||||
|
watch: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
@ -27,6 +34,7 @@ export default {
|
|||||||
emitEvent('field-change', this, {
|
emitEvent('field-change', this, {
|
||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
watch: this.watch,
|
||||||
})
|
})
|
||||||
this.showError = false
|
this.showError = false
|
||||||
this.showValid = true
|
this.showValid = true
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import FormMixin from '../mixins/form'
|
import FormMixin from '../mixins/form'
|
||||||
|
import optionsinput from './options_input'
|
||||||
import textinput from './text_input'
|
import textinput from './text_input'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -8,6 +9,7 @@ export default {
|
|||||||
|
|
||||||
components: {
|
components: {
|
||||||
textinput,
|
textinput,
|
||||||
|
optionsinput,
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
|
@ -16,8 +16,7 @@ export default {
|
|||||||
const { name, valid, parent_uid } = event
|
const { name, valid, parent_uid } = event
|
||||||
if (typeof this[name] !== undefined) {
|
if (typeof this[name] !== undefined) {
|
||||||
this.fields[name] = valid
|
this.fields[name] = valid
|
||||||
|
if (event['parent_uid'] === this._uid || event['watch']) {
|
||||||
if (parent_uid === this._uid) {
|
|
||||||
this.changed = true
|
this.changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,15 @@
|
|||||||
|
|
||||||
.accordion-table__item-content {
|
.accordion-table__item-content {
|
||||||
padding: ($gap * 2);
|
padding: ($gap * 2);
|
||||||
|
|
||||||
|
.usa-input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
border: none;
|
||||||
|
font-weight: $font-normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-table__items {
|
.accordion-table__items {
|
||||||
|
@ -126,6 +126,7 @@ table {
|
|||||||
@include h4;
|
@include h4;
|
||||||
|
|
||||||
font-size: $lead-font-size;
|
font-size: $lead-font-size;
|
||||||
|
justify-content: space-between;
|
||||||
flex: 2;
|
flex: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% from "components/tooltip.html" import Tooltip %}
|
{% from "components/tooltip.html" import Tooltip %}
|
||||||
|
|
||||||
{% macro OptionsInput(field, tooltip, inline=False, label=True, disabled=False) -%}
|
{% macro OptionsInput(field, tooltip, inline=False, label=True, disabled=False, watch=False) -%}
|
||||||
<optionsinput
|
<optionsinput
|
||||||
name='{{ field.name }}'
|
name='{{ field.name }}'
|
||||||
inline-template
|
inline-template
|
||||||
{% if field.errors %}v-bind:initial-errors='{{ field.errors | list }}'{% endif %}
|
{% if field.errors %}v-bind:initial-errors='{{ field.errors | list }}'{% endif %}
|
||||||
{% if field.data and field.data != "None" %}v-bind:initial-value="'{{ field.data }}'"{% endif %}
|
{% if field.data and field.data != "None" %}v-bind:initial-value="'{{ field.data }}'"{% endif %}
|
||||||
key='{{ field.name }}'
|
key='{{ field.name }}'
|
||||||
|
v-bind:watch='{{ watch | string | lower }}'
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-bind:class="['usa-input', { 'usa-input--error': showError, 'usa-input--success': showValid }]">
|
v-bind:class="['usa-input', { 'usa-input--error': showError, 'usa-input--success': showValid }]">
|
||||||
|
@ -9,23 +9,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='form-row'>
|
<div class='form-row'>
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(member_form.user_data.first_name, validation='requiredField') }}
|
{{ TextInput(new_member_form.user_data.first_name, validation='requiredField') }}
|
||||||
</div>
|
</div>
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(member_form.user_data.last_name, validation='requiredField') }}
|
{{ TextInput(new_member_form.user_data.last_name, validation='requiredField') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-row'>
|
<div class='form-row'>
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(member_form.user_data.email, validation='email') }}
|
{{ TextInput(new_member_form.user_data.email, validation='email') }}
|
||||||
</div>
|
</div>
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(member_form.user_data.phone_number, validation='usPhone', optional=True) }}
|
{{ TextInput(new_member_form.user_data.phone_number, validation='usPhone', optional=True) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-row'>
|
<div class='form-row'>
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(member_form.user_data.dod_id, validation='dodId') }}
|
{{ TextInput(new_member_form.user_data.dod_id, validation='dodId') }}
|
||||||
</div>
|
</div>
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
</div>
|
</div>
|
||||||
@ -61,7 +61,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for environment_data in member_form.environment_roles %}
|
{% for environment_data in new_member_form.environment_roles %}
|
||||||
<optionsinput inline-template
|
<optionsinput inline-template
|
||||||
v-bind:initial-value="'{{ environment_data.role.data | string }}'"
|
v-bind:initial-value="'{{ environment_data.role.data | string }}'"
|
||||||
>
|
>
|
||||||
@ -86,9 +86,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ "portfolios.applications.members.new.manage_perms" | translate({"application_name": application.name}) }}</h1>
|
<h1>{{ "portfolios.applications.members.new.manage_perms" | translate({"application_name": application.name}) }}</h1>
|
||||||
{{ CheckboxInput(member_form.permission_sets.perms_team_mgmt, classes="input__inline-fields") }}
|
{{ CheckboxInput(new_member_form.permission_sets.perms_team_mgmt, classes="input__inline-fields") }}
|
||||||
{% call CheckboxInput(member_form.permission_sets.perms_env_mgmt, classes="input__inline-fields") %}
|
{% call CheckboxInput(new_member_form.permission_sets.perms_env_mgmt, classes="input__inline-fields") %}
|
||||||
{% set field=member_form.permission_sets.perms_del_env %}
|
{% set field=new_member_form.permission_sets.perms_del_env %}
|
||||||
<nestedcheckboxinput
|
<nestedcheckboxinput
|
||||||
name='{{ field.name }}'
|
name='{{ field.name }}'
|
||||||
inline-template
|
inline-template
|
||||||
@ -128,7 +128,7 @@
|
|||||||
{% endset %}
|
{% endset %}
|
||||||
{{ MultiStepModalForm(
|
{{ MultiStepModalForm(
|
||||||
'add-app-mem',
|
'add-app-mem',
|
||||||
member_form,
|
new_member_form,
|
||||||
url_for("applications.create_member", application_id=application.id),
|
url_for("applications.create_member", application_id=application.id),
|
||||||
[step_one, step_two],
|
[step_one, step_two],
|
||||||
button_text=("portfolios.admin.add_new_member" | translate),
|
button_text=("portfolios.admin.add_new_member" | translate),
|
||||||
|
48
templates/fragments/applications/edit_team.html
Normal file
48
templates/fragments/applications/edit_team.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{% from "components/options_input.html" import OptionsInput %}
|
||||||
|
|
||||||
|
<form method='POST' id="team" action='{{ url_for("applications.update_team", application_id=application.id) }}' autocomplete="off" enctype="multipart/form-data">
|
||||||
|
{{ team_form.csrf_token }}
|
||||||
|
|
||||||
|
{% for member_form in team_form.members %}
|
||||||
|
{% set environment_roles_form = member_form.environment_roles %}
|
||||||
|
{% set permissions_form = member_form.permission_sets %}
|
||||||
|
|
||||||
|
<toggler inline-template>
|
||||||
|
<li class="accordion-table__item">
|
||||||
|
<div class="accordion-table__item-content row">
|
||||||
|
<div class="col col--grow">{{ member_form.user_name.data }}</div>
|
||||||
|
<div class="col col--grow">{{ OptionsInput(permissions_form.perms_team_mgmt, label=False, watch=True) }}</div>
|
||||||
|
<div class="col col--grow">{{ OptionsInput(permissions_form.perms_env_mgmt, label=False, watch=True) }}</div>
|
||||||
|
<div class="col col--grow">{{ OptionsInput(permissions_form.perms_del_env, label=False, watch=True) }}</div>
|
||||||
|
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
|
||||||
|
{% set open_html %}
|
||||||
|
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ environment_roles_form | length }}) {{ Icon('caret_down') }}
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{% set close_html %}
|
||||||
|
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ environment_roles_form | length }}) {{ Icon('caret_up') }}
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{{
|
||||||
|
ToggleButton(
|
||||||
|
open_html=open_html,
|
||||||
|
close_html=close_html,
|
||||||
|
section_name="environments"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% call ToggleSection(section_name="environments") %}
|
||||||
|
<ul>
|
||||||
|
{% for environment_form in environment_roles_form %}
|
||||||
|
<li class="accordion-table__item__expanded">
|
||||||
|
{{ environment_form.environment_name.data }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endcall %}
|
||||||
|
{{ member_form.user_id() }}
|
||||||
|
</li>
|
||||||
|
</toggler>
|
||||||
|
{% endfor %}
|
||||||
|
</form>
|
45
templates/fragments/applications/read_only_team.html
Normal file
45
templates/fragments/applications/read_only_team.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% for member in team_form.members %}
|
||||||
|
{% set user_permissions = [member.permission_sets.perms_team_mgmt, member.permission_sets.perms_env_mgmt, member.permission_sets.perms_del_env] %}
|
||||||
|
|
||||||
|
{% macro PermissionField(value) %}
|
||||||
|
<div class="col col--grow user-permission{% if "Edit" in value or "Yes" in value %} green{% endif %}">{{ value }}</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
<toggler inline-template>
|
||||||
|
<li class="accordion-table__item">
|
||||||
|
<div class="accordion-table__item-content row">
|
||||||
|
<div class="col col--grow">{{ member.user_name.data }}</div>
|
||||||
|
{% for permission in user_permissions %}
|
||||||
|
{% set perm = dict(permission.choices).get(permission.data) %}
|
||||||
|
{{ PermissionField(perm) }}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
|
||||||
|
{% set open_html %}
|
||||||
|
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ member.environment_roles | length }}) {{ Icon('caret_down') }}
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{% set close_html %}
|
||||||
|
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ member.environment_roles | length }}) {{ Icon('caret_up') }}
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{{
|
||||||
|
ToggleButton(
|
||||||
|
open_html=open_html,
|
||||||
|
close_html=close_html,
|
||||||
|
section_name="environments"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% call ToggleSection(section_name="environments") %}
|
||||||
|
<ul>
|
||||||
|
{% for environment in member.environment_roles %}
|
||||||
|
<li class="accordion-table__item__expanded">
|
||||||
|
{{ environment.environment_name.data }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endcall %}
|
||||||
|
</li>
|
||||||
|
</toggler>
|
||||||
|
{% endfor %}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
{% from "components/empty_state.html" import EmptyState %}
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
|
{% from 'components/save_button.html' import SaveButton %}
|
||||||
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
|
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
|
||||||
|
|
||||||
{% set secondary_breadcrumb = 'portfolios.applications.team_settings.title' | translate({ "application_name": application.name }) %}
|
{% set secondary_breadcrumb = 'portfolios.applications.team_settings.title' | translate({ "application_name": application.name }) %}
|
||||||
@ -24,18 +25,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="member-list application-list" id="application-members">
|
<section class="member-list application-list" id="application-members">
|
||||||
|
<base-form inline-template>
|
||||||
<div class='responsive-table-wrapper panel'>
|
<div class='responsive-table-wrapper panel'>
|
||||||
{% if g.matchesPath("application-members") %}
|
{% if g.matchesPath("application-members") %}
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<header>
|
<header>
|
||||||
<div class="responsive-table-wrapper__header">
|
<div class="responsive-table-wrapper__header">
|
||||||
<div class="responsive-table-wrapper__title">
|
<div class="responsive-table-wrapper__title row">
|
||||||
<div class="h3">
|
<div class="h3">
|
||||||
{{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }}
|
{{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<a class="icon-link">
|
||||||
<a class='icon-link'>
|
|
||||||
{{ Icon('info') }}
|
{{ Icon('info') }}
|
||||||
{{ "portfolios.admin.settings_info" | translate }}
|
{{ "portfolios.admin.settings_info" | translate }}
|
||||||
</a>
|
</a>
|
||||||
@ -48,77 +49,41 @@
|
|||||||
{{ "common.name" | translate }}
|
{{ "common.name" | translate }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col--grow">
|
<div class="col col--grow">
|
||||||
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }}
|
{{ "portfolios.applications.team_settings.section.table.team_management" | translate }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col--grow">
|
<div class="col col--grow">
|
||||||
{{ "portfolios.applications.team_settings.section.table.environment_management" | translate }}
|
{{ "portfolios.applications.team_settings.section.table.environment_management" | translate }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col--grow">
|
<div class="col col--grow">
|
||||||
{{ "portfolios.applications.team_settings.section.table.team_management" | translate }}
|
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col--grow">
|
<div class="col col--grow">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="accordion-table__items">
|
<ul class="accordion-table__items">
|
||||||
{% for member in application.members %}
|
{% if user_can(permissions.EDIT_APPLICATION_MEMBER) %}
|
||||||
{% set user = member.user %}
|
{% include "fragments/applications/edit_team.html" %}
|
||||||
{% set user_info = environment_users[user.id] %}
|
{% elif user_can(permissions.VIEW_APPLICATION_MEMBER) %}
|
||||||
{% set user_permissions = user_info["permissions"] %}
|
{% include "fragments/applications/read_only_team.html" %}
|
||||||
|
{% endif %}
|
||||||
{% macro PermissionField(value) %}
|
|
||||||
<div class="col col--grow user-permission{% if "Edit" in value %} green{% endif %}">{{ value }}</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
<toggler inline-template>
|
|
||||||
<li class="accordion-table__item">
|
|
||||||
<div class="accordion-table__item-content row">
|
|
||||||
<div class="col col--grow">{{ user.full_name }}</div>
|
|
||||||
{{ PermissionField(user_permissions["delete_access"]) }}
|
|
||||||
{{ PermissionField(user_permissions["environment_management"]) }}
|
|
||||||
{{ PermissionField(user_permissions["team_management"]) }}
|
|
||||||
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
|
|
||||||
{% set open_html %}
|
|
||||||
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ user_info['environments'] | length }}) {{ Icon('caret_down') }}
|
|
||||||
{% endset %}
|
|
||||||
|
|
||||||
{% set close_html %}
|
|
||||||
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ user_info['environments'] | length }}) {{ Icon('caret_up') }}
|
|
||||||
{% endset %}
|
|
||||||
|
|
||||||
{{
|
|
||||||
ToggleButton(
|
|
||||||
open_html=open_html,
|
|
||||||
close_html=close_html,
|
|
||||||
section_name="environments"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% call ToggleSection(section_name="environments") %}
|
|
||||||
<ul>
|
|
||||||
{% for environment in user_info["environments"] %}
|
|
||||||
<li class="accordion-table__item__expanded">
|
|
||||||
{{ environment.name }}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endcall %}
|
|
||||||
</li>
|
|
||||||
</toggler>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="members-table-footer">
|
<div class="panel__footer">
|
||||||
<div class="action-group save">
|
<div class="action-group save">
|
||||||
|
{% if user_can(permissions.EDIT_APPLICATION_MEMBER) %}
|
||||||
|
{{ SaveButton(text=('common.save' | translate), element="input", form="team") }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
|
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
|
||||||
{% include "fragments/applications/add_new_application_member.html" %}
|
{% include "fragments/applications/add_new_application_member.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
</base-form>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
from atst.domain.application_roles import ApplicationRoles
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
|
from atst.domain.exceptions import NotFoundError
|
||||||
from atst.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from atst.models import ApplicationRoleStatus
|
from atst.models import ApplicationRoleStatus
|
||||||
|
|
||||||
@ -33,3 +36,38 @@ def test_enabled_application_role():
|
|||||||
ApplicationRoles.enable(app_role)
|
ApplicationRoles.enable(app_role)
|
||||||
|
|
||||||
assert app_role.status == ApplicationRoleStatus.ACTIVE
|
assert app_role.status == ApplicationRoleStatus.ACTIVE
|
||||||
|
|
||||||
|
|
||||||
|
def test_get():
|
||||||
|
user = UserFactory.create()
|
||||||
|
application = ApplicationFactory.create()
|
||||||
|
app_role = ApplicationRoleFactory.create(user=user, application=application)
|
||||||
|
|
||||||
|
assert ApplicationRoles.get(user.id, application.id)
|
||||||
|
assert app_role.application == application
|
||||||
|
assert app_role.user == user
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_handles_invalid_id():
|
||||||
|
user = UserFactory.create()
|
||||||
|
application = ApplicationFactory.create()
|
||||||
|
|
||||||
|
with pytest.raises(NotFoundError):
|
||||||
|
ApplicationRoles.get(user.id, application.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_permission_sets():
|
||||||
|
user = UserFactory.create()
|
||||||
|
application = ApplicationFactory.create()
|
||||||
|
app_role = ApplicationRoleFactory.create(user=user, application=application)
|
||||||
|
|
||||||
|
view_app = [PermissionSets.get(PermissionSets.VIEW_APPLICATION)]
|
||||||
|
new_perms_names = [
|
||||||
|
PermissionSets.EDIT_APPLICATION_TEAM,
|
||||||
|
PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
|
||||||
|
]
|
||||||
|
new_perms = PermissionSets.get_many(new_perms_names)
|
||||||
|
# view application permission is included by default
|
||||||
|
assert app_role.permission_sets == view_app
|
||||||
|
assert ApplicationRoles.update_permission_sets(app_role, new_perms_names)
|
||||||
|
assert set(app_role.permission_sets) == set(new_perms + view_app)
|
||||||
|
16
tests/domain/test_environment_roles.py
Normal file
16
tests/domain/test_environment_roles.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
|
|
||||||
|
from tests.factories import *
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_for_application_and_user():
|
||||||
|
user = UserFactory.create()
|
||||||
|
application = ApplicationFactory.create()
|
||||||
|
env1 = EnvironmentFactory.create(application=application)
|
||||||
|
EnvironmentFactory.create(application=application)
|
||||||
|
EnvironmentRoleFactory.create(user=user, environment=env1)
|
||||||
|
|
||||||
|
roles = EnvironmentRoles.get_for_application_and_user(user.id, application.id)
|
||||||
|
assert len(roles) == 1
|
||||||
|
assert roles[0].environment == env1
|
||||||
|
assert roles[0].user == user
|
0
tests/forms/__init__.py
Normal file
0
tests/forms/__init__.py
Normal file
31
tests/forms/test_team.py
Normal file
31
tests/forms/test_team.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from wtforms.validators import ValidationError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from atst.domain.permission_sets import PermissionSets
|
||||||
|
from atst.forms.team import *
|
||||||
|
|
||||||
|
|
||||||
|
def test_permissions_form_permission_sets():
|
||||||
|
form_data = {
|
||||||
|
"perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM,
|
||||||
|
"perms_env_mgmt": PermissionSets.VIEW_APPLICATION,
|
||||||
|
"perms_del_env": PermissionSets.VIEW_APPLICATION,
|
||||||
|
}
|
||||||
|
form = PermissionsForm(data=form_data)
|
||||||
|
|
||||||
|
assert form.validate()
|
||||||
|
assert form.data == [
|
||||||
|
PermissionSets.EDIT_APPLICATION_TEAM,
|
||||||
|
PermissionSets.VIEW_APPLICATION,
|
||||||
|
PermissionSets.VIEW_APPLICATION,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_permissions_form_invalid():
|
||||||
|
form_data = {
|
||||||
|
"perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM,
|
||||||
|
"perms_env_mgmt": "not a real choice",
|
||||||
|
"perms_del_env": PermissionSets.VIEW_APPLICATION,
|
||||||
|
}
|
||||||
|
form = PermissionsForm(data=form_data)
|
||||||
|
assert not form.validate()
|
0
tests/routes/applications/__init__.py
Normal file
0
tests/routes/applications/__init__.py
Normal file
@ -1,6 +1,9 @@
|
|||||||
|
import pytest
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from tests.factories import PortfolioFactory, ApplicationFactory, UserFactory
|
from atst.domain.permission_sets import PermissionSets
|
||||||
|
|
||||||
|
from tests.factories import *
|
||||||
|
|
||||||
|
|
||||||
def test_application_team(client, user_session):
|
def test_application_team(client, user_session):
|
||||||
@ -10,10 +13,83 @@ def test_application_team(client, user_session):
|
|||||||
user_session(portfolio.owner)
|
user_session(portfolio.owner)
|
||||||
|
|
||||||
response = client.get(url_for("applications.team", application_id=application.id))
|
response = client.get(url_for("applications.team", application_id=application.id))
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_team(client, user_session):
|
||||||
|
application = ApplicationFactory.create()
|
||||||
|
owner = application.portfolio.owner
|
||||||
|
app_role = ApplicationRoleFactory.create(
|
||||||
|
application=application, permission_sets=[]
|
||||||
|
)
|
||||||
|
app_user = app_role.user
|
||||||
|
user_session(owner)
|
||||||
|
response = client.post(
|
||||||
|
url_for("applications.update_team", application_id=application.id),
|
||||||
|
data={
|
||||||
|
"members-0-user_id": app_user.id,
|
||||||
|
"members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM,
|
||||||
|
"members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
|
||||||
|
"members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
actual_perms_names = [perm.name for perm in app_role.permission_sets]
|
||||||
|
expected_perms_names = [
|
||||||
|
PermissionSets.VIEW_APPLICATION,
|
||||||
|
PermissionSets.EDIT_APPLICATION_TEAM,
|
||||||
|
PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
|
||||||
|
PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
|
||||||
|
]
|
||||||
|
assert expected_perms_names == actual_perms_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_team_with_bad_permission_sets(client, user_session):
|
||||||
|
application = ApplicationFactory.create()
|
||||||
|
owner = application.portfolio.owner
|
||||||
|
app_role = ApplicationRoleFactory.create(
|
||||||
|
application=application, permission_sets=[]
|
||||||
|
)
|
||||||
|
app_user = app_role.user
|
||||||
|
permission_sets = app_user.permission_sets
|
||||||
|
|
||||||
|
user_session(owner)
|
||||||
|
response = client.post(
|
||||||
|
url_for("applications.update_team", application_id=application.id),
|
||||||
|
data={
|
||||||
|
"members-0-user_id": app_user.id,
|
||||||
|
"members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM,
|
||||||
|
"members-0-permission_sets-perms_env_mgmt": "some random string",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert app_user.permission_sets == permission_sets
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_team_with_non_app_user(client, user_session):
|
||||||
|
application = ApplicationFactory.create()
|
||||||
|
owner = application.portfolio.owner
|
||||||
|
app_role = ApplicationRoleFactory.create(
|
||||||
|
application=application, permission_sets=[]
|
||||||
|
)
|
||||||
|
non_app_user = UserFactory.create()
|
||||||
|
app_user = app_role.user
|
||||||
|
|
||||||
|
user_session(owner)
|
||||||
|
response = client.post(
|
||||||
|
url_for("applications.update_team", application_id=application.id),
|
||||||
|
data={
|
||||||
|
"members-0-user_id": non_app_user.id,
|
||||||
|
"members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM,
|
||||||
|
"members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
|
||||||
|
"members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def test_create_member(client, user_session):
|
def test_create_member(client, user_session):
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
application = ApplicationFactory.create(
|
application = ApplicationFactory.create(
|
||||||
|
@ -73,6 +73,7 @@ flash:
|
|||||||
portfolio_home: Go to my portfolio home page
|
portfolio_home: Go to my portfolio home page
|
||||||
success: Success!
|
success: Success!
|
||||||
new_application_member: 'You have successfully invited {user_name} to the team.'
|
new_application_member: 'You have successfully invited {user_name} to the team.'
|
||||||
|
updated_application_members_permissions: 'You have successfully updated member permissions.'
|
||||||
footer:
|
footer:
|
||||||
about_link_text: Joint Enterprise Defense Infrastructure
|
about_link_text: Joint Enterprise Defense Infrastructure
|
||||||
browser_support: JEDI Cloud supported on these web browsers
|
browser_support: JEDI Cloud supported on these web browsers
|
||||||
|
Loading…
x
Reference in New Issue
Block a user