Merge pull request #789 from dod-ccpo/application-invitations
Application invitations
This commit is contained in:
commit
3799e5c73f
69
alembic/versions/432c5287256d_add_application_invitation.py
Normal file
69
alembic/versions/432c5287256d_add_application_invitation.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"""add application_invitation
|
||||||
|
|
||||||
|
Revision ID: 432c5287256d
|
||||||
|
Revises: 1880551a32e4
|
||||||
|
Create Date: 2019-04-23 09:23:05.738680
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '432c5287256d'
|
||||||
|
down_revision = '1880551a32e4'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.rename_table('invitations', 'portfolio_invitations')
|
||||||
|
op.create_table('application_invitations',
|
||||||
|
sa.Column('time_created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('time_updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||||
|
sa.Column('status', sa.Enum('ACCEPTED', 'REVOKED', 'PENDING', 'REJECTED_WRONG_USER', 'REJECTED_EXPIRED', name='status', native_enum=False), nullable=True),
|
||||||
|
sa.Column('expiration_time', sa.TIMESTAMP(timezone=True), nullable=True),
|
||||||
|
sa.Column('token', sa.String(), nullable=True),
|
||||||
|
sa.Column('email', sa.String(), nullable=False),
|
||||||
|
sa.Column('application_role_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('inviter_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['application_role_id'], ['application_roles.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['inviter_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_application_invitations_application_role_id'), 'application_invitations', ['application_role_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_application_invitations_inviter_id'), 'application_invitations', ['inviter_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_application_invitations_token'), 'application_invitations', ['token'], unique=False)
|
||||||
|
op.create_index(op.f('ix_application_invitations_user_id'), 'application_invitations', ['user_id'], unique=False)
|
||||||
|
op.drop_index('ix_invitations_inviter_id', table_name='invitations')
|
||||||
|
op.drop_index('ix_invitations_portfolio_role_id', table_name='invitations')
|
||||||
|
op.drop_index('ix_invitations_token', table_name='invitations')
|
||||||
|
op.drop_index('ix_invitations_user_id', table_name='invitations')
|
||||||
|
op.create_index(op.f('ix_portfolio_invitations_inviter_id'), 'portfolio_invitations', ['inviter_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_portfolio_invitations_portfolio_role_id'), 'portfolio_invitations', ['portfolio_role_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_portfolio_invitations_token'), 'portfolio_invitations', ['token'], unique=False)
|
||||||
|
op.create_index(op.f('ix_portfolio_invitations_user_id'), 'portfolio_invitations', ['user_id'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.rename_table('portfolio_invitations', 'invitations')
|
||||||
|
op.drop_index(op.f('ix_application_invitations_user_id'), table_name='application_invitations')
|
||||||
|
op.drop_index(op.f('ix_application_invitations_token'), table_name='application_invitations')
|
||||||
|
op.drop_index(op.f('ix_application_invitations_inviter_id'), table_name='application_invitations')
|
||||||
|
op.drop_index(op.f('ix_application_invitations_application_role_id'), table_name='application_invitations')
|
||||||
|
op.drop_table('application_invitations')
|
||||||
|
op.drop_index(op.f('ix_portfolio_invitations_user_id'), table_name='portfolio_invitations')
|
||||||
|
op.drop_index(op.f('ix_portfolio_invitations_token'), table_name='portfolio_invitations')
|
||||||
|
op.drop_index(op.f('ix_portfolio_invitations_portfolio_role_id'), table_name='portfolio_invitations')
|
||||||
|
op.drop_index(op.f('ix_portfolio_invitations_inviter_id'), table_name='portfolio_invitations')
|
||||||
|
op.create_index('ix_invitations_user_id', 'invitations', ['user_id'], unique=False)
|
||||||
|
op.create_index('ix_invitations_token', 'invitations', ['token'], unique=False)
|
||||||
|
op.create_index('ix_invitations_portfolio_role_id', 'invitations', ['portfolio_role_id'], unique=False)
|
||||||
|
op.create_index('ix_invitations_inviter_id', 'invitations', ['inviter_id'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
@ -2,8 +2,10 @@ from sqlalchemy.orm.exc import NoResultFound
|
|||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from . import BaseDomainClass
|
from . import BaseDomainClass
|
||||||
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
from atst.domain.environments import Environments
|
from atst.domain.environments import Environments
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
|
from atst.domain.users import Users
|
||||||
from atst.models.application import Application
|
from atst.models.application import Application
|
||||||
from atst.models.environment import Environment
|
from atst.models.environment import Environment
|
||||||
from atst.models.environment_role import EnvironmentRole
|
from atst.models.environment_role import EnvironmentRole
|
||||||
@ -72,3 +74,30 @@ class Applications(BaseDomainClass):
|
|||||||
|
|
||||||
db.session.add(application)
|
db.session.add(application)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_member(
|
||||||
|
cls, application, user_data, permission_sets=None, environment_roles_data=None
|
||||||
|
):
|
||||||
|
permission_sets = [] if permission_sets is None else permission_sets
|
||||||
|
environment_roles_data = (
|
||||||
|
[] if environment_roles_data is None else environment_roles_data
|
||||||
|
)
|
||||||
|
|
||||||
|
user = Users.get_or_create_by_dod_id(
|
||||||
|
user_data["dod_id"],
|
||||||
|
first_name=user_data["first_name"],
|
||||||
|
last_name=user_data["last_name"],
|
||||||
|
phone_number=user_data.get("phone_number"),
|
||||||
|
email=user_data["email"],
|
||||||
|
)
|
||||||
|
|
||||||
|
application_role = ApplicationRoles.create(user, application, permission_sets)
|
||||||
|
|
||||||
|
for env_role_data in environment_roles_data:
|
||||||
|
role = env_role_data.get("role")
|
||||||
|
if role:
|
||||||
|
environment = Environments.get(env_role_data.get("environment_id"))
|
||||||
|
Environments.add_member(environment, user, env_role_data.get("role"))
|
||||||
|
|
||||||
|
return application_role
|
||||||
|
@ -6,8 +6,8 @@ from . import user_can_access
|
|||||||
from atst.domain.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.domain.applications import Applications
|
from atst.domain.applications import Applications
|
||||||
from atst.domain.invitations import Invitations
|
|
||||||
from atst.domain.environments import Environments
|
from atst.domain.environments import Environments
|
||||||
|
from atst.domain.invitations import PortfolioInvitations
|
||||||
from atst.domain.exceptions import UnauthorizedError
|
from atst.domain.exceptions import UnauthorizedError
|
||||||
|
|
||||||
|
|
||||||
@ -24,8 +24,8 @@ def check_access(permission, message, override, *args, **kwargs):
|
|||||||
access_args["portfolio"] = task_order.portfolio
|
access_args["portfolio"] = task_order.portfolio
|
||||||
|
|
||||||
elif "token" in kwargs:
|
elif "token" in kwargs:
|
||||||
invite = Invitations._get(kwargs["token"])
|
invite = PortfolioInvitations._get(kwargs["token"])
|
||||||
access_args["portfolio"] = invite.portfolio_role.portfolio
|
access_args["portfolio"] = invite.role.portfolio
|
||||||
|
|
||||||
elif "portfolio_id" in kwargs:
|
elif "portfolio_id" in kwargs:
|
||||||
access_args["portfolio"] = Portfolios.get(
|
access_args["portfolio"] = Portfolios.get(
|
||||||
|
@ -2,8 +2,9 @@ import datetime
|
|||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models.invitation import Invitation, Status as InvitationStatus
|
from atst.models import ApplicationInvitation, InvitationStatus, PortfolioInvitation
|
||||||
from atst.domain.portfolio_roles import PortfolioRoles
|
from atst.domain.portfolio_roles import PortfolioRoles
|
||||||
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
|
|
||||||
from .exceptions import NotFoundError
|
from .exceptions import NotFoundError
|
||||||
|
|
||||||
@ -38,27 +39,30 @@ class InvitationError(Exception):
|
|||||||
return "{} has a status of {}".format(self.invite.id, self.invite.status.value)
|
return "{} has a status of {}".format(self.invite.id, self.invite.status.value)
|
||||||
|
|
||||||
|
|
||||||
class Invitations(object):
|
class BaseInvitations(object):
|
||||||
|
model = None
|
||||||
|
role_domain_class = None
|
||||||
# number of minutes a given invitation is considered valid
|
# number of minutes a given invitation is considered valid
|
||||||
EXPIRATION_LIMIT_MINUTES = 360
|
EXPIRATION_LIMIT_MINUTES = 360
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get(cls, token):
|
def _get(cls, token):
|
||||||
try:
|
try:
|
||||||
invite = db.session.query(Invitation).filter_by(token=token).one()
|
invite = db.session.query(cls.model).filter_by(token=token).one()
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
raise NotFoundError("invite")
|
raise NotFoundError(cls.model.__tablename__)
|
||||||
|
|
||||||
return invite
|
return invite
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, inviter, portfolio_role, email):
|
def create(cls, inviter, role, email):
|
||||||
invite = Invitation(
|
# pylint: disable=not-callable
|
||||||
portfolio_role=portfolio_role,
|
invite = cls.model(
|
||||||
|
role=role,
|
||||||
inviter=inviter,
|
inviter=inviter,
|
||||||
user=portfolio_role.user,
|
user=role.user,
|
||||||
status=InvitationStatus.PENDING,
|
status=InvitationStatus.PENDING,
|
||||||
expiration_time=Invitations.current_expiration_time(),
|
expiration_time=cls.current_expiration_time(),
|
||||||
email=email,
|
email=email,
|
||||||
)
|
)
|
||||||
db.session.add(invite)
|
db.session.add(invite)
|
||||||
@ -68,29 +72,29 @@ class Invitations(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def accept(cls, user, token):
|
def accept(cls, user, token):
|
||||||
invite = Invitations._get(token)
|
invite = cls._get(token)
|
||||||
|
|
||||||
if invite.user.dod_id != user.dod_id:
|
if invite.user.dod_id != user.dod_id:
|
||||||
if invite.is_pending:
|
if invite.is_pending:
|
||||||
Invitations._update_status(invite, InvitationStatus.REJECTED_WRONG_USER)
|
cls._update_status(invite, InvitationStatus.REJECTED_WRONG_USER)
|
||||||
raise WrongUserError(user, invite)
|
raise WrongUserError(user, invite)
|
||||||
|
|
||||||
elif invite.is_expired:
|
elif invite.is_expired:
|
||||||
Invitations._update_status(invite, InvitationStatus.REJECTED_EXPIRED)
|
cls._update_status(invite, InvitationStatus.REJECTED_EXPIRED)
|
||||||
raise ExpiredError(invite)
|
raise ExpiredError(invite)
|
||||||
|
|
||||||
elif invite.is_accepted or invite.is_revoked or invite.is_rejected:
|
elif invite.is_accepted or invite.is_revoked or invite.is_rejected:
|
||||||
raise InvitationError(invite)
|
raise InvitationError(invite)
|
||||||
|
|
||||||
elif invite.is_pending: # pragma: no branch
|
elif invite.is_pending: # pragma: no branch
|
||||||
Invitations._update_status(invite, InvitationStatus.ACCEPTED)
|
cls._update_status(invite, InvitationStatus.ACCEPTED)
|
||||||
PortfolioRoles.enable(invite.portfolio_role)
|
cls.role_domain_class.enable(invite.role)
|
||||||
return invite
|
return invite
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def current_expiration_time(cls):
|
def current_expiration_time(cls):
|
||||||
return datetime.datetime.now() + datetime.timedelta(
|
return datetime.datetime.now() + datetime.timedelta(
|
||||||
minutes=Invitations.EXPIRATION_LIMIT_MINUTES
|
minutes=cls.EXPIRATION_LIMIT_MINUTES
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -103,23 +107,31 @@ class Invitations(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def revoke(cls, token):
|
def revoke(cls, token):
|
||||||
invite = Invitations._get(token)
|
invite = cls._get(token)
|
||||||
return Invitations._update_status(invite, InvitationStatus.REVOKED)
|
return cls._update_status(invite, InvitationStatus.REVOKED)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def lookup_by_portfolio_and_user(cls, portfolio, user):
|
def lookup_by_resource_and_user(cls, resource, user):
|
||||||
portfolio_role = PortfolioRoles.get(portfolio.id, user.id)
|
role = cls.role_domain_class.get(resource.id, user.id)
|
||||||
|
|
||||||
if portfolio_role.latest_invitation is None:
|
if role.latest_invitation is None:
|
||||||
raise NotFoundError("invitation")
|
raise NotFoundError(cls.model.__tablename__)
|
||||||
|
|
||||||
return portfolio_role.latest_invitation
|
return role.latest_invitation
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def resend(cls, user, token):
|
def resend(cls, user, token):
|
||||||
previous_invitation = Invitations._get(token)
|
previous_invitation = cls._get(token)
|
||||||
Invitations._update_status(previous_invitation, InvitationStatus.REVOKED)
|
cls._update_status(previous_invitation, InvitationStatus.REVOKED)
|
||||||
|
|
||||||
return Invitations.create(
|
return cls.create(user, previous_invitation.role, previous_invitation.email)
|
||||||
user, previous_invitation.portfolio_role, previous_invitation.email
|
|
||||||
)
|
|
||||||
|
class PortfolioInvitations(BaseInvitations):
|
||||||
|
model = PortfolioInvitation
|
||||||
|
role_domain_class = PortfolioRoles
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationInvitations(BaseInvitations):
|
||||||
|
model = ApplicationInvitation
|
||||||
|
role_domain_class = ApplicationRoles
|
||||||
|
54
atst/forms/application_member.py
Normal file
54
atst/forms/application_member.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms.fields import FormField, FieldList, HiddenField, BooleanField
|
||||||
|
|
||||||
|
from .forms import BaseForm
|
||||||
|
from .member import NewForm as BaseNewMemberForm
|
||||||
|
from .data import ENV_ROLES
|
||||||
|
from atst.forms.fields import SelectField
|
||||||
|
from atst.domain.permission_sets import PermissionSets
|
||||||
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
|
|
||||||
|
class EnvironmentForm(FlaskForm):
|
||||||
|
environment_id = HiddenField()
|
||||||
|
environment_name = HiddenField()
|
||||||
|
role = SelectField(
|
||||||
|
environment_name,
|
||||||
|
choices=ENV_ROLES,
|
||||||
|
default=None,
|
||||||
|
filters=[lambda x: None if x == "None" else x],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsForm(FlaskForm):
|
||||||
|
perms_env_mgmt = BooleanField(
|
||||||
|
translate("portfolios.applications.members.new.manage_envs"), default=False
|
||||||
|
)
|
||||||
|
perms_team_mgmt = BooleanField(
|
||||||
|
translate("portfolios.applications.members.new.manage_team"), default=False
|
||||||
|
)
|
||||||
|
perms_del_env = BooleanField(
|
||||||
|
translate("portfolios.applications.members.new.delete_envs"), default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
_data = super().data
|
||||||
|
perm_sets = []
|
||||||
|
|
||||||
|
if _data["perms_env_mgmt"]:
|
||||||
|
perm_sets.append(PermissionSets.EDIT_APPLICATION_ENVIRONMENTS)
|
||||||
|
|
||||||
|
if _data["perms_team_mgmt"]:
|
||||||
|
perm_sets.append(PermissionSets.EDIT_APPLICATION_TEAM)
|
||||||
|
|
||||||
|
if _data["perms_del_env"]:
|
||||||
|
perm_sets.append(PermissionSets.DELETE_APPLICATION_ENVIRONMENTS)
|
||||||
|
|
||||||
|
return perm_sets
|
||||||
|
|
||||||
|
|
||||||
|
class NewForm(BaseForm):
|
||||||
|
user_data = FormField(BaseNewMemberForm)
|
||||||
|
permission_sets = FormField(PermissionsForm)
|
||||||
|
environment_roles = FieldList(FormField(EnvironmentForm))
|
@ -1,5 +1,5 @@
|
|||||||
|
from atst.models import CSPRole
|
||||||
from atst.utils.localization import translate, translate_duration
|
from atst.utils.localization import translate, translate_duration
|
||||||
from atst.models.environment_role import CSPRole
|
|
||||||
|
|
||||||
|
|
||||||
SERVICE_BRANCHES = [
|
SERVICE_BRANCHES = [
|
||||||
|
27
atst/forms/member.py
Normal file
27
atst/forms/member.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms.fields.html5 import EmailField, TelField
|
||||||
|
from wtforms.validators import Required, Email, Length, Optional
|
||||||
|
from wtforms.fields import StringField
|
||||||
|
|
||||||
|
from atst.forms.validators import IsNumber, PhoneNumber
|
||||||
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
|
|
||||||
|
class NewForm(FlaskForm):
|
||||||
|
first_name = StringField(
|
||||||
|
label=translate("forms.new_member.first_name_label"), validators=[Required()]
|
||||||
|
)
|
||||||
|
last_name = StringField(
|
||||||
|
label=translate("forms.new_member.last_name_label"), validators=[Required()]
|
||||||
|
)
|
||||||
|
email = EmailField(
|
||||||
|
translate("forms.new_member.email_label"), validators=[Required(), Email()]
|
||||||
|
)
|
||||||
|
phone_number = TelField(
|
||||||
|
translate("forms.new_member.phone_number_label"),
|
||||||
|
validators=[Optional(), PhoneNumber()],
|
||||||
|
)
|
||||||
|
dod_id = StringField(
|
||||||
|
translate("forms.new_member.dod_id_label"),
|
||||||
|
validators=[Required(), Length(min=10), IsNumber()],
|
||||||
|
)
|
@ -1,10 +1,9 @@
|
|||||||
from wtforms.fields.html5 import EmailField, TelField
|
from wtforms.validators import Required
|
||||||
from wtforms.validators import Required, Email, Length, Optional
|
|
||||||
from wtforms.fields import StringField, FormField, FieldList, HiddenField
|
from wtforms.fields import StringField, FormField, FieldList, HiddenField
|
||||||
|
|
||||||
from atst.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from .forms import BaseForm
|
from .forms import BaseForm
|
||||||
from atst.forms.validators import IsNumber, PhoneNumber
|
from .member import NewForm as BaseNewMemberForm
|
||||||
from atst.forms.fields import SelectField
|
from atst.forms.fields import SelectField
|
||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
@ -62,24 +61,16 @@ class EditForm(PermissionsForm):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NewForm(PermissionsForm):
|
class NewForm(BaseForm):
|
||||||
first_name = StringField(
|
user_data = FormField(BaseNewMemberForm)
|
||||||
label=translate("forms.new_member.first_name_label"), validators=[Required()]
|
permission_sets = FormField(PermissionsForm)
|
||||||
)
|
|
||||||
last_name = StringField(
|
@property
|
||||||
label=translate("forms.new_member.last_name_label"), validators=[Required()]
|
def update_data(self):
|
||||||
)
|
return {
|
||||||
email = EmailField(
|
"permission_sets": self.data.get("permission_sets").get("permission_sets"),
|
||||||
translate("forms.new_member.email_label"), validators=[Required(), Email()]
|
**self.data.get("user_data"),
|
||||||
)
|
}
|
||||||
phone_number = TelField(
|
|
||||||
translate("forms.new_member.phone_number_label"),
|
|
||||||
validators=[Optional(), PhoneNumber()],
|
|
||||||
)
|
|
||||||
dod_id = StringField(
|
|
||||||
translate("forms.new_member.dod_id_label"),
|
|
||||||
validators=[Required(), Length(min=10), IsNumber()],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AssignPPOCForm(PermissionsForm):
|
class AssignPPOCForm(PermissionsForm):
|
||||||
|
@ -5,14 +5,17 @@ Base = declarative_base()
|
|||||||
from .permissions import Permissions
|
from .permissions import Permissions
|
||||||
from .permission_set import PermissionSet
|
from .permission_set import PermissionSet
|
||||||
from .user import User
|
from .user import User
|
||||||
from .portfolio_role import PortfolioRole
|
from .portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
|
||||||
from .application_role import ApplicationRole
|
from .application_role import ApplicationRole, Status as ApplicationRoleStatus
|
||||||
from .environment_role import EnvironmentRole
|
from .environment_role import EnvironmentRole, CSPRole
|
||||||
from .portfolio import Portfolio
|
from .portfolio import Portfolio
|
||||||
from .application import Application
|
from .application import Application
|
||||||
from .environment import Environment
|
from .environment import Environment
|
||||||
from .attachment import Attachment
|
from .attachment import Attachment
|
||||||
from .audit_event import AuditEvent
|
from .audit_event import AuditEvent
|
||||||
from .invitation import Invitation
|
from .portfolio_invitation import PortfolioInvitation
|
||||||
|
from .application_invitation import ApplicationInvitation
|
||||||
from .task_order import TaskOrder
|
from .task_order import TaskOrder
|
||||||
from .dd_254 import DD254
|
from .dd_254 import DD254
|
||||||
|
|
||||||
|
from .mixins.invites import Status as InvitationStatus
|
||||||
|
41
atst/models/application_invitation.py
Normal file
41
atst/models/application_invitation.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from sqlalchemy import Column, ForeignKey
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import relationship, backref
|
||||||
|
|
||||||
|
from atst.models import Base
|
||||||
|
from atst.models.mixins import TimestampsMixin, AuditableMixin, InvitesMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationInvitation(Base, TimestampsMixin, AuditableMixin, InvitesMixin):
|
||||||
|
__tablename__ = "application_invitations"
|
||||||
|
|
||||||
|
application_role_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("application_roles.id"), index=True
|
||||||
|
)
|
||||||
|
role = relationship(
|
||||||
|
"ApplicationRole",
|
||||||
|
backref=backref("invitations", order_by="ApplicationInvitation.time_created"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def application(self):
|
||||||
|
if self.role: # pragma: no branch
|
||||||
|
return self.role.application
|
||||||
|
|
||||||
|
@property
|
||||||
|
def application_id(self):
|
||||||
|
return self.role.application_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event_details(self):
|
||||||
|
return {"email": self.email, "dod_id": self.user_dod_id}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def history(self):
|
||||||
|
changes = self.get_changes()
|
||||||
|
change_set = {}
|
||||||
|
|
||||||
|
if "status" in changes:
|
||||||
|
change_set["status"] = [s.name for s in changes["status"]]
|
||||||
|
|
||||||
|
return change_set
|
@ -51,6 +51,10 @@ class ApplicationRole(
|
|||||||
"PermissionSet", secondary=application_roles_permission_sets
|
"PermissionSet", secondary=application_roles_permission_sets
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_name(self):
|
||||||
|
return self.user.full_name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<ApplicationRole(application='{}', user_id='{}', id='{}', permissions={})>".format(
|
return "<ApplicationRole(application='{}', user_id='{}', id='{}', permissions={})>".format(
|
||||||
self.application.name, self.user_id, self.id, self.permissions
|
self.application.name, self.user_id, self.id, self.permissions
|
||||||
|
@ -2,3 +2,4 @@ from .timestamps import TimestampsMixin
|
|||||||
from .auditable import AuditableMixin
|
from .auditable import AuditableMixin
|
||||||
from .permissions import PermissionsMixin
|
from .permissions import PermissionsMixin
|
||||||
from .deletable import DeletableMixin
|
from .deletable import DeletableMixin
|
||||||
|
from .invites import InvitesMixin
|
||||||
|
@ -3,12 +3,11 @@ from enum import Enum
|
|||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum, TIMESTAMP, String
|
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum, TIMESTAMP, String
|
||||||
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.orm import relationship, backref
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from atst.models import Base, types
|
from atst.models import types
|
||||||
from atst.models.mixins.timestamps import TimestampsMixin
|
|
||||||
from atst.models.mixins.auditable import AuditableMixin
|
|
||||||
|
|
||||||
|
|
||||||
class Status(Enum):
|
class Status(Enum):
|
||||||
@ -19,24 +18,24 @@ class Status(Enum):
|
|||||||
REJECTED_EXPIRED = "rejected_expired"
|
REJECTED_EXPIRED = "rejected_expired"
|
||||||
|
|
||||||
|
|
||||||
class Invitation(Base, TimestampsMixin, AuditableMixin):
|
class InvitesMixin(object):
|
||||||
__tablename__ = "invitations"
|
|
||||||
|
|
||||||
id = types.Id()
|
id = types.Id()
|
||||||
|
|
||||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
@declared_attr
|
||||||
user = relationship("User", backref="invitations", foreign_keys=[user_id])
|
def user_id(cls):
|
||||||
|
return Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
||||||
|
|
||||||
portfolio_role_id = Column(
|
@declared_attr
|
||||||
UUID(as_uuid=True), ForeignKey("portfolio_roles.id"), index=True
|
def user(cls):
|
||||||
)
|
return relationship("User", foreign_keys=[cls.user_id])
|
||||||
portfolio_role = relationship(
|
|
||||||
"PortfolioRole",
|
|
||||||
backref=backref("invitations", order_by="Invitation.time_created"),
|
|
||||||
)
|
|
||||||
|
|
||||||
inviter_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
@declared_attr
|
||||||
inviter = relationship("User", backref="sent_invites", foreign_keys=[inviter_id])
|
def inviter_id(cls):
|
||||||
|
return Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def inviter(cls):
|
||||||
|
return relationship("User", foreign_keys=[cls.inviter_id])
|
||||||
|
|
||||||
status = Column(SQLAEnum(Status, native_enum=False, default=Status.PENDING))
|
status = Column(SQLAEnum(Status, native_enum=False, default=Status.PENDING))
|
||||||
|
|
||||||
@ -47,8 +46,9 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
|||||||
email = Column(String, nullable=False)
|
email = Column(String, nullable=False)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Invitation(user='{}', portfolio_role='{}', id='{}', email='{}')>".format(
|
role_id = self.role.id if self.role else None
|
||||||
self.user_id, self.portfolio_role_id, self.id, self.email
|
return "<{}(user='{}', role='{}', id='{}', email='{}')>".format(
|
||||||
|
self.__class__.__name__, self.user_id, role_id, self.id, self.email
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -90,14 +90,9 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
|||||||
Status.REVOKED,
|
Status.REVOKED,
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
|
||||||
def portfolio(self):
|
|
||||||
if self.portfolio_role: # pragma: no branch
|
|
||||||
return self.portfolio_role.portfolio
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_name(self):
|
def user_name(self):
|
||||||
return self.portfolio_role.user.full_name
|
return self.role.user.full_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_revokable(self):
|
def is_revokable(self):
|
||||||
@ -110,21 +105,3 @@ class Invitation(Base, TimestampsMixin, AuditableMixin):
|
|||||||
@property
|
@property
|
||||||
def user_dod_id(self):
|
def user_dod_id(self):
|
||||||
return self.user.dod_id if self.user is not None else None
|
return self.user.dod_id if self.user is not None else None
|
||||||
|
|
||||||
@property
|
|
||||||
def event_details(self):
|
|
||||||
return {"email": self.email, "dod_id": self.user_dod_id}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def history(self):
|
|
||||||
changes = self.get_changes()
|
|
||||||
change_set = {}
|
|
||||||
|
|
||||||
if "status" in changes:
|
|
||||||
change_set["status"] = [s.name for s in changes["status"]]
|
|
||||||
|
|
||||||
return change_set
|
|
||||||
|
|
||||||
@property
|
|
||||||
def portfolio_id(self):
|
|
||||||
return self.portfolio_role.portfolio_id
|
|
41
atst/models/portfolio_invitation.py
Normal file
41
atst/models/portfolio_invitation.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from sqlalchemy import Column, ForeignKey
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import relationship, backref
|
||||||
|
|
||||||
|
from atst.models import Base
|
||||||
|
from atst.models.mixins import TimestampsMixin, AuditableMixin, InvitesMixin
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioInvitation(Base, TimestampsMixin, AuditableMixin, InvitesMixin):
|
||||||
|
__tablename__ = "portfolio_invitations"
|
||||||
|
|
||||||
|
portfolio_role_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("portfolio_roles.id"), index=True
|
||||||
|
)
|
||||||
|
role = relationship(
|
||||||
|
"PortfolioRole",
|
||||||
|
backref=backref("invitations", order_by="PortfolioInvitation.time_created"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def portfolio(self):
|
||||||
|
if self.role: # pragma: no branch
|
||||||
|
return self.role.portfolio
|
||||||
|
|
||||||
|
@property
|
||||||
|
def portfolio_id(self):
|
||||||
|
return self.role.portfolio_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event_details(self):
|
||||||
|
return {"email": self.email, "dod_id": self.user_dod_id}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def history(self):
|
||||||
|
changes = self.get_changes()
|
||||||
|
change_set = {}
|
||||||
|
|
||||||
|
if "status" in changes:
|
||||||
|
change_set["status"] = [s.name for s in changes["status"]]
|
||||||
|
|
||||||
|
return change_set
|
@ -4,6 +4,8 @@ from sqlalchemy.dialects.postgresql import UUID
|
|||||||
|
|
||||||
from atst.models import Base, types, mixins
|
from atst.models import Base, types, mixins
|
||||||
from atst.models.permissions import Permissions
|
from atst.models.permissions import Permissions
|
||||||
|
from atst.models.portfolio_invitation import PortfolioInvitation
|
||||||
|
from atst.models.application_invitation import ApplicationInvitation
|
||||||
|
|
||||||
|
|
||||||
users_permission_sets = Table(
|
users_permission_sets = Table(
|
||||||
@ -31,6 +33,20 @@ class User(
|
|||||||
primaryjoin="and_(ApplicationRole.user_id==User.id, ApplicationRole.deleted==False)",
|
primaryjoin="and_(ApplicationRole.user_id==User.id, ApplicationRole.deleted==False)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
portfolio_invitations = relationship(
|
||||||
|
"PortfolioInvitation", foreign_keys=PortfolioInvitation.user_id
|
||||||
|
)
|
||||||
|
sent_portfolio_invitations = relationship(
|
||||||
|
"PortfolioInvitation", foreign_keys=PortfolioInvitation.inviter_id
|
||||||
|
)
|
||||||
|
|
||||||
|
application_invitations = relationship(
|
||||||
|
"ApplicationInvitation", foreign_keys=ApplicationInvitation.user_id
|
||||||
|
)
|
||||||
|
sent_application_invitations = relationship(
|
||||||
|
"ApplicationInvitation", foreign_keys=ApplicationInvitation.inviter_id
|
||||||
|
)
|
||||||
|
|
||||||
email = Column(String)
|
email = Column(String)
|
||||||
dod_id = Column(String, unique=True, nullable=False)
|
dod_id = Column(String, unique=True, nullable=False)
|
||||||
first_name = Column(String)
|
first_name = Column(String)
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
from flask import render_template
|
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.environments import Environments
|
||||||
from atst.domain.applications import Applications
|
from atst.domain.applications import Applications
|
||||||
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.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
|
from atst.domain.exceptions import AlreadyExistsError
|
||||||
|
from atst.forms.application_member import NewForm as NewMemberForm
|
||||||
|
from atst.models import Permissions
|
||||||
|
from atst.services.invitation import Invitation as InvitationService
|
||||||
|
from atst.utils.flash import formatted_flash as flash
|
||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
|
|
||||||
@ -42,8 +46,57 @@ def team(application_id):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
env_roles = [
|
||||||
|
{"environment_id": e.id, "environment_name": e.name}
|
||||||
|
for e in application.environments
|
||||||
|
]
|
||||||
|
member_form = NewMemberForm(data={"environment_roles": env_roles})
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"portfolios/applications/team.html",
|
"portfolios/applications/team.html",
|
||||||
application=application,
|
application=application,
|
||||||
environment_users=environment_users,
|
environment_users=environment_users,
|
||||||
|
member_form=member_form,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@applications_bp.route("/application/<application_id>/members/new", methods=["POST"])
|
||||||
|
@user_can(
|
||||||
|
Permissions.CREATE_APPLICATION_MEMBER, message="create new application member"
|
||||||
|
)
|
||||||
|
def create_member(application_id):
|
||||||
|
application = Applications.get(application_id)
|
||||||
|
form = NewMemberForm(http_request.form)
|
||||||
|
|
||||||
|
if form.validate():
|
||||||
|
try:
|
||||||
|
member = Applications.create_member(
|
||||||
|
application,
|
||||||
|
form.user_data.data,
|
||||||
|
permission_sets=form.permission_sets.data,
|
||||||
|
environment_roles_data=form.environment_roles.data,
|
||||||
|
)
|
||||||
|
|
||||||
|
invite_service = InvitationService(
|
||||||
|
g.current_user, member, form.user_data.data.get("email")
|
||||||
|
)
|
||||||
|
invite_service.invite()
|
||||||
|
|
||||||
|
flash("new_portfolio_member", new_member=member)
|
||||||
|
|
||||||
|
except AlreadyExistsError:
|
||||||
|
return render_template(
|
||||||
|
"error.html", message="There was an error processing your request."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
# TODO: flash error message
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
url_for(
|
||||||
|
"applications.team",
|
||||||
|
application_id=application_id,
|
||||||
|
fragment="application-members",
|
||||||
|
_anchor="application-members",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from flask import g, redirect, url_for, render_template
|
from flask import g, redirect, url_for, render_template
|
||||||
|
|
||||||
from . import portfolios_bp
|
from . import portfolios_bp
|
||||||
from atst.domain.invitations import Invitations
|
from atst.domain.invitations import PortfolioInvitations
|
||||||
from atst.queue import queue
|
from atst.queue import queue
|
||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
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
|
||||||
@ -9,7 +9,9 @@ from atst.models.permissions import Permissions
|
|||||||
|
|
||||||
|
|
||||||
def send_invite_email(owner_name, token, new_member_email):
|
def send_invite_email(owner_name, token, new_member_email):
|
||||||
body = render_template("emails/invitation.txt", owner=owner_name, token=token)
|
body = render_template(
|
||||||
|
"emails/portfolio/invitation.txt", owner=owner_name, token=token
|
||||||
|
)
|
||||||
queue.send_mail(
|
queue.send_mail(
|
||||||
[new_member_email],
|
[new_member_email],
|
||||||
"{} has invited you to a JEDI Cloud Portfolio".format(owner_name),
|
"{} has invited you to a JEDI Cloud Portfolio".format(owner_name),
|
||||||
@ -19,7 +21,7 @@ def send_invite_email(owner_name, token, new_member_email):
|
|||||||
|
|
||||||
@portfolios_bp.route("/portfolios/invitations/<token>", methods=["GET"])
|
@portfolios_bp.route("/portfolios/invitations/<token>", methods=["GET"])
|
||||||
def accept_invitation(token):
|
def accept_invitation(token):
|
||||||
invite = Invitations.accept(g.current_user, token)
|
invite = PortfolioInvitations.accept(g.current_user, token)
|
||||||
|
|
||||||
for task_order in invite.portfolio.task_orders:
|
for task_order in invite.portfolio.task_orders:
|
||||||
if g.current_user in task_order.officers:
|
if g.current_user in task_order.officers:
|
||||||
@ -37,7 +39,7 @@ def accept_invitation(token):
|
|||||||
)
|
)
|
||||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="revoke invitation")
|
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="revoke invitation")
|
||||||
def revoke_invitation(portfolio_id, token):
|
def revoke_invitation(portfolio_id, token):
|
||||||
Invitations.revoke(token)
|
PortfolioInvitations.revoke(token)
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
@ -54,7 +56,7 @@ def revoke_invitation(portfolio_id, token):
|
|||||||
)
|
)
|
||||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="resend invitation")
|
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="resend invitation")
|
||||||
def resend_invitation(portfolio_id, token):
|
def resend_invitation(portfolio_id, token):
|
||||||
invite = Invitations.resend(g.current_user, token)
|
invite = PortfolioInvitations.resend(g.current_user, token)
|
||||||
send_invite_email(g.current_user.full_name, invite.token, invite.email)
|
send_invite_email(g.current_user.full_name, invite.token, invite.email)
|
||||||
flash("resend_portfolio_invitation", user_name=invite.user_name)
|
flash("resend_portfolio_invitation", user_name=invite.user_name)
|
||||||
return redirect(
|
return redirect(
|
||||||
|
@ -34,9 +34,9 @@ def create_member(portfolio_id):
|
|||||||
|
|
||||||
if form.validate():
|
if form.validate():
|
||||||
try:
|
try:
|
||||||
member = Portfolios.create_member(portfolio, form.data)
|
member = Portfolios.create_member(portfolio, form.update_data)
|
||||||
invite_service = InvitationService(
|
invite_service = InvitationService(
|
||||||
g.current_user, member, form.data.get("email")
|
g.current_user, member, form.update_data.get("email")
|
||||||
)
|
)
|
||||||
invite_service.invite()
|
invite_service.invite()
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ 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.database import db
|
from atst.database import db
|
||||||
from atst.domain.exceptions import NotFoundError, NoAccessError
|
from atst.domain.exceptions import NotFoundError, NoAccessError
|
||||||
from atst.domain.invitations import Invitations
|
from atst.domain.invitations import PortfolioInvitations
|
||||||
from atst.domain.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
from atst.forms.officers import EditTaskOrderOfficersForm
|
from atst.forms.officers import EditTaskOrderOfficersForm
|
||||||
@ -57,7 +57,7 @@ def resend_invite(task_order_id):
|
|||||||
if not officer:
|
if not officer:
|
||||||
raise NotFoundError("officer")
|
raise NotFoundError("officer")
|
||||||
|
|
||||||
invitation = Invitations.lookup_by_portfolio_and_user(portfolio, officer)
|
invitation = PortfolioInvitations.lookup_by_resource_and_user(portfolio, officer)
|
||||||
|
|
||||||
if not invitation:
|
if not invitation:
|
||||||
raise NotFoundError("invitation")
|
raise NotFoundError("invitation")
|
||||||
@ -65,11 +65,11 @@ def resend_invite(task_order_id):
|
|||||||
if not invitation.can_resend:
|
if not invitation.can_resend:
|
||||||
raise NoAccessError("invitation")
|
raise NoAccessError("invitation")
|
||||||
|
|
||||||
Invitations.revoke(token=invitation.token)
|
PortfolioInvitations.revoke(token=invitation.token)
|
||||||
|
|
||||||
invite_service = InvitationService(
|
invite_service = InvitationService(
|
||||||
g.current_user,
|
g.current_user,
|
||||||
invitation.portfolio_role,
|
invitation.role,
|
||||||
invitation.email,
|
invitation.email,
|
||||||
subject=invite_type_info["subject"],
|
subject=invite_type_info["subject"],
|
||||||
email_template=invite_type_info["template"],
|
email_template=invite_type_info["template"],
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
|
||||||
from atst.domain.invitations import Invitations
|
from atst.domain.invitations import PortfolioInvitations, ApplicationInvitations
|
||||||
from atst.queue import queue
|
from atst.queue import queue
|
||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.domain.portfolio_roles import PortfolioRoles
|
from atst.domain.portfolio_roles import PortfolioRoles
|
||||||
|
from atst.models import ApplicationRole, PortfolioRole
|
||||||
|
|
||||||
OFFICER_INVITATIONS = {
|
OFFICER_INVITATIONS = {
|
||||||
"ko_invite": {
|
"ko_invite": {
|
||||||
"role": "contracting_officer",
|
"role": "contracting_officer",
|
||||||
"subject": "Review a task order",
|
"subject": "Review a task order",
|
||||||
"template": "emails/invitation.txt",
|
"template": "emails/portfolio/invitation.txt",
|
||||||
},
|
},
|
||||||
"cor_invite": {
|
"cor_invite": {
|
||||||
"role": "contracting_officer_representative",
|
"role": "contracting_officer_representative",
|
||||||
"subject": "Help with a task order",
|
"subject": "Help with a task order",
|
||||||
"template": "emails/invitation.txt",
|
"template": "emails/portfolio/invitation.txt",
|
||||||
},
|
},
|
||||||
"so_invite": {
|
"so_invite": {
|
||||||
"role": "security_officer",
|
"role": "security_officer",
|
||||||
"subject": "Review security for a task order",
|
"subject": "Review security for a task order",
|
||||||
"template": "emails/invitation.txt",
|
"template": "emails/portfolio/invitation.txt",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,20 +48,30 @@ def update_officer_invitations(user, task_order):
|
|||||||
|
|
||||||
|
|
||||||
class Invitation:
|
class Invitation:
|
||||||
def __init__(
|
def __init__(self, inviter, member, email, subject="", email_template=None):
|
||||||
self,
|
|
||||||
inviter,
|
|
||||||
member,
|
|
||||||
email,
|
|
||||||
subject="{} has invited you to a JEDI cloud portfolio",
|
|
||||||
email_template="emails/invitation.txt",
|
|
||||||
):
|
|
||||||
self.inviter = inviter
|
self.inviter = inviter
|
||||||
self.member = member
|
self.member = member
|
||||||
self.email = email
|
self.email = email
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.email_template = email_template
|
self.email_template = email_template
|
||||||
|
|
||||||
|
if isinstance(member, PortfolioRole):
|
||||||
|
self.email_template = (
|
||||||
|
self.email_template or "emails/portfolio/invitation.txt"
|
||||||
|
)
|
||||||
|
self.subject = (
|
||||||
|
self.subject or "{} has invited you to a JEDI cloud portfolio"
|
||||||
|
)
|
||||||
|
self.domain_class = PortfolioInvitations
|
||||||
|
elif isinstance(member, ApplicationRole):
|
||||||
|
self.email_template = (
|
||||||
|
self.email_template or "emails/application/invitation.txt"
|
||||||
|
)
|
||||||
|
self.subject = (
|
||||||
|
self.subject or "{} has invited you to a JEDI cloud application"
|
||||||
|
)
|
||||||
|
self.domain_class = ApplicationInvitations
|
||||||
|
|
||||||
def invite(self):
|
def invite(self):
|
||||||
invite = self._create_invite()
|
invite = self._create_invite()
|
||||||
self._send_invite_email(invite.token)
|
self._send_invite_email(invite.token)
|
||||||
@ -68,7 +79,7 @@ class Invitation:
|
|||||||
return invite
|
return invite
|
||||||
|
|
||||||
def _create_invite(self):
|
def _create_invite(self):
|
||||||
return Invitations.create(self.inviter, self.member, self.email)
|
return self.domain_class.create(self.inviter, self.member, self.email)
|
||||||
|
|
||||||
def _send_invite_email(self, token):
|
def _send_invite_email(self, token):
|
||||||
body = render_template(
|
body = render_template(
|
||||||
|
@ -161,6 +161,13 @@ MESSAGES = {
|
|||||||
""",
|
""",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
|
"new_application_member": {
|
||||||
|
"title_template": translate("flash.success"),
|
||||||
|
"message_template": """
|
||||||
|
<p>{{ "flash.new_application_member" | translate({ "user_name": new_member.user_name }) }}</p>
|
||||||
|
""",
|
||||||
|
"category": "success",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,22 @@
|
|||||||
import { emitEvent } from '../lib/emitters'
|
import { emitEvent } from '../lib/emitters'
|
||||||
|
import nestedcheckboxinput from './nested_checkbox_input'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'checkboxinput',
|
name: 'checkboxinput',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
nestedcheckboxinput,
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
name: String,
|
name: String,
|
||||||
|
initialChecked: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
isChecked: this.initialChecked,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import FormMixin from '../../mixins/form'
|
import FormMixin from '../../mixins/form'
|
||||||
import textinput from '../text_input'
|
import textinput from '../text_input'
|
||||||
import optionsinput from '../options_input'
|
import optionsinput from '../options_input'
|
||||||
|
import checkboxinput from '../checkbox_input'
|
||||||
import Selector from '../selector'
|
import Selector from '../selector'
|
||||||
import Modal from '../../mixins/modal'
|
import Modal from '../../mixins/modal'
|
||||||
import toggler from '../toggler'
|
import toggler from '../toggler'
|
||||||
@ -16,6 +17,7 @@ export default {
|
|||||||
Selector,
|
Selector,
|
||||||
textinput,
|
textinput,
|
||||||
optionsinput,
|
optionsinput,
|
||||||
|
checkboxinput,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -45,6 +47,9 @@ export default {
|
|||||||
this.step += 1
|
this.step += 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
previous: function() {
|
||||||
|
this.step -= 1
|
||||||
|
},
|
||||||
goToStep: function(step) {
|
goToStep: function(step) {
|
||||||
if (this._checkIsValid()) {
|
if (this._checkIsValid()) {
|
||||||
this.step = step
|
this.step = step
|
||||||
|
31
js/components/nested_checkbox_input.js
Normal file
31
js/components/nested_checkbox_input.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { emitEvent } from '../lib/emitters'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'nestedcheckboxinput',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
isParentChecked: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
isChecked: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updated: function() {
|
||||||
|
if (!this.isParentChecked) {
|
||||||
|
this.isChecked = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onInput: function(e) {
|
||||||
|
emitEvent('field-change', this, {
|
||||||
|
value: e.target.checked,
|
||||||
|
name: this.name,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -18,6 +18,7 @@ export default {
|
|||||||
showError: showError,
|
showError: showError,
|
||||||
showValid: !showError && !!this.initialValue,
|
showValid: !showError && !!this.initialValue,
|
||||||
validationError: this.initialErrors.join(' '),
|
validationError: this.initialErrors.join(' '),
|
||||||
|
value: this.initialValue,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import optionsinput from './components/options_input'
|
|||||||
import multicheckboxinput from './components/multi_checkbox_input'
|
import multicheckboxinput from './components/multi_checkbox_input'
|
||||||
import textinput from './components/text_input'
|
import textinput from './components/text_input'
|
||||||
import checkboxinput from './components/checkbox_input'
|
import checkboxinput from './components/checkbox_input'
|
||||||
|
import nestedcheckboxinput from './components/nested_checkbox_input'
|
||||||
import EditOfficerForm from './components/forms/edit_officer_form'
|
import EditOfficerForm from './components/forms/edit_officer_form'
|
||||||
import poc from './components/forms/poc'
|
import poc from './components/forms/poc'
|
||||||
import oversight from './components/forms/oversight'
|
import oversight from './components/forms/oversight'
|
||||||
@ -74,6 +75,7 @@ const app = new Vue({
|
|||||||
KoReview,
|
KoReview,
|
||||||
BaseForm,
|
BaseForm,
|
||||||
DeleteConfirmation,
|
DeleteConfirmation,
|
||||||
|
nestedcheckboxinput,
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function() {
|
mounted: function() {
|
||||||
|
@ -35,7 +35,7 @@ export default {
|
|||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
mask: emailMask,
|
mask: emailMask,
|
||||||
match: /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/i,
|
match: /^.+@[^.].*\.[a-z]{2,10}$/,
|
||||||
unmask: [],
|
unmask: [],
|
||||||
validationError: 'Please enter a valid e-mail address',
|
validationError: 'Please enter a valid e-mail address',
|
||||||
},
|
},
|
||||||
|
@ -46,6 +46,14 @@
|
|||||||
flex-basis: 16.66%;
|
flex-basis: 16.66%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.form-col--quarter {
|
||||||
|
flex-basis: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.form-col--three-quarters {
|
||||||
|
flex-basis: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
.usa-input {
|
.usa-input {
|
||||||
margin-left: ($gap * 4);
|
margin-left: ($gap * 4);
|
||||||
margin-right: ($gap * 4);
|
margin-right: ($gap * 4);
|
||||||
@ -171,3 +179,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input__inline-fields {
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
&.input__inline-fields--indented {
|
||||||
|
margin-left: $gap*4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&> fieldset.usa-input__choices label {
|
||||||
|
display: inline;
|
||||||
|
font-weight: $font-normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -43,6 +43,7 @@ body {
|
|||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
padding: $gap * 2;
|
padding: $gap * 2;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-ms-overflow-style: scrollbar;
|
-ms-overflow-style: scrollbar;
|
||||||
@ -52,7 +53,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include media($medium-screen) {
|
@include media($medium-screen) {
|
||||||
padding: $gap * 4;
|
padding: $gap * 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
@ -186,6 +187,7 @@ body {
|
|||||||
|
|
||||||
.form-row {
|
.form-row {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
.form-col {
|
.form-col {
|
||||||
.usa-input {
|
.usa-input {
|
||||||
|
@ -21,4 +21,9 @@
|
|||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: $gap * 3;
|
margin-bottom: $gap * 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-group__action--left {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,3 +59,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.environment-roles-new {
|
||||||
|
margin-top: 5*$gap;
|
||||||
|
margin-bottom: 8*$gap;
|
||||||
|
|
||||||
|
.usa-input {
|
||||||
|
margin: 2rem 0 2rem 0;
|
||||||
|
|
||||||
|
.usa-input__title-inline {
|
||||||
|
line-height: $hit-area;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
font-size: $lead-font-size;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.environment-roles-new__head {
|
||||||
|
font-weight: $font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.environment-name--gray {
|
||||||
|
font-weight: $font-normal;
|
||||||
|
color: $color-gray-medium;
|
||||||
|
}
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
{% macro CheckboxInput(
|
{% macro CheckboxInput(
|
||||||
field,
|
field,
|
||||||
label=field.label | striptags,
|
label=field.label,
|
||||||
inline=False,
|
inline=False,
|
||||||
classes="") -%}
|
classes="") -%}
|
||||||
<checkboxinput name='{{ field.name }}' inline-template key='{{ field.name }}'>
|
<checkboxinput
|
||||||
<div class='usa-input {{ classes }} {% if field.errors %}usa-input--error{% endif %}'>
|
name='{{ field.name }}'
|
||||||
|
inline-template
|
||||||
|
key='{{ field.name }}'
|
||||||
|
v-bind:initial-checked='{{ field.data|string|lower }}'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class='usa-input {{ classes }} {% if field.errors %}usa-input--error{% endif %}'>
|
||||||
|
|
||||||
<fieldset data-ally-disabled="true" v-on:change="onInput" class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
<fieldset data-ally-disabled="true" v-on:change="onInput" class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
||||||
<legend>
|
<legend>
|
||||||
{{ field() }}
|
{{ field(**{"v-model": "isChecked"}) }}
|
||||||
<label for={{field.name}}>
|
{{ label | safe }}
|
||||||
{{ label }}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{% if field.description %}
|
{% if field.description %}
|
||||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</legend>
|
</legend>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
{% if caller %}
|
||||||
|
{{ caller() }}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</checkboxinput>
|
</checkboxinput>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
10
templates/emails/application/invitation.txt
Normal file
10
templates/emails/application/invitation.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "emails/base.txt" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
Join this JEDI Cloud Application
|
||||||
|
{{ owner }} has invited you to join a JEDI Cloud Application. Login now to view or use your JEDI Cloud resources.
|
||||||
|
|
||||||
|
{# url_for("application.accept_invitation", token=token, _external=True) #}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -1,7 +1,4 @@
|
|||||||
Join this JEDI Cloud Portfolio
|
{% block content %}{% endblock %}
|
||||||
{{ owner }} has invited you to join a JEDI Cloud Portfolio. Login now to view or use your JEDI Cloud resources.
|
|
||||||
|
|
||||||
{{ url_for("portfolios.accept_invitation", token=token, _external=True) }}
|
|
||||||
|
|
||||||
What is JEDI Cloud?
|
What is JEDI Cloud?
|
||||||
JEDI Cloud is a DoD enterprise-wide solution for commercial cloud services.
|
JEDI Cloud is a DoD enterprise-wide solution for commercial cloud services.
|
10
templates/emails/portfolio/invitation.txt
Normal file
10
templates/emails/portfolio/invitation.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "emails/base.txt" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
Join this JEDI Cloud Portfolio
|
||||||
|
{{ owner }} has invited you to join a JEDI Cloud Portfolio. Login now to view or use your JEDI Cloud resources.
|
||||||
|
|
||||||
|
{{ url_for("portfolios.accept_invitation", token=token, _external=True) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -21,23 +21,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.first_name, validation='requiredField') }}
|
{{ TextInput(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.last_name, validation='requiredField') }}
|
{{ TextInput(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.email, validation='email') }}
|
{{ TextInput(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.phone_number, validation='usPhone', optional=True) }}
|
{{ TextInput(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.dod_id, validation='dodId') }}
|
{{ TextInput(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,10 +61,10 @@
|
|||||||
{{ "portfolios.admin.permissions_info" | translate }}
|
{{ "portfolios.admin.permissions_info" | translate }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{ SimpleOptionsInput(member_form.perms_app_mgmt) }}
|
{{ SimpleOptionsInput(member_form.permission_sets.perms_app_mgmt) }}
|
||||||
{{ SimpleOptionsInput(member_form.perms_funding) }}
|
{{ SimpleOptionsInput(member_form.permission_sets.perms_funding) }}
|
||||||
{{ SimpleOptionsInput(member_form.perms_reporting) }}
|
{{ SimpleOptionsInput(member_form.permission_sets.perms_reporting) }}
|
||||||
{{ SimpleOptionsInput(member_form.perms_portfolio_mgmt) }}
|
{{ SimpleOptionsInput(member_form.permission_sets.perms_portfolio_mgmt) }}
|
||||||
<div class='action-group'>
|
<div class='action-group'>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
|
136
templates/fragments/applications/add_new_application_member.html
Normal file
136
templates/fragments/applications/add_new_application_member.html
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
{% from "components/checkbox_input.html" import CheckboxInput %}
|
||||||
|
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||||
|
|
||||||
|
{% set step_one %}
|
||||||
|
<div class="modal__form--header">
|
||||||
|
<h1>Invite new member</h1>
|
||||||
|
</div>
|
||||||
|
<div class='form-row'>
|
||||||
|
<div class='form-col form-col--half'>
|
||||||
|
{{ TextInput(member_form.user_data.first_name, validation='requiredField') }}
|
||||||
|
</div>
|
||||||
|
<div class='form-col form-col--half'>
|
||||||
|
{{ TextInput(member_form.user_data.last_name, validation='requiredField') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='form-row'>
|
||||||
|
<div class='form-col form-col--half'>
|
||||||
|
{{ TextInput(member_form.user_data.email, validation='email') }}
|
||||||
|
</div>
|
||||||
|
<div class='form-col form-col--half'>
|
||||||
|
{{ TextInput(member_form.user_data.phone_number, validation='usPhone', optional=True) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='form-row'>
|
||||||
|
<div class='form-col form-col--half'>
|
||||||
|
{{ TextInput(member_form.user_data.dod_id, validation='dodId') }}
|
||||||
|
</div>
|
||||||
|
<div class='form-col form-col--half'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='action-group'>
|
||||||
|
<input
|
||||||
|
type='button'
|
||||||
|
v-on:click="next()"
|
||||||
|
v-bind:disabled="invalid"
|
||||||
|
class='action-group__action usa-button'
|
||||||
|
value='Next'>
|
||||||
|
<a class='action-group__action icon-link icon-link--default' v-on:click="closeModal('{{ new_port_mem }}')">{{ "common.cancel" | translate }}</a>
|
||||||
|
</div>
|
||||||
|
{% endset %}
|
||||||
|
{% set step_two %}
|
||||||
|
<div class="modal__form">
|
||||||
|
<div class="modal__form--header">
|
||||||
|
<h1>{{ "portfolios.applications.members.new.assign_roles" | translate }}</h1>
|
||||||
|
<a class='icon-link'>
|
||||||
|
{{ Icon('info') }}
|
||||||
|
{{ "portfolios.applications.members.new.learn_more" | translate }}
|
||||||
|
</a>
|
||||||
|
<div class="environment-roles-new">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-col form-col--quarter">
|
||||||
|
<span class="environment-roles-new__head">
|
||||||
|
{{ "common.resource_names.environments" | translate }}:
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-col form-col--three-quarters">
|
||||||
|
<span class="environment-roles-new__head">
|
||||||
|
{{ "common.choose_role" | translate }}:
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for environment_data in member_form.environment_roles %}
|
||||||
|
<optionsinput inline-template
|
||||||
|
v-bind:initial-value="'{{ environment_data.role.data | string }}'"
|
||||||
|
>
|
||||||
|
<div class="usa-input">
|
||||||
|
<fieldset data-ally-disabled="true" class="usa-input__choices">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-col form-col--quarter">
|
||||||
|
<legend>
|
||||||
|
<div v-bind:class='["usa-input__title-inline", {"environment-name--gray": value === "None" }]'>
|
||||||
|
{{ environment_data.environment_name.data }}
|
||||||
|
</div>
|
||||||
|
</legend>
|
||||||
|
</div>
|
||||||
|
<div class="form-col form-col--three-quarters">
|
||||||
|
{{ environment_data.role(**{"v-model": "value"}) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</optionsinput>
|
||||||
|
{{ environment_data.environment_id() }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<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") }}
|
||||||
|
{% call CheckboxInput(member_form.permission_sets.perms_env_mgmt, classes="input__inline-fields") %}
|
||||||
|
{% set field=member_form.permission_sets.perms_del_env %}
|
||||||
|
<nestedcheckboxinput
|
||||||
|
name='{{ field.name }}'
|
||||||
|
inline-template
|
||||||
|
key='{{ field.name }}'
|
||||||
|
v-bind:is-parent-checked="isChecked"
|
||||||
|
>
|
||||||
|
<div class="usa-input input__inline-fields input__inline-fields--indented">
|
||||||
|
<fieldset data-ally-disabled="true" class="usa-input__choices usa-input__choices--inline">
|
||||||
|
<legend>
|
||||||
|
<input
|
||||||
|
id="permission_sets-perms_del_env"
|
||||||
|
name="permission_sets-perms_del_env"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="isChecked"
|
||||||
|
v-bind:disabled="!$parent.isChecked">
|
||||||
|
{{ field.label | safe }}
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</checkboxinput>
|
||||||
|
{% endcall %}
|
||||||
|
</div>
|
||||||
|
<div class='action-group'>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
class='action-group__action usa-button'
|
||||||
|
form="add-app-mem"
|
||||||
|
value='Invite member'>
|
||||||
|
<a class='action-group__action icon-link icon-link--default' v-on:click="closeModal('{{ new_port_mem }}')">{{ "common.cancel" | translate }}</a>
|
||||||
|
<input
|
||||||
|
type='button'
|
||||||
|
v-on:click="previous()"
|
||||||
|
class='action-group__action usa-button action-group__action--left'
|
||||||
|
value='Previous step'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endset %}
|
||||||
|
{{ MultiStepModalForm(
|
||||||
|
'add-app-mem',
|
||||||
|
member_form,
|
||||||
|
url_for("applications.create_member", application_id=application.id),
|
||||||
|
[step_one, step_two],
|
||||||
|
button_text=("portfolios.admin.add_new_member" | translate),
|
||||||
|
button_icon="plus-circle-solid",
|
||||||
|
) }}
|
@ -28,93 +28,95 @@
|
|||||||
{% if g.matchesPath("application-members") %}
|
{% if g.matchesPath("application-members") %}
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form>
|
<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">
|
<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'>
|
|
||||||
{{ Icon('info') }}
|
|
||||||
{{ "portfolios.admin.settings_info" | translate }}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
<a class='icon-link'>
|
||||||
|
{{ Icon('info') }}
|
||||||
<div class="accordion-table accordion-table-list">
|
{{ "portfolios.admin.settings_info" | translate }}
|
||||||
<div class="accordion-table__head">
|
</a>
|
||||||
<span>
|
|
||||||
<span>
|
|
||||||
{{ "portfolios.applications.team_settings.user" | translate }}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{{ "portfolios.applications.team_settings.section.table.environment_management" | translate }}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{{ "portfolios.applications.team_settings.section.table.team_management" | translate }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span class="icon-link" />
|
|
||||||
</div>
|
|
||||||
<ul class="accordion-table__items">
|
|
||||||
{% for member in application.members %}
|
|
||||||
{% set user = member.user %}
|
|
||||||
{% set user_info = environment_users[user.id] %}
|
|
||||||
{% set user_permissions = user_info["permissions"] %}
|
|
||||||
|
|
||||||
|
|
||||||
<toggler inline-template>
|
|
||||||
<li class="accordion-table__item">
|
|
||||||
<div class="accordion-table__item-content">
|
|
||||||
<span>
|
|
||||||
{{ name }}
|
|
||||||
<span>{{ user.full_name }}</span>
|
|
||||||
<span>{{ user_permissions["delete_access"] }}</span>
|
|
||||||
<span>{{ user_permissions["environment_management"] }}</span>
|
|
||||||
<span>{{ user_permissions["team_management"] }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="icon-link icon-link--large accordion-table__item__toggler">
|
|
||||||
{% set open_html %}
|
|
||||||
{{ "common.show" | translate }} {{ "portfolios.applications.team_settings.environments" | translate }} ({{ user_info['environments'] | length }})
|
|
||||||
{% endset %}
|
|
||||||
|
|
||||||
{% set close_html %}
|
|
||||||
{{ "common.hide" | translate }} {{ "portfolios.applications.team_settings.environments" | translate }} ({{ user_info['environments'] | length }})
|
|
||||||
{% endset %}
|
|
||||||
|
|
||||||
{{
|
|
||||||
ToggleButton(
|
|
||||||
open_html=open_html,
|
|
||||||
close_html=close_html,
|
|
||||||
section_name="environments"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% call ToggleSection(section_name="environments") %}
|
|
||||||
<ul>
|
|
||||||
{% for environment in user_info["environments"] %}
|
|
||||||
<li>
|
|
||||||
<div class="accordion-table__item-content">
|
|
||||||
{{ environment.name }}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endcall %}
|
|
||||||
</li>
|
|
||||||
</toggler>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="accordion-table accordion-table-list">
|
||||||
|
<div class="accordion-table__head">
|
||||||
|
<span>
|
||||||
|
<span>
|
||||||
|
{{ "portfolios.applications.team_settings.user" | translate }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ "portfolios.applications.team_settings.section.table.environment_management" | translate }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ "portfolios.applications.team_settings.section.table.team_management" | translate }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="icon-link" />
|
||||||
|
</div>
|
||||||
|
<ul class="accordion-table__items">
|
||||||
|
{% for member in application.members %}
|
||||||
|
{% set user = member.user %}
|
||||||
|
{% set user_info = environment_users[user.id] %}
|
||||||
|
{% set user_permissions = user_info["permissions"] %}
|
||||||
|
|
||||||
|
|
||||||
|
<toggler inline-template>
|
||||||
|
<li class="accordion-table__item">
|
||||||
|
<div class="accordion-table__item-content">
|
||||||
|
<span>
|
||||||
|
{{ name }}
|
||||||
|
<span>{{ user.full_name }}</span>
|
||||||
|
<span>{{ user_permissions["delete_access"] }}</span>
|
||||||
|
<span>{{ user_permissions["environment_management"] }}</span>
|
||||||
|
<span>{{ user_permissions["team_management"] }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="icon-link icon-link--large accordion-table__item__toggler">
|
||||||
|
{% set open_html %}
|
||||||
|
{{ "common.show" | translate }} {{ "portfolios.applications.team_settings.environments" | translate }} ({{ user_info['environments'] | length }})
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{% set close_html %}
|
||||||
|
{{ "common.hide" | translate }} {{ "portfolios.applications.team_settings.environments" | translate }} ({{ user_info['environments'] | length }})
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{{
|
||||||
|
ToggleButton(
|
||||||
|
open_html=open_html,
|
||||||
|
close_html=close_html,
|
||||||
|
section_name="environments"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% call ToggleSection(section_name="environments") %}
|
||||||
|
<ul>
|
||||||
|
{% for environment in user_info["environments"] %}
|
||||||
|
<li>
|
||||||
|
<div class="accordion-table__item-content">
|
||||||
|
{{ environment.name }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endcall %}
|
||||||
|
</li>
|
||||||
|
</toggler>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="members-table-footer">
|
<div class="members-table-footer">
|
||||||
<div class="action-group save">
|
<div class="action-group save">
|
||||||
|
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
|
||||||
|
{% include "fragments/applications/add_new_application_member.html" %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from atst.models import CSPRole
|
||||||
from atst.domain.applications import Applications
|
from atst.domain.applications import Applications
|
||||||
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
@ -100,3 +102,29 @@ def test_delete_application(session):
|
|||||||
|
|
||||||
# changes are flushed
|
# changes are flushed
|
||||||
assert not session.dirty
|
assert not session.dirty
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_member():
|
||||||
|
application = ApplicationFactory.create()
|
||||||
|
env1 = EnvironmentFactory.create(application=application)
|
||||||
|
env2 = EnvironmentFactory.create(application=application)
|
||||||
|
user_data = UserFactory.dictionary()
|
||||||
|
permission_set_names = [PermissionSets.EDIT_APPLICATION_TEAM]
|
||||||
|
|
||||||
|
member_role = Applications.create_member(
|
||||||
|
application,
|
||||||
|
user_data,
|
||||||
|
permission_set_names,
|
||||||
|
environment_roles_data=[
|
||||||
|
{"environment_id": env1.id, "role": CSPRole.BASIC_ACCESS.value},
|
||||||
|
{"environment_id": env2.id, "role": None},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert member_role.user.dod_id == user_data["dod_id"]
|
||||||
|
# view application AND edit application team
|
||||||
|
assert len(member_role.permission_sets) == 2
|
||||||
|
|
||||||
|
env_roles = member_role.user.environment_roles
|
||||||
|
assert len(env_roles) == 1
|
||||||
|
assert env_roles[0].environment == env1
|
||||||
|
@ -3,33 +3,32 @@ import pytest
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from atst.domain.invitations import (
|
from atst.domain.invitations import (
|
||||||
Invitations,
|
PortfolioInvitations,
|
||||||
InvitationError,
|
InvitationError,
|
||||||
WrongUserError,
|
WrongUserError,
|
||||||
ExpiredError,
|
ExpiredError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
)
|
)
|
||||||
from atst.models.invitation import Status
|
from atst.domain.audit_log import AuditLog
|
||||||
|
from atst.models import InvitationStatus
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
PortfolioFactory,
|
PortfolioFactory,
|
||||||
PortfolioRoleFactory,
|
PortfolioRoleFactory,
|
||||||
UserFactory,
|
UserFactory,
|
||||||
InvitationFactory,
|
PortfolioInvitationFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
from atst.domain.audit_log import AuditLog
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_invitation():
|
def test_create_invitation():
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email)
|
||||||
assert invite.user == user
|
assert invite.user == user
|
||||||
assert invite.portfolio_role == ws_role
|
assert invite.role == ws_role
|
||||||
assert invite.inviter == portfolio.owner
|
assert invite.inviter == portfolio.owner
|
||||||
assert invite.status == Status.PENDING
|
assert invite.status == InvitationStatus.PENDING
|
||||||
assert re.match(r"^[\w\-_]+$", invite.token)
|
assert re.match(r"^[\w\-_]+$", invite.token)
|
||||||
|
|
||||||
|
|
||||||
@ -37,9 +36,9 @@ def test_accept_invitation():
|
|||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email)
|
||||||
assert invite.is_pending
|
assert invite.is_pending
|
||||||
accepted_invite = Invitations.accept(user, invite.token)
|
accepted_invite = PortfolioInvitations.accept(user, invite.token)
|
||||||
assert accepted_invite.is_accepted
|
assert accepted_invite.is_accepted
|
||||||
|
|
||||||
|
|
||||||
@ -47,16 +46,16 @@ def test_accept_expired_invitation():
|
|||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
increment = Invitations.EXPIRATION_LIMIT_MINUTES + 1
|
increment = PortfolioInvitations.EXPIRATION_LIMIT_MINUTES + 1
|
||||||
expiration_time = datetime.datetime.now() - datetime.timedelta(minutes=increment)
|
expiration_time = datetime.datetime.now() - datetime.timedelta(minutes=increment)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
user=user,
|
user=user,
|
||||||
expiration_time=expiration_time,
|
expiration_time=expiration_time,
|
||||||
status=Status.PENDING,
|
status=InvitationStatus.PENDING,
|
||||||
portfolio_role=ws_role,
|
role=ws_role,
|
||||||
)
|
)
|
||||||
with pytest.raises(ExpiredError):
|
with pytest.raises(ExpiredError):
|
||||||
Invitations.accept(user, invite.token)
|
PortfolioInvitations.accept(user, invite.token)
|
||||||
|
|
||||||
assert invite.is_rejected
|
assert invite.is_rejected
|
||||||
|
|
||||||
@ -65,22 +64,22 @@ def test_accept_rejected_invite():
|
|||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
user=user, status=Status.REJECTED_EXPIRED, portfolio_role=ws_role
|
user=user, status=InvitationStatus.REJECTED_EXPIRED, role=ws_role
|
||||||
)
|
)
|
||||||
with pytest.raises(InvitationError):
|
with pytest.raises(InvitationError):
|
||||||
Invitations.accept(user, invite.token)
|
PortfolioInvitations.accept(user, invite.token)
|
||||||
|
|
||||||
|
|
||||||
def test_accept_revoked_invite():
|
def test_accept_revoked_invite():
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
user=user, status=Status.REVOKED, portfolio_role=ws_role
|
user=user, status=InvitationStatus.REVOKED, role=ws_role
|
||||||
)
|
)
|
||||||
with pytest.raises(InvitationError):
|
with pytest.raises(InvitationError):
|
||||||
Invitations.accept(user, invite.token)
|
PortfolioInvitations.accept(user, invite.token)
|
||||||
|
|
||||||
|
|
||||||
def test_wrong_user_accepts_invitation():
|
def test_wrong_user_accepts_invitation():
|
||||||
@ -88,9 +87,9 @@ def test_wrong_user_accepts_invitation():
|
|||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
wrong_user = UserFactory.create()
|
wrong_user = UserFactory.create()
|
||||||
invite = InvitationFactory.create(user=user, portfolio_role=ws_role)
|
invite = PortfolioInvitationFactory.create(user=user, role=ws_role)
|
||||||
with pytest.raises(WrongUserError):
|
with pytest.raises(WrongUserError):
|
||||||
Invitations.accept(wrong_user, invite.token)
|
PortfolioInvitations.accept(wrong_user, invite.token)
|
||||||
|
|
||||||
|
|
||||||
def test_user_cannot_accept_invitation_accepted_by_wrong_user():
|
def test_user_cannot_accept_invitation_accepted_by_wrong_user():
|
||||||
@ -98,30 +97,30 @@ def test_user_cannot_accept_invitation_accepted_by_wrong_user():
|
|||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
wrong_user = UserFactory.create()
|
wrong_user = UserFactory.create()
|
||||||
invite = InvitationFactory.create(user=user, portfolio_role=ws_role)
|
invite = PortfolioInvitationFactory.create(user=user, role=ws_role)
|
||||||
with pytest.raises(WrongUserError):
|
with pytest.raises(WrongUserError):
|
||||||
Invitations.accept(wrong_user, invite.token)
|
PortfolioInvitations.accept(wrong_user, invite.token)
|
||||||
with pytest.raises(InvitationError):
|
with pytest.raises(InvitationError):
|
||||||
Invitations.accept(user, invite.token)
|
PortfolioInvitations.accept(user, invite.token)
|
||||||
|
|
||||||
|
|
||||||
def test_accept_invitation_twice():
|
def test_accept_invitation_twice():
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email)
|
||||||
Invitations.accept(user, invite.token)
|
PortfolioInvitations.accept(user, invite.token)
|
||||||
with pytest.raises(InvitationError):
|
with pytest.raises(InvitationError):
|
||||||
Invitations.accept(user, invite.token)
|
PortfolioInvitations.accept(user, invite.token)
|
||||||
|
|
||||||
|
|
||||||
def test_revoke_invitation():
|
def test_revoke_invitation():
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email)
|
||||||
assert invite.is_pending
|
assert invite.is_pending
|
||||||
Invitations.revoke(invite.token)
|
PortfolioInvitations.revoke(invite.token)
|
||||||
assert invite.is_revoked
|
assert invite.is_revoked
|
||||||
|
|
||||||
|
|
||||||
@ -129,8 +128,8 @@ def test_resend_invitation():
|
|||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email)
|
||||||
Invitations.resend(user, invite.token)
|
PortfolioInvitations.resend(user, invite.token)
|
||||||
assert ws_role.invitations[0].is_revoked
|
assert ws_role.invitations[0].is_revoked
|
||||||
assert ws_role.invitations[1].is_pending
|
assert ws_role.invitations[1].is_pending
|
||||||
|
|
||||||
@ -139,8 +138,8 @@ def test_audit_event_for_accepted_invite():
|
|||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email)
|
||||||
invite = Invitations.accept(user, invite.token)
|
invite = PortfolioInvitations.accept(user, invite.token)
|
||||||
|
|
||||||
accepted_event = AuditLog.get_by_resource(invite.id)[0]
|
accepted_event = AuditLog.get_by_resource(invite.id)[0]
|
||||||
assert "email" in accepted_event.event_details
|
assert "email" in accepted_event.event_details
|
||||||
@ -151,9 +150,11 @@ def test_lookup_by_user_and_portfolio():
|
|||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
ws_role = PortfolioRoleFactory.create(user=user, portfolio=portfolio)
|
||||||
invite = Invitations.create(portfolio.owner, ws_role, user.email)
|
invite = PortfolioInvitations.create(portfolio.owner, ws_role, user.email)
|
||||||
|
|
||||||
assert Invitations.lookup_by_portfolio_and_user(portfolio, user) == invite
|
assert PortfolioInvitations.lookup_by_resource_and_user(portfolio, user) == invite
|
||||||
|
|
||||||
with pytest.raises(NotFoundError):
|
with pytest.raises(NotFoundError):
|
||||||
Invitations.lookup_by_portfolio_and_user(portfolio, UserFactory.create())
|
PortfolioInvitations.lookup_by_resource_and_user(
|
||||||
|
portfolio, UserFactory.create()
|
||||||
|
)
|
||||||
|
@ -7,7 +7,7 @@ from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
|||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
PortfolioFactory,
|
PortfolioFactory,
|
||||||
UserFactory,
|
UserFactory,
|
||||||
InvitationFactory,
|
PortfolioInvitationFactory,
|
||||||
PortfolioRoleFactory,
|
PortfolioRoleFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,11 +7,8 @@ import datetime
|
|||||||
|
|
||||||
from atst.forms import data
|
from atst.forms import data
|
||||||
from atst.models import *
|
from atst.models import *
|
||||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
|
||||||
from atst.models.application_role import Status as ApplicationRoleStatus
|
from atst.domain.invitations import PortfolioInvitations
|
||||||
from atst.models.invitation import Status as InvitationStatus
|
|
||||||
from atst.models.environment_role import CSPRole
|
|
||||||
from atst.domain.invitations import Invitations
|
|
||||||
from atst.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from atst.domain.portfolio_roles import PortfolioRoles
|
from atst.domain.portfolio_roles import PortfolioRoles
|
||||||
|
|
||||||
@ -240,13 +237,22 @@ class EnvironmentRoleFactory(Base):
|
|||||||
user = factory.SubFactory(UserFactory)
|
user = factory.SubFactory(UserFactory)
|
||||||
|
|
||||||
|
|
||||||
class InvitationFactory(Base):
|
class PortfolioInvitationFactory(Base):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Invitation
|
model = PortfolioInvitation
|
||||||
|
|
||||||
email = factory.Faker("email")
|
email = factory.Faker("email")
|
||||||
status = InvitationStatus.PENDING
|
status = InvitationStatus.PENDING
|
||||||
expiration_time = Invitations.current_expiration_time()
|
expiration_time = PortfolioInvitations.current_expiration_time()
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationInvitationFactory(Base):
|
||||||
|
class Meta:
|
||||||
|
model = ApplicationInvitation
|
||||||
|
|
||||||
|
email = factory.Faker("email")
|
||||||
|
status = InvitationStatus.PENDING
|
||||||
|
expiration_time = PortfolioInvitations.current_expiration_time()
|
||||||
|
|
||||||
|
|
||||||
class AttachmentFactory(Base):
|
class AttachmentFactory(Base):
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import pytest
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from atst.models.invitation import Invitation, Status
|
from atst.models import InvitationStatus, PortfolioRoleStatus
|
||||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
InvitationFactory,
|
PortfolioInvitationFactory,
|
||||||
PortfolioFactory,
|
PortfolioFactory,
|
||||||
UserFactory,
|
UserFactory,
|
||||||
PortfolioRoleFactory,
|
PortfolioRoleFactory,
|
||||||
@ -18,9 +16,9 @@ def test_expired_invite_is_not_revokable():
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
expiration_time=datetime.datetime.now() - datetime.timedelta(minutes=60),
|
expiration_time=datetime.datetime.now() - datetime.timedelta(minutes=60),
|
||||||
portfolio_role=portfolio_role,
|
role=portfolio_role,
|
||||||
)
|
)
|
||||||
assert not invite.is_revokable
|
assert not invite.is_revokable
|
||||||
|
|
||||||
@ -31,7 +29,7 @@ def test_unexpired_invite_is_revokable():
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(portfolio_role=portfolio_role)
|
invite = PortfolioInvitationFactory.create(role=portfolio_role)
|
||||||
assert invite.is_revokable
|
assert invite.is_revokable
|
||||||
|
|
||||||
|
|
||||||
@ -41,7 +39,7 @@ def test_invite_is_not_revokable_if_invite_is_not_pending():
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
portfolio_role=portfolio_role, status=Status.ACCEPTED
|
role=portfolio_role, status=InvitationStatus.ACCEPTED
|
||||||
)
|
)
|
||||||
assert not invite.is_revokable
|
assert not invite.is_revokable
|
@ -6,14 +6,11 @@ from atst.domain.portfolios import Portfolios
|
|||||||
from atst.domain.portfolio_roles import PortfolioRoles
|
from atst.domain.portfolio_roles import PortfolioRoles
|
||||||
from atst.domain.applications import Applications
|
from atst.domain.applications import Applications
|
||||||
from atst.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from atst.models.portfolio_role import Status
|
from atst.models import AuditEvent, InvitationStatus, PortfolioRoleStatus, CSPRole
|
||||||
from atst.models.invitation import Status as InvitationStatus
|
|
||||||
from atst.models.audit_event import AuditEvent
|
|
||||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
|
||||||
from atst.models.environment_role import CSPRole
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
UserFactory,
|
UserFactory,
|
||||||
InvitationFactory,
|
PortfolioInvitationFactory,
|
||||||
PortfolioRoleFactory,
|
PortfolioRoleFactory,
|
||||||
EnvironmentFactory,
|
EnvironmentFactory,
|
||||||
EnvironmentRoleFactory,
|
EnvironmentRoleFactory,
|
||||||
@ -189,12 +186,12 @@ def test_has_environment_roles():
|
|||||||
|
|
||||||
|
|
||||||
def test_status_when_member_is_active():
|
def test_status_when_member_is_active():
|
||||||
portfolio_role = PortfolioRoleFactory.create(status=Status.ACTIVE)
|
portfolio_role = PortfolioRoleFactory.create(status=PortfolioRoleStatus.ACTIVE)
|
||||||
assert portfolio_role.display_status == "Active"
|
assert portfolio_role.display_status == "Active"
|
||||||
|
|
||||||
|
|
||||||
def test_status_when_member_is_disabled():
|
def test_status_when_member_is_disabled():
|
||||||
portfolio_role = PortfolioRoleFactory.create(status=Status.DISABLED)
|
portfolio_role = PortfolioRoleFactory.create(status=PortfolioRoleStatus.DISABLED)
|
||||||
assert portfolio_role.display_status == "Disabled"
|
assert portfolio_role.display_status == "Disabled"
|
||||||
|
|
||||||
|
|
||||||
@ -204,8 +201,8 @@ def test_status_when_invitation_has_been_rejected_for_expirations():
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invitation = InvitationFactory.create(
|
PortfolioInvitationFactory.create(
|
||||||
portfolio_role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED
|
role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED
|
||||||
)
|
)
|
||||||
assert portfolio_role.display_status == "Invite expired"
|
assert portfolio_role.display_status == "Invite expired"
|
||||||
|
|
||||||
@ -216,8 +213,8 @@ def test_status_when_invitation_has_been_rejected_for_wrong_user():
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invitation = InvitationFactory.create(
|
PortfolioInvitationFactory.create(
|
||||||
portfolio_role=portfolio_role, status=InvitationStatus.REJECTED_WRONG_USER
|
role=portfolio_role, status=InvitationStatus.REJECTED_WRONG_USER
|
||||||
)
|
)
|
||||||
assert portfolio_role.display_status == "Error on invite"
|
assert portfolio_role.display_status == "Error on invite"
|
||||||
|
|
||||||
@ -228,8 +225,8 @@ def test_status_when_invitation_has_been_revoked():
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invitation = InvitationFactory.create(
|
PortfolioInvitationFactory.create(
|
||||||
portfolio_role=portfolio_role, status=InvitationStatus.REVOKED
|
role=portfolio_role, status=InvitationStatus.REVOKED
|
||||||
)
|
)
|
||||||
assert portfolio_role.display_status == "Invite revoked"
|
assert portfolio_role.display_status == "Invite revoked"
|
||||||
|
|
||||||
@ -240,8 +237,8 @@ def test_status_when_invitation_is_expired():
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invitation = InvitationFactory.create(
|
PortfolioInvitationFactory.create(
|
||||||
portfolio_role=portfolio_role,
|
role=portfolio_role,
|
||||||
status=InvitationStatus.PENDING,
|
status=InvitationStatus.PENDING,
|
||||||
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
||||||
)
|
)
|
||||||
@ -254,8 +251,8 @@ def test_can_not_resend_invitation_if_active():
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invitation = InvitationFactory.create(
|
PortfolioInvitationFactory.create(
|
||||||
portfolio_role=portfolio_role, status=InvitationStatus.ACCEPTED
|
role=portfolio_role, status=InvitationStatus.ACCEPTED
|
||||||
)
|
)
|
||||||
assert not portfolio_role.can_resend_invitation
|
assert not portfolio_role.can_resend_invitation
|
||||||
|
|
||||||
@ -266,8 +263,8 @@ def test_can_resend_invitation_if_expired():
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invitation = InvitationFactory.create(
|
PortfolioInvitationFactory.create(
|
||||||
portfolio_role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED
|
role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED
|
||||||
)
|
)
|
||||||
assert portfolio_role.can_resend_invitation
|
assert portfolio_role.can_resend_invitation
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from tests.factories import PortfolioFactory, ApplicationFactory
|
from tests.factories import PortfolioFactory, ApplicationFactory, UserFactory
|
||||||
|
|
||||||
|
|
||||||
def test_application_team(client, user_session):
|
def test_application_team(client, user_session):
|
||||||
@ -12,3 +12,43 @@ def test_application_team(client, user_session):
|
|||||||
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_create_member(client, user_session):
|
||||||
|
user = UserFactory.create()
|
||||||
|
application = ApplicationFactory.create(
|
||||||
|
environments=[{"name": "Naboo"}, {"name": "Endor"}]
|
||||||
|
)
|
||||||
|
env = application.environments[0]
|
||||||
|
|
||||||
|
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-environment_name": env.name,
|
||||||
|
"environment_roles-0-role": "Basic Access",
|
||||||
|
"permission_sets-perms_env_mgmt": True,
|
||||||
|
"permission_sets-perms_team_mgmt": True,
|
||||||
|
"permission_sets-perms_del_env": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
expected_url = url_for(
|
||||||
|
"applications.team",
|
||||||
|
application_id=application.id,
|
||||||
|
fragment="application-members",
|
||||||
|
_anchor="application-members",
|
||||||
|
_external=True,
|
||||||
|
)
|
||||||
|
assert response.location == expected_url
|
||||||
|
assert len(user.application_roles) == 1
|
||||||
|
assert user.application_roles[0].application == application
|
||||||
|
assert len(user.environment_roles) == 1
|
||||||
|
assert user.environment_roles[0].environment == env
|
||||||
|
@ -6,12 +6,11 @@ from tests.factories import (
|
|||||||
UserFactory,
|
UserFactory,
|
||||||
PortfolioFactory,
|
PortfolioFactory,
|
||||||
PortfolioRoleFactory,
|
PortfolioRoleFactory,
|
||||||
InvitationFactory,
|
PortfolioInvitationFactory,
|
||||||
TaskOrderFactory,
|
TaskOrderFactory,
|
||||||
)
|
)
|
||||||
from atst.domain.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
from atst.models import InvitationStatus, PortfolioRoleStatus
|
||||||
from atst.models.invitation import Status as InvitationStatus
|
|
||||||
from atst.domain.users import Users
|
from atst.domain.users import Users
|
||||||
from atst.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ def test_existing_member_accepts_valid_invite(client, user_session):
|
|||||||
ws_role = PortfolioRoleFactory.create(
|
ws_role = PortfolioRoleFactory.create(
|
||||||
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
portfolio=portfolio, user=user, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(user_id=user.id, portfolio_role=ws_role)
|
invite = PortfolioInvitationFactory.create(user_id=user.id, role=ws_role)
|
||||||
|
|
||||||
# the user does not have access to the portfolio before accepting the invite
|
# the user does not have access to the portfolio before accepting the invite
|
||||||
assert len(Portfolios.for_user(user)) == 0
|
assert len(Portfolios.for_user(user)) == 0
|
||||||
@ -50,17 +49,20 @@ def test_new_member_accepts_valid_invite(monkeypatch, client, user_session):
|
|||||||
response = client.post(
|
response = client.post(
|
||||||
url_for("portfolios.create_member", portfolio_id=portfolio.id),
|
url_for("portfolios.create_member", portfolio_id=portfolio.id),
|
||||||
data={
|
data={
|
||||||
"perms_app_mgmt": PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
"permission_sets-perms_app_mgmt": PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||||
"perms_funding": PermissionSets.VIEW_PORTFOLIO_FUNDING,
|
"permission_sets-perms_funding": PermissionSets.VIEW_PORTFOLIO_FUNDING,
|
||||||
"perms_reporting": PermissionSets.VIEW_PORTFOLIO_REPORTS,
|
"permission_sets-perms_reporting": PermissionSets.VIEW_PORTFOLIO_REPORTS,
|
||||||
"perms_portfolio_mgmt": PermissionSets.VIEW_PORTFOLIO_ADMIN,
|
"permission_sets-perms_portfolio_mgmt": PermissionSets.VIEW_PORTFOLIO_ADMIN,
|
||||||
**user_info,
|
"user_data-first_name": user_info["first_name"],
|
||||||
|
"user_data-last_name": user_info["last_name"],
|
||||||
|
"user_data-dod_id": user_info["dod_id"],
|
||||||
|
"user_data-email": user_info["email"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
user = Users.get_by_dod_id(user_info["dod_id"])
|
user = Users.get_by_dod_id(user_info["dod_id"])
|
||||||
token = user.invitations[0].token
|
token = user.portfolio_invitations[0].token
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"atst.domain.auth.should_redirect_to_user_profile", lambda *args: False
|
"atst.domain.auth.should_redirect_to_user_profile", lambda *args: False
|
||||||
@ -84,10 +86,8 @@ def test_member_accepts_invalid_invite(client, user_session):
|
|||||||
ws_role = PortfolioRoleFactory.create(
|
ws_role = PortfolioRoleFactory.create(
|
||||||
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
user_id=user.id,
|
user_id=user.id, role=ws_role, status=InvitationStatus.REJECTED_WRONG_USER
|
||||||
portfolio_role=ws_role,
|
|
||||||
status=InvitationStatus.REJECTED_WRONG_USER,
|
|
||||||
)
|
)
|
||||||
user_session(user)
|
user_session(user)
|
||||||
response = client.get(url_for("portfolios.accept_invitation", token=invite.token))
|
response = client.get(url_for("portfolios.accept_invitation", token=invite.token))
|
||||||
@ -119,7 +119,7 @@ def test_user_accepts_invite_with_wrong_dod_id(client, user_session):
|
|||||||
ws_role = PortfolioRoleFactory.create(
|
ws_role = PortfolioRoleFactory.create(
|
||||||
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(user_id=user.id, portfolio_role=ws_role)
|
invite = PortfolioInvitationFactory.create(user_id=user.id, role=ws_role)
|
||||||
user_session(different_user)
|
user_session(different_user)
|
||||||
response = client.get(url_for("portfolios.accept_invitation", token=invite.token))
|
response = client.get(url_for("portfolios.accept_invitation", token=invite.token))
|
||||||
|
|
||||||
@ -132,9 +132,9 @@ def test_user_accepts_expired_invite(client, user_session):
|
|||||||
ws_role = PortfolioRoleFactory.create(
|
ws_role = PortfolioRoleFactory.create(
|
||||||
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
portfolio_role=ws_role,
|
role=ws_role,
|
||||||
status=InvitationStatus.REJECTED_EXPIRED,
|
status=InvitationStatus.REJECTED_EXPIRED,
|
||||||
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
||||||
)
|
)
|
||||||
@ -150,9 +150,9 @@ def test_revoke_invitation(client, user_session):
|
|||||||
ws_role = PortfolioRoleFactory.create(
|
ws_role = PortfolioRoleFactory.create(
|
||||||
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
portfolio_role=ws_role,
|
role=ws_role,
|
||||||
status=InvitationStatus.REJECTED_EXPIRED,
|
status=InvitationStatus.REJECTED_EXPIRED,
|
||||||
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
||||||
)
|
)
|
||||||
@ -176,9 +176,9 @@ def test_user_can_only_revoke_invites_in_their_portfolio(client, user_session):
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
user=user, portfolio=other_portfolio, status=PortfolioRoleStatus.PENDING
|
user=user, portfolio=other_portfolio, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
portfolio_role=portfolio_role,
|
role=portfolio_role,
|
||||||
status=InvitationStatus.REJECTED_EXPIRED,
|
status=InvitationStatus.REJECTED_EXPIRED,
|
||||||
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
||||||
)
|
)
|
||||||
@ -202,9 +202,9 @@ def test_user_can_only_resend_invites_in_their_portfolio(client, user_session, q
|
|||||||
portfolio_role = PortfolioRoleFactory.create(
|
portfolio_role = PortfolioRoleFactory.create(
|
||||||
user=user, portfolio=other_portfolio, status=PortfolioRoleStatus.PENDING
|
user=user, portfolio=other_portfolio, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
portfolio_role=portfolio_role,
|
role=portfolio_role,
|
||||||
status=InvitationStatus.REJECTED_EXPIRED,
|
status=InvitationStatus.REJECTED_EXPIRED,
|
||||||
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
|
||||||
)
|
)
|
||||||
@ -227,8 +227,8 @@ def test_resend_invitation_sends_email(client, user_session, queue):
|
|||||||
ws_role = PortfolioRoleFactory.create(
|
ws_role = PortfolioRoleFactory.create(
|
||||||
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
user_id=user.id, portfolio_role=ws_role, status=InvitationStatus.PENDING
|
user_id=user.id, role=ws_role, status=InvitationStatus.PENDING
|
||||||
)
|
)
|
||||||
user_session(portfolio.owner)
|
user_session(portfolio.owner)
|
||||||
client.post(
|
client.post(
|
||||||
@ -250,9 +250,9 @@ def test_existing_member_invite_resent_to_email_submitted_in_form(
|
|||||||
ws_role = PortfolioRoleFactory.create(
|
ws_role = PortfolioRoleFactory.create(
|
||||||
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
portfolio_role=ws_role,
|
role=ws_role,
|
||||||
status=InvitationStatus.PENDING,
|
status=InvitationStatus.PENDING,
|
||||||
email="example@example.com",
|
email="example@example.com",
|
||||||
)
|
)
|
||||||
@ -290,7 +290,7 @@ def test_contracting_officer_accepts_invite(monkeypatch, client, user_session):
|
|||||||
|
|
||||||
# contracting officer accepts invitation
|
# contracting officer accepts invitation
|
||||||
user = Users.get_by_dod_id(user_info["dod_id"])
|
user = Users.get_by_dod_id(user_info["dod_id"])
|
||||||
token = user.invitations[0].token
|
token = user.portfolio_invitations[0].token
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"atst.domain.auth.should_redirect_to_user_profile", lambda *args: False
|
"atst.domain.auth.should_redirect_to_user_profile", lambda *args: False
|
||||||
)
|
)
|
||||||
@ -324,7 +324,7 @@ def test_cor_accepts_invite(monkeypatch, client, user_session):
|
|||||||
|
|
||||||
# contracting officer representative accepts invitation
|
# contracting officer representative accepts invitation
|
||||||
user = Users.get_by_dod_id(user_info["dod_id"])
|
user = Users.get_by_dod_id(user_info["dod_id"])
|
||||||
token = user.invitations[0].token
|
token = user.portfolio_invitations[0].token
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"atst.domain.auth.should_redirect_to_user_profile", lambda *args: False
|
"atst.domain.auth.should_redirect_to_user_profile", lambda *args: False
|
||||||
)
|
)
|
||||||
@ -358,7 +358,7 @@ def test_so_accepts_invite(monkeypatch, client, user_session):
|
|||||||
|
|
||||||
# security officer accepts invitation
|
# security officer accepts invitation
|
||||||
user = Users.get_by_dod_id(user_info["dod_id"])
|
user = Users.get_by_dod_id(user_info["dod_id"])
|
||||||
token = user.invitations[0].token
|
token = user.portfolio_invitations[0].token
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"atst.domain.auth.should_redirect_to_user_profile", lambda *args: False
|
"atst.domain.auth.should_redirect_to_user_profile", lambda *args: False
|
||||||
)
|
)
|
||||||
|
@ -5,10 +5,10 @@ from atst.domain.permission_sets import PermissionSets
|
|||||||
from atst.queue import queue
|
from atst.queue import queue
|
||||||
|
|
||||||
_DEFAULT_PERMS_FORM_DATA = {
|
_DEFAULT_PERMS_FORM_DATA = {
|
||||||
"perms_app_mgmt": PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
"permission_sets-perms_app_mgmt": PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||||
"perms_funding": PermissionSets.VIEW_PORTFOLIO_FUNDING,
|
"permission_sets-perms_funding": PermissionSets.VIEW_PORTFOLIO_FUNDING,
|
||||||
"perms_reporting": PermissionSets.VIEW_PORTFOLIO_REPORTS,
|
"permission_sets-perms_reporting": PermissionSets.VIEW_PORTFOLIO_REPORTS,
|
||||||
"perms_portfolio_mgmt": PermissionSets.VIEW_PORTFOLIO_ADMIN,
|
"permission_sets-perms_portfolio_mgmt": PermissionSets.VIEW_PORTFOLIO_ADMIN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -32,11 +32,11 @@ def test_create_member(client, user_session):
|
|||||||
response = client.post(
|
response = client.post(
|
||||||
url_for("portfolios.create_member", portfolio_id=portfolio.id),
|
url_for("portfolios.create_member", portfolio_id=portfolio.id),
|
||||||
data={
|
data={
|
||||||
"dod_id": user.dod_id,
|
"user_data-dod_id": user.dod_id,
|
||||||
"first_name": "Wilbur",
|
"user_data-first_name": "user_data-Wilbur",
|
||||||
"last_name": "Zuckerman",
|
"user_data-last_name": "user_data-Zuckerman",
|
||||||
"email": "some_pig@zuckermans.com",
|
"user_data-email": "user_data-some_pig@zuckermans.com",
|
||||||
"portfolio_role": "developer",
|
"user_data-portfolio_role": "user_data-developer",
|
||||||
**_DEFAULT_PERMS_FORM_DATA,
|
**_DEFAULT_PERMS_FORM_DATA,
|
||||||
},
|
},
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
@ -45,7 +45,7 @@ def test_create_member(client, user_session):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert user.full_name in response.data.decode()
|
assert user.full_name in response.data.decode()
|
||||||
assert user.has_portfolios
|
assert user.has_portfolios
|
||||||
assert user.invitations
|
assert user.portfolio_invitations
|
||||||
assert len(queue.get_queue()) == queue_length + 1
|
assert len(queue.get_queue()) == queue_length + 1
|
||||||
portfolio_role = user.portfolio_roles[0]
|
portfolio_role = user.portfolio_roles[0]
|
||||||
assert len(portfolio_role.permission_sets) == 5
|
assert len(portfolio_role.permission_sets) == 5
|
||||||
|
@ -4,7 +4,7 @@ from flask import url_for
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.models.invitation import Status as InvitationStatus
|
from atst.models import InvitationStatus
|
||||||
from atst.models.portfolio_role import Status as PortfolioStatus
|
from atst.models.portfolio_role import Status as PortfolioStatus
|
||||||
from atst.queue import queue
|
from atst.queue import queue
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ from tests.factories import (
|
|||||||
TaskOrderFactory,
|
TaskOrderFactory,
|
||||||
UserFactory,
|
UserFactory,
|
||||||
PortfolioRoleFactory,
|
PortfolioRoleFactory,
|
||||||
InvitationFactory,
|
PortfolioInvitationFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ def test_does_not_resend_officer_invitation(client, user_session):
|
|||||||
user_session(user)
|
user_session(user)
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
client.post(url_for("task_orders.invite", task_order_id=task_order.id))
|
client.post(url_for("task_orders.invite", task_order_id=task_order.id))
|
||||||
assert len(contracting_officer.invitations) == 1
|
assert len(contracting_officer.portfolio_invitations) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_does_not_invite_if_task_order_incomplete(client, user_session, queue):
|
def test_does_not_invite_if_task_order_incomplete(client, user_session, queue):
|
||||||
@ -272,9 +272,9 @@ class TestTaskOrderInvitations:
|
|||||||
cor_invite=True,
|
cor_invite=True,
|
||||||
)
|
)
|
||||||
portfolio_role = PortfolioRoleFactory.create(portfolio=self.portfolio, user=cor)
|
portfolio_role = PortfolioRoleFactory.create(portfolio=self.portfolio, user=cor)
|
||||||
invitation = InvitationFactory.create(
|
PortfolioInvitationFactory.create(
|
||||||
inviter=self.portfolio.owner,
|
inviter=self.portfolio.owner,
|
||||||
portfolio_role=portfolio_role,
|
role=portfolio_role,
|
||||||
user=cor,
|
user=cor,
|
||||||
status=InvitationStatus.PENDING,
|
status=InvitationStatus.PENDING,
|
||||||
)
|
)
|
||||||
@ -369,9 +369,9 @@ def test_resend_invite_when_not_pending(app, client, user_session, portfolio, us
|
|||||||
portfolio=portfolio, user=user, status=PortfolioStatus.ACTIVE
|
portfolio=portfolio, user=user, status=PortfolioStatus.ACTIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
original_invitation = InvitationFactory.create(
|
original_invitation = PortfolioInvitationFactory.create(
|
||||||
inviter=user,
|
inviter=user,
|
||||||
portfolio_role=portfolio_role,
|
role=portfolio_role,
|
||||||
email=user.email,
|
email=user.email,
|
||||||
status=InvitationStatus.ACCEPTED,
|
status=InvitationStatus.ACCEPTED,
|
||||||
)
|
)
|
||||||
@ -397,9 +397,9 @@ def test_resending_revoked_invite(app, client, user_session, portfolio, user):
|
|||||||
|
|
||||||
portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=user)
|
portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=user)
|
||||||
|
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
inviter=user,
|
inviter=user,
|
||||||
portfolio_role=portfolio_role,
|
role=portfolio_role,
|
||||||
email=user.email,
|
email=user.email,
|
||||||
status=InvitationStatus.REVOKED,
|
status=InvitationStatus.REVOKED,
|
||||||
)
|
)
|
||||||
@ -427,9 +427,9 @@ def test_resending_expired_invite(app, client, user_session, portfolio):
|
|||||||
portfolio=portfolio, contracting_officer=ko, ko_invite=True
|
portfolio=portfolio, contracting_officer=ko, ko_invite=True
|
||||||
)
|
)
|
||||||
portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=ko)
|
portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=ko)
|
||||||
invite = InvitationFactory.create(
|
invite = PortfolioInvitationFactory.create(
|
||||||
inviter=portfolio.owner,
|
inviter=portfolio.owner,
|
||||||
portfolio_role=portfolio_role,
|
role=portfolio_role,
|
||||||
email=ko.email,
|
email=ko.email,
|
||||||
expiration_time=datetime.now() - timedelta(days=1),
|
expiration_time=datetime.now() - timedelta(days=1),
|
||||||
)
|
)
|
||||||
|
@ -1,14 +1,31 @@
|
|||||||
from tests.factories import UserFactory, PortfolioFactory, PortfolioRoleFactory
|
from tests.factories import (
|
||||||
|
ApplicationFactory,
|
||||||
|
ApplicationRoleFactory,
|
||||||
|
UserFactory,
|
||||||
|
PortfolioFactory,
|
||||||
|
PortfolioRoleFactory,
|
||||||
|
)
|
||||||
|
|
||||||
from atst.services.invitation import Invitation
|
from atst.services.invitation import Invitation
|
||||||
|
|
||||||
|
|
||||||
def test_invite_member(queue):
|
def test_invite_portfolio_member(queue):
|
||||||
inviter = UserFactory.create()
|
inviter = UserFactory.create()
|
||||||
new_member = UserFactory.create()
|
new_member = UserFactory.create()
|
||||||
portfolio = PortfolioFactory.create(owner=inviter)
|
portfolio = PortfolioFactory.create(owner=inviter)
|
||||||
ws_member = PortfolioRoleFactory.create(user=new_member, portfolio=portfolio)
|
ws_member = PortfolioRoleFactory.create(user=new_member, portfolio=portfolio)
|
||||||
invite_service = Invitation(inviter, ws_member, new_member.email)
|
invite_service = Invitation(inviter, ws_member, new_member.email)
|
||||||
new_invitation = invite_service.invite()
|
new_invitation = invite_service.invite()
|
||||||
assert new_invitation == new_member.invitations[0]
|
assert new_invitation == new_member.portfolio_invitations[0]
|
||||||
|
assert len(queue.get_queue()) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_invite_application_member(queue):
|
||||||
|
inviter = UserFactory.create()
|
||||||
|
new_member = UserFactory.create()
|
||||||
|
application = ApplicationFactory.create()
|
||||||
|
member = ApplicationRoleFactory.create(user=new_member, application=application)
|
||||||
|
invite_service = Invitation(inviter, member, new_member.email)
|
||||||
|
new_invitation = invite_service.invite()
|
||||||
|
assert new_invitation == new_member.application_invitations[0]
|
||||||
assert len(queue.get_queue()) == 1
|
assert len(queue.get_queue()) == 1
|
||||||
|
@ -17,7 +17,7 @@ from tests.factories import (
|
|||||||
ApplicationRoleFactory,
|
ApplicationRoleFactory,
|
||||||
EnvironmentFactory,
|
EnvironmentFactory,
|
||||||
EnvironmentRoleFactory,
|
EnvironmentRoleFactory,
|
||||||
InvitationFactory,
|
PortfolioInvitationFactory,
|
||||||
PortfolioFactory,
|
PortfolioFactory,
|
||||||
PortfolioRoleFactory,
|
PortfolioRoleFactory,
|
||||||
TaskOrderFactory,
|
TaskOrderFactory,
|
||||||
@ -75,7 +75,9 @@ def test_all_protected_routes_have_access_control(
|
|||||||
monkeypatch.setattr("atst.domain.portfolios.Portfolios.get", lambda *a: None)
|
monkeypatch.setattr("atst.domain.portfolios.Portfolios.get", lambda *a: None)
|
||||||
monkeypatch.setattr("atst.domain.task_orders.TaskOrders.get", lambda *a: Mock())
|
monkeypatch.setattr("atst.domain.task_orders.TaskOrders.get", lambda *a: Mock())
|
||||||
monkeypatch.setattr("atst.domain.applications.Applications.get", lambda *a: Mock())
|
monkeypatch.setattr("atst.domain.applications.Applications.get", lambda *a: Mock())
|
||||||
monkeypatch.setattr("atst.domain.invitations.Invitations._get", lambda *a: Mock())
|
monkeypatch.setattr(
|
||||||
|
"atst.domain.invitations.PortfolioInvitations._get", lambda *a: Mock()
|
||||||
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"atst.utils.context_processors.get_portfolio_from_context", lambda *a: None
|
"atst.utils.context_processors.get_portfolio_from_context", lambda *a: None
|
||||||
)
|
)
|
||||||
@ -402,7 +404,7 @@ def test_portfolios_resend_invitation_access(post_url_assert_status):
|
|||||||
|
|
||||||
portfolio = PortfolioFactory.create(owner=owner)
|
portfolio = PortfolioFactory.create(owner=owner)
|
||||||
prr = PortfolioRoleFactory.create(user=invitee, portfolio=portfolio)
|
prr = PortfolioRoleFactory.create(user=invitee, portfolio=portfolio)
|
||||||
invite = InvitationFactory.create(user=UserFactory.create(), portfolio_role=prr)
|
invite = PortfolioInvitationFactory.create(user=UserFactory.create(), role=prr)
|
||||||
|
|
||||||
url = url_for(
|
url = url_for(
|
||||||
"portfolios.resend_invitation", portfolio_id=portfolio.id, token=invite.token
|
"portfolios.resend_invitation", portfolio_id=portfolio.id, token=invite.token
|
||||||
@ -423,7 +425,7 @@ def test_task_orders_resend_invite_access(post_url_assert_status):
|
|||||||
portfolio = PortfolioFactory.create(owner=owner)
|
portfolio = PortfolioFactory.create(owner=owner)
|
||||||
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
|
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
|
||||||
prr = PortfolioRoleFactory.create(user=ko, portfolio=portfolio)
|
prr = PortfolioRoleFactory.create(user=ko, portfolio=portfolio)
|
||||||
invite = InvitationFactory.create(user=UserFactory.create(), portfolio_role=prr)
|
PortfolioInvitationFactory.create(user=UserFactory.create(), role=prr)
|
||||||
|
|
||||||
url = url_for(
|
url = url_for(
|
||||||
"task_orders.resend_invite",
|
"task_orders.resend_invite",
|
||||||
@ -449,7 +451,7 @@ def test_portfolios_revoke_invitation_access(post_url_assert_status):
|
|||||||
prr = PortfolioRoleFactory.create(
|
prr = PortfolioRoleFactory.create(
|
||||||
user=prt_member, portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE
|
user=prt_member, portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE
|
||||||
)
|
)
|
||||||
invite = InvitationFactory.create(user=prt_member, portfolio_role=prr)
|
invite = PortfolioInvitationFactory.create(user=prt_member, role=prr)
|
||||||
url = url_for(
|
url = url_for(
|
||||||
"portfolios.revoke_invitation",
|
"portfolios.revoke_invitation",
|
||||||
portfolio_id=portfolio.id,
|
portfolio_id=portfolio.id,
|
||||||
|
@ -35,6 +35,9 @@ common:
|
|||||||
save_and_continue: Save & continue
|
save_and_continue: Save & continue
|
||||||
show: Show
|
show: Show
|
||||||
sign: Sign
|
sign: Sign
|
||||||
|
resource_names:
|
||||||
|
environments: Environments
|
||||||
|
choose_role: Choose a role
|
||||||
components:
|
components:
|
||||||
modal:
|
modal:
|
||||||
close: Close
|
close: Close
|
||||||
@ -65,6 +68,7 @@ flash:
|
|||||||
next_steps: Review next steps below
|
next_steps: Review next steps below
|
||||||
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.'
|
||||||
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
|
||||||
@ -433,6 +437,14 @@ portfolios:
|
|||||||
user: User
|
user: User
|
||||||
team_text: Team
|
team_text: Team
|
||||||
update_button_text: Save
|
update_button_text: Save
|
||||||
|
members:
|
||||||
|
new:
|
||||||
|
assign_roles: Assign Member Environments and Roles
|
||||||
|
learn_more: Learn more about these roles
|
||||||
|
manage_perms: 'Manage permissions for {application_name}'
|
||||||
|
manage_envs: 'Allow member to <strong>add</strong> and <strong>rename environments</strong> within the application.'
|
||||||
|
delete_envs: 'Allow member to <strong>delete environments</strong> within the application.'
|
||||||
|
manage_team: 'Allow member to <strong>add, update,</strong> and <strong>remove members</strong> from the application team.'
|
||||||
index:
|
index:
|
||||||
empty:
|
empty:
|
||||||
start_button: Start a new JEDI portfolio
|
start_button: Start a new JEDI portfolio
|
||||||
|
Loading…
x
Reference in New Issue
Block a user