Route for adding new application member

- domain method for creating a new application member
- ApplicationInvitations domain class
- nested form for adding a new user that holds user data, application
  permission sets, and environment roles
- Invitation service can infer invitation type based on role it's given
- new invitation email templates
This commit is contained in:
dandds
2019-04-23 11:24:04 -04:00
parent 054f6b80b9
commit ade77e6b91
17 changed files with 284 additions and 25 deletions

View File

@@ -2,8 +2,10 @@ from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from . import BaseDomainClass
from atst.domain.application_roles import ApplicationRoles
from atst.domain.environments import Environments
from atst.domain.exceptions import NotFoundError
from atst.domain.users import Users
from atst.models.application import Application
from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole
@@ -72,3 +74,30 @@ class Applications(BaseDomainClass):
db.session.add(application)
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

View File

@@ -2,7 +2,7 @@ import datetime
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models import InvitationStatus, PortfolioInvitation
from atst.models import ApplicationInvitation, InvitationStatus, PortfolioInvitation
from atst.domain.portfolio_roles import PortfolioRoles
from .exceptions import NotFoundError
@@ -127,3 +127,7 @@ class BaseInvitations(object):
class PortfolioInvitations(BaseInvitations):
model = PortfolioInvitation
class ApplicationInvitations(BaseInvitations):
model = ApplicationInvitation

View File

@@ -0,0 +1,41 @@
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
class EnvironmentForm(BaseForm):
environment_id = HiddenField()
environment_name = HiddenField()
role = SelectField(environment_name, choices=ENV_ROLES, default=None)
class PermissionsForm(BaseForm):
perms_env_mgmt = BooleanField(None, default=False)
perms_team_mgmt = BooleanField(None, default=False)
perms_del_env = BooleanField(None, 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))

View File

@@ -1,3 +1,4 @@
from atst.models import CSPRole
from atst.utils.localization import translate, translate_duration
from atst.models.environment_role import CSPRole

View File

@@ -7,7 +7,7 @@ from .permission_set import PermissionSet
from .user import User
from .portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
from .application_role import ApplicationRole, Status as ApplicationRoleStatus
from .environment_role import EnvironmentRole
from .environment_role import EnvironmentRole, CSPRole
from .portfolio import Portfolio
from .application import Application
from .environment import Environment

View File

@@ -5,6 +5,7 @@ from sqlalchemy.dialects.postgresql import UUID
from atst.models import Base, types, mixins
from atst.models.permissions import Permissions
from atst.models.portfolio_invitation import PortfolioInvitation
from atst.models.application_invitation import ApplicationInvitation
users_permission_sets = Table(
@@ -39,6 +40,13 @@ class User(
"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)
dod_id = Column(String, unique=True, nullable=False)
first_name = Column(String)

View File

@@ -1,12 +1,15 @@
from flask import render_template
from flask import render_template, request as http_request, g, url_for, redirect
from . import applications_bp
from atst.domain.environments import Environments
from atst.domain.applications import Applications
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.forms.application_member import NewForm as NewMemberForm
from atst.models.permissions 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
@@ -47,3 +50,45 @@ def team(application_id):
application=application,
environment_users=environment_users,
)
@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",
)
)

View File

@@ -9,7 +9,9 @@ from atst.models.permissions import Permissions
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(
[new_member_email],
"{} has invited you to a JEDI Cloud Portfolio".format(owner_name),

View File

@@ -1,25 +1,26 @@
from flask import render_template
from atst.domain.invitations import PortfolioInvitations
from atst.domain.invitations import PortfolioInvitations, ApplicationInvitations
from atst.queue import queue
from atst.domain.task_orders import TaskOrders
from atst.domain.portfolio_roles import PortfolioRoles
from atst.models import ApplicationRole, PortfolioRole
OFFICER_INVITATIONS = {
"ko_invite": {
"role": "contracting_officer",
"subject": "Review a task order",
"template": "emails/invitation.txt",
"template": "emails/portfolio/invitation.txt",
},
"cor_invite": {
"role": "contracting_officer_representative",
"subject": "Help with a task order",
"template": "emails/invitation.txt",
"template": "emails/portfolio/invitation.txt",
},
"so_invite": {
"role": "security_officer",
"subject": "Review security for a task order",
"template": "emails/invitation.txt",
"template": "emails/portfolio/invitation.txt",
},
}
@@ -47,20 +48,38 @@ def update_officer_invitations(user, task_order):
class Invitation:
def __init__(
self,
inviter,
member,
email,
subject="{} has invited you to a JEDI cloud portfolio",
email_template="emails/invitation.txt",
):
def __init__(self, inviter, member, email, subject="", email_template=None):
self.inviter = inviter
self.member = member
self.email = email
self.subject = subject
self.email_template = email_template
if isinstance(member, PortfolioRole):
self.email_template = (
"emails/portfolio/invitation.txt"
if self.email_template is None
else self.email_template
)
self.subject = (
"{} has invited you to a JEDI cloud portfolio"
if self.subject is None
else self.subject
)
self.domain_class = PortfolioInvitations
elif isinstance(member, ApplicationRole):
self.email_template = (
"emails/application/invitation.txt"
if self.email_template is None
else self.email_template
)
self.subject = (
"{} has invited you to a JEDI cloud application"
if self.subject is None
else self.subject
)
self.domain_class = ApplicationInvitations
def invite(self):
invite = self._create_invite()
self._send_invite_email(invite.token)
@@ -68,7 +87,7 @@ class Invitation:
return invite
def _create_invite(self):
return PortfolioInvitations.create(self.inviter, self.member, self.email)
return self.domain_class.create(self.inviter, self.member, self.email)
def _send_invite_email(self, token):
body = render_template(

View File

@@ -161,6 +161,13 @@ MESSAGES = {
""",
"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",
},
}