Update atst to atat
This commit is contained in:
132
atat/routes/__init__.py
Normal file
132
atat/routes/__init__.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import urllib.parse as url
|
||||
from flask import (
|
||||
Blueprint,
|
||||
render_template,
|
||||
g,
|
||||
redirect,
|
||||
session,
|
||||
url_for,
|
||||
request,
|
||||
make_response,
|
||||
current_app as app,
|
||||
)
|
||||
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
import pendulum
|
||||
import os
|
||||
from werkzeug.exceptions import NotFound, MethodNotAllowed
|
||||
from werkzeug.routing import RequestRedirect
|
||||
|
||||
|
||||
from atat.domain.users import Users
|
||||
from atat.domain.authnid import AuthenticationContext
|
||||
from atat.domain.auth import logout as _logout
|
||||
from atat.domain.exceptions import UnauthenticatedError
|
||||
from atat.utils.flash import formatted_flash as flash
|
||||
|
||||
|
||||
bp = Blueprint("atat", __name__)
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
def root():
|
||||
if g.current_user:
|
||||
return redirect(url_for(".home"))
|
||||
|
||||
redirect_url = app.config.get("CAC_URL")
|
||||
if request.args.get("next"):
|
||||
redirect_url = url.urljoin(
|
||||
redirect_url,
|
||||
"?{}".format(url.urlencode({"next": request.args.get("next")})),
|
||||
)
|
||||
flash("login_next")
|
||||
|
||||
return render_template("login.html", redirect_url=redirect_url)
|
||||
|
||||
|
||||
@bp.route("/home")
|
||||
def home():
|
||||
return render_template("home.html")
|
||||
|
||||
|
||||
def _client_s_dn():
|
||||
return request.environ.get("HTTP_X_SSL_CLIENT_S_DN")
|
||||
|
||||
|
||||
def _make_authentication_context():
|
||||
return AuthenticationContext(
|
||||
crl_cache=app.crl_cache,
|
||||
auth_status=request.environ.get("HTTP_X_SSL_CLIENT_VERIFY"),
|
||||
sdn=_client_s_dn(),
|
||||
cert=request.environ.get("HTTP_X_SSL_CLIENT_CERT"),
|
||||
)
|
||||
|
||||
|
||||
def redirect_after_login_url():
|
||||
returl = request.args.get("next")
|
||||
if match_url_pattern(returl):
|
||||
param_name = request.args.get(app.form_cache.PARAM_NAME)
|
||||
if param_name:
|
||||
returl += "?" + url.urlencode({app.form_cache.PARAM_NAME: param_name})
|
||||
return returl
|
||||
else:
|
||||
return url_for("atat.home")
|
||||
|
||||
|
||||
def match_url_pattern(url, method="GET"):
|
||||
"""Ensure a url matches a url pattern in the flask app
|
||||
inspired by https://stackoverflow.com/questions/38488134/get-the-flask-view-function-that-matches-a-url/38488506#38488506
|
||||
"""
|
||||
server_name = app.config.get("SERVER_NAME") or "localhost"
|
||||
adapter = app.url_map.bind(server_name=server_name)
|
||||
|
||||
try:
|
||||
match = adapter.match(url, method=method)
|
||||
except RequestRedirect as e:
|
||||
# recursively match redirects
|
||||
return match_url_pattern(e.new_url, method)
|
||||
except (MethodNotAllowed, NotFound):
|
||||
# no match
|
||||
return None
|
||||
|
||||
if match[0] in app.view_functions:
|
||||
return url
|
||||
|
||||
|
||||
def current_user_setup(user):
|
||||
session["user_id"] = user.id
|
||||
session["last_login"] = user.last_login
|
||||
app.session_limiter.on_login(user)
|
||||
app.logger.info(f"authentication succeeded for user with EDIPI {user.dod_id}")
|
||||
Users.update_last_login(user)
|
||||
|
||||
|
||||
@bp.route("/login-redirect")
|
||||
def login_redirect():
|
||||
try:
|
||||
auth_context = _make_authentication_context()
|
||||
auth_context.authenticate()
|
||||
|
||||
user = auth_context.get_user()
|
||||
current_user_setup(user)
|
||||
except UnauthenticatedError as err:
|
||||
app.logger.info(
|
||||
f"authentication failed for subject distinguished name {_client_s_dn()}"
|
||||
)
|
||||
raise err
|
||||
|
||||
return redirect(redirect_after_login_url())
|
||||
|
||||
|
||||
@bp.route("/logout")
|
||||
def logout():
|
||||
_logout()
|
||||
response = make_response(redirect(url_for(".root")))
|
||||
response.set_cookie("expandSidenav", "", expires=0)
|
||||
flash("logged_out")
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/about")
|
||||
def about():
|
||||
return render_template("about.html")
|
25
atat/routes/applications/__init__.py
Normal file
25
atat/routes/applications/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from flask import current_app as app, g, redirect, url_for
|
||||
|
||||
from . import index
|
||||
from . import new
|
||||
from . import settings
|
||||
from . import invitations
|
||||
from .blueprint import applications_bp
|
||||
from atat.domain.environment_roles import EnvironmentRoles
|
||||
from atat.domain.exceptions import UnauthorizedError
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.models.permissions import Permissions
|
||||
|
||||
|
||||
def wrap_environment_role_lookup(user, environment_id=None, **kwargs):
|
||||
env_role = EnvironmentRoles.get_by_user_and_environment(user.id, environment_id)
|
||||
if not env_role:
|
||||
raise UnauthorizedError(user, "access environment {}".format(environment_id))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@applications_bp.route("/environments/<environment_id>/access")
|
||||
@user_can(None, override=wrap_environment_role_lookup, message="access environment")
|
||||
def access_environment(environment_id):
|
||||
return redirect("https://portal.azure.com")
|
6
atat/routes/applications/blueprint.py
Normal file
6
atat/routes/applications/blueprint.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from flask import Blueprint
|
||||
|
||||
from atat.utils.context_processors import portfolio as portfolio_context_processor
|
||||
|
||||
applications_bp = Blueprint("applications", __name__)
|
||||
applications_bp.context_processor(portfolio_context_processor)
|
34
atat/routes/applications/index.py
Normal file
34
atat/routes/applications/index.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from flask import render_template, g
|
||||
|
||||
from .blueprint import applications_bp
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.domain.environment_roles import EnvironmentRoles
|
||||
from atat.models.permissions import Permissions
|
||||
|
||||
|
||||
def has_portfolio_applications(_user, portfolio=None, **_kwargs):
|
||||
"""
|
||||
If the portfolio exists and the user has access to applications
|
||||
within the scoped portfolio, the user has access to the
|
||||
portfolio landing page.
|
||||
"""
|
||||
if portfolio and portfolio.applications:
|
||||
return True
|
||||
|
||||
|
||||
@applications_bp.route("/portfolios/<portfolio_id>")
|
||||
@applications_bp.route("/portfolios/<portfolio_id>/applications")
|
||||
@user_can(
|
||||
Permissions.VIEW_APPLICATION,
|
||||
override=has_portfolio_applications,
|
||||
message="view portfolio applications",
|
||||
)
|
||||
def portfolio_applications(portfolio_id):
|
||||
user_env_roles = EnvironmentRoles.for_user(g.current_user.id, portfolio_id)
|
||||
environment_access = {
|
||||
env_role.environment_id: env_role.role.value for env_role in user_env_roles
|
||||
}
|
||||
|
||||
return render_template(
|
||||
"applications/index.html", environment_access=environment_access
|
||||
)
|
16
atat/routes/applications/invitations.py
Normal file
16
atat/routes/applications/invitations.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from flask import redirect, url_for, g
|
||||
|
||||
from .blueprint import applications_bp
|
||||
from atat.domain.invitations import ApplicationInvitations
|
||||
|
||||
|
||||
@applications_bp.route("/applications/invitations/<token>", methods=["GET"])
|
||||
def accept_invitation(token):
|
||||
invite = ApplicationInvitations.accept(g.current_user, token)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.portfolio_applications",
|
||||
portfolio_id=invite.application.portfolio_id,
|
||||
)
|
||||
)
|
167
atat/routes/applications/new.py
Normal file
167
atat/routes/applications/new.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from flask import redirect, render_template, request as http_request, url_for
|
||||
|
||||
from .blueprint import applications_bp
|
||||
from atat.domain.applications import Applications
|
||||
from atat.forms.application import NameAndDescriptionForm, EnvironmentsForm
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.models.permissions import Permissions
|
||||
from atat.utils.flash import formatted_flash as flash
|
||||
from atat.routes.applications.settings import (
|
||||
get_members_data,
|
||||
get_new_member_form,
|
||||
handle_create_member,
|
||||
handle_update_member,
|
||||
handle_update_application,
|
||||
)
|
||||
|
||||
|
||||
def get_new_application_form(form_data, form_class, application_id=None):
|
||||
if application_id:
|
||||
application = Applications.get(application_id)
|
||||
return form_class(form_data, obj=application)
|
||||
else:
|
||||
return form_class(form_data)
|
||||
|
||||
|
||||
def render_new_application_form(
|
||||
template, form_class, portfolio_id=None, application_id=None, form=None
|
||||
):
|
||||
render_args = {"application_id": application_id}
|
||||
if application_id:
|
||||
application = Applications.get(application_id)
|
||||
render_args["form"] = form or form_class(obj=application)
|
||||
render_args["application"] = application
|
||||
else:
|
||||
render_args["form"] = form or form_class()
|
||||
|
||||
return render_template(template, **render_args)
|
||||
|
||||
|
||||
@applications_bp.route("/portfolios/<portfolio_id>/applications/new")
|
||||
@applications_bp.route("/applications/<application_id>/new/step_1")
|
||||
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
|
||||
def view_new_application_step_1(portfolio_id=None, application_id=None):
|
||||
return render_new_application_form(
|
||||
"applications/new/step_1.html",
|
||||
NameAndDescriptionForm,
|
||||
portfolio_id=portfolio_id,
|
||||
application_id=application_id,
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route(
|
||||
"/portfolios/<portfolio_id>/applications/new",
|
||||
endpoint="create_new_application_step_1",
|
||||
methods=["POST"],
|
||||
)
|
||||
@applications_bp.route(
|
||||
"/applications/<application_id>/new/step_1",
|
||||
endpoint="update_new_application_step_1",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
|
||||
def create_or_update_new_application_step_1(portfolio_id=None, application_id=None):
|
||||
form = get_new_application_form(
|
||||
{**http_request.form}, NameAndDescriptionForm, application_id
|
||||
)
|
||||
application = handle_update_application(form, application_id, portfolio_id)
|
||||
|
||||
if application:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.update_new_application_step_2",
|
||||
application_id=application.id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return (
|
||||
render_new_application_form(
|
||||
"applications/new/step_1.html",
|
||||
NameAndDescriptionForm,
|
||||
portfolio_id,
|
||||
application_id,
|
||||
form,
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route("/applications/<application_id>/new/step_2")
|
||||
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
|
||||
def view_new_application_step_2(application_id):
|
||||
application = Applications.get(application_id)
|
||||
render_args = {
|
||||
"form": EnvironmentsForm(
|
||||
data={
|
||||
"environment_names": [
|
||||
environment.name for environment in application.environments
|
||||
]
|
||||
}
|
||||
),
|
||||
"application": application,
|
||||
}
|
||||
|
||||
return render_template("applications/new/step_2.html", **render_args)
|
||||
|
||||
|
||||
@applications_bp.route("/applications/<application_id>/new/step_2", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
|
||||
def update_new_application_step_2(application_id):
|
||||
form = get_new_application_form(
|
||||
{**http_request.form}, EnvironmentsForm, application_id
|
||||
)
|
||||
if form.validate():
|
||||
application = Applications.get(application_id)
|
||||
application = Applications.update(application, form.data)
|
||||
flash("application_environments_updated")
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.update_new_application_step_3",
|
||||
application_id=application_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return (
|
||||
render_new_application_form(
|
||||
"applications/new/step_2.html",
|
||||
EnvironmentsForm,
|
||||
application_id=application_id,
|
||||
form=form,
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route("/applications/<application_id>/new/step_3")
|
||||
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
|
||||
def view_new_application_step_3(application_id):
|
||||
application = Applications.get(application_id)
|
||||
members = get_members_data(application)
|
||||
new_member_form = get_new_member_form(application)
|
||||
|
||||
return render_template(
|
||||
"applications/new/step_3.html",
|
||||
application_id=application_id,
|
||||
application=application,
|
||||
members=members,
|
||||
new_member_form=new_member_form,
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route("/applications/<application_id>/new/step_3", methods=["POST"])
|
||||
@applications_bp.route(
|
||||
"/applications/<application_id>/new/step_3/member/<application_role_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
|
||||
def update_new_application_step_3(application_id, application_role_id=None):
|
||||
if application_role_id:
|
||||
handle_update_member(application_id, application_role_id, http_request.form)
|
||||
else:
|
||||
handle_create_member(application_id, http_request.form)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.view_new_application_step_3", application_id=application_id
|
||||
)
|
||||
)
|
579
atat/routes/applications/settings.py
Normal file
579
atat/routes/applications/settings.py
Normal file
@@ -0,0 +1,579 @@
|
||||
from flask import (
|
||||
current_app as app,
|
||||
g,
|
||||
redirect,
|
||||
render_template,
|
||||
request as http_request,
|
||||
url_for,
|
||||
)
|
||||
from secrets import token_urlsafe
|
||||
|
||||
from .blueprint import applications_bp
|
||||
from atat.domain.exceptions import AlreadyExistsError
|
||||
from atat.domain.environments import Environments
|
||||
from atat.domain.applications import Applications
|
||||
from atat.domain.application_roles import ApplicationRoles
|
||||
from atat.domain.audit_log import AuditLog
|
||||
from atat.domain.csp.cloud.exceptions import GeneralCSPException
|
||||
|
||||
from atat.domain.csp.cloud.models import SubscriptionCreationCSPPayload
|
||||
from atat.domain.common import Paginator
|
||||
from atat.domain.environment_roles import EnvironmentRoles
|
||||
from atat.domain.invitations import ApplicationInvitations
|
||||
from atat.domain.portfolios import Portfolios
|
||||
from atat.forms.application_member import NewForm as NewMemberForm, UpdateMemberForm
|
||||
from atat.forms.application import NameAndDescriptionForm, EditEnvironmentForm
|
||||
from atat.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
|
||||
from atat.forms.member import NewForm as MemberForm
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.models.permissions import Permissions
|
||||
from atat.domain.permission_sets import PermissionSets
|
||||
from atat.utils.flash import formatted_flash as flash
|
||||
from atat.utils.localization import translate
|
||||
from atat.jobs import send_mail
|
||||
from atat.routes.errors import log_error
|
||||
|
||||
|
||||
def get_environments_obj_for_app(application):
|
||||
return sorted(
|
||||
[
|
||||
{
|
||||
"id": env.id,
|
||||
"name": env.name,
|
||||
"pending": env.is_pending,
|
||||
"edit_form": EditEnvironmentForm(obj=env),
|
||||
"member_count": len(env.roles),
|
||||
"members": sorted(
|
||||
[
|
||||
{
|
||||
"user_name": env_role.application_role.user_name,
|
||||
"status": env_role.status.value,
|
||||
}
|
||||
for env_role in env.roles
|
||||
],
|
||||
key=lambda env_role: env_role["user_name"],
|
||||
),
|
||||
}
|
||||
for env in application.environments
|
||||
],
|
||||
key=lambda env: env["name"],
|
||||
)
|
||||
|
||||
|
||||
def filter_perm_sets_data(member):
|
||||
perm_sets_data = {
|
||||
"perms_team_mgmt": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_APPLICATION_TEAM)
|
||||
),
|
||||
"perms_env_mgmt": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_APPLICATION_ENVIRONMENTS)
|
||||
),
|
||||
}
|
||||
|
||||
return perm_sets_data
|
||||
|
||||
|
||||
def filter_env_roles_data(roles):
|
||||
return sorted(
|
||||
[
|
||||
{
|
||||
"environment_id": str(role.environment.id),
|
||||
"environment_name": role.environment.name,
|
||||
"role": (role.role.value if role.role else "None"),
|
||||
}
|
||||
for role in roles
|
||||
],
|
||||
key=lambda env: env["environment_name"],
|
||||
)
|
||||
|
||||
|
||||
def filter_env_roles_form_data(member, environments):
|
||||
env_roles_form_data = []
|
||||
for env in environments:
|
||||
env_data = {
|
||||
"environment_id": str(env.id),
|
||||
"environment_name": env.name,
|
||||
"role": NO_ACCESS,
|
||||
"disabled": False,
|
||||
}
|
||||
env_roles_set = set(env.roles).intersection(set(member.environment_roles))
|
||||
|
||||
if len(env_roles_set) == 1:
|
||||
(env_role,) = env_roles_set
|
||||
env_data["disabled"] = env_role.disabled
|
||||
if env_role.role:
|
||||
env_data["role"] = env_role.role.name
|
||||
|
||||
env_roles_form_data.append(env_data)
|
||||
|
||||
return env_roles_form_data
|
||||
|
||||
|
||||
def get_members_data(application):
|
||||
members_data = []
|
||||
for member in application.members:
|
||||
permission_sets = filter_perm_sets_data(member)
|
||||
roles = EnvironmentRoles.get_for_application_member(member.id)
|
||||
environment_roles = filter_env_roles_data(roles)
|
||||
env_roles_form_data = filter_env_roles_form_data(
|
||||
member, application.environments
|
||||
)
|
||||
form = UpdateMemberForm(
|
||||
environment_roles=env_roles_form_data, **permission_sets
|
||||
)
|
||||
update_invite_form = (
|
||||
MemberForm(obj=member.latest_invitation)
|
||||
if member.latest_invitation and member.latest_invitation.can_resend
|
||||
else MemberForm()
|
||||
)
|
||||
|
||||
members_data.append(
|
||||
{
|
||||
"role_id": member.id,
|
||||
"user_name": member.user_name,
|
||||
"permission_sets": permission_sets,
|
||||
"environment_roles": environment_roles,
|
||||
"role_status": member.display_status,
|
||||
"form": form,
|
||||
"update_invite_form": update_invite_form,
|
||||
}
|
||||
)
|
||||
|
||||
return sorted(members_data, key=lambda member: member["user_name"])
|
||||
|
||||
|
||||
def get_new_member_form(application):
|
||||
env_roles = sorted(
|
||||
[
|
||||
{"environment_id": e.id, "environment_name": e.name}
|
||||
for e in application.environments
|
||||
],
|
||||
key=lambda role: role["environment_name"],
|
||||
)
|
||||
|
||||
return NewMemberForm(data={"environment_roles": env_roles})
|
||||
|
||||
|
||||
def render_settings_page(application, **kwargs):
|
||||
environments_obj = get_environments_obj_for_app(application=application)
|
||||
new_env_form = EditEnvironmentForm()
|
||||
pagination_opts = Paginator.get_pagination_opts(http_request)
|
||||
audit_events = AuditLog.get_application_events(application, pagination_opts)
|
||||
new_member_form = get_new_member_form(application)
|
||||
members = get_members_data(application)
|
||||
|
||||
if "application_form" not in kwargs:
|
||||
kwargs["application_form"] = NameAndDescriptionForm(
|
||||
name=application.name, description=application.description
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"applications/settings.html",
|
||||
application=application,
|
||||
environments_obj=environments_obj,
|
||||
new_env_form=new_env_form,
|
||||
audit_events=audit_events,
|
||||
new_member_form=new_member_form,
|
||||
members=members,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def send_application_invitation(invitee_email, inviter_name, token):
|
||||
body = render_template(
|
||||
"emails/application/invitation.txt", owner=inviter_name, token=token
|
||||
)
|
||||
send_mail.delay(
|
||||
[invitee_email],
|
||||
translate("email.application_invite", {"inviter_name": inviter_name}),
|
||||
body,
|
||||
)
|
||||
|
||||
|
||||
def handle_create_member(application_id, form_data):
|
||||
application = Applications.get(application_id)
|
||||
form = NewMemberForm(form_data)
|
||||
|
||||
if form.validate():
|
||||
try:
|
||||
invite = Applications.invite(
|
||||
application=application,
|
||||
inviter=g.current_user,
|
||||
user_data=form.user_data.data,
|
||||
permission_sets_names=form.data["permission_sets"],
|
||||
environment_roles_data=form.environment_roles.data,
|
||||
)
|
||||
|
||||
send_application_invitation(
|
||||
invitee_email=invite.email,
|
||||
inviter_name=g.current_user.full_name,
|
||||
token=invite.token,
|
||||
)
|
||||
|
||||
flash("new_application_member", user_name=invite.first_name)
|
||||
|
||||
except AlreadyExistsError:
|
||||
return render_template(
|
||||
"error.html", message="There was an error processing your request."
|
||||
)
|
||||
else:
|
||||
pass
|
||||
# TODO: flash error message
|
||||
|
||||
|
||||
def handle_update_member(application_id, application_role_id, form_data):
|
||||
app_role = ApplicationRoles.get_by_id(application_role_id)
|
||||
application = Applications.get(application_id)
|
||||
existing_env_roles_data = filter_env_roles_form_data(
|
||||
app_role, application.environments
|
||||
)
|
||||
form = UpdateMemberForm(
|
||||
formdata=form_data, environment_roles=existing_env_roles_data
|
||||
)
|
||||
|
||||
if form.validate():
|
||||
try:
|
||||
ApplicationRoles.update_permission_sets(
|
||||
app_role, form.data["permission_sets"]
|
||||
)
|
||||
|
||||
for env_role in form.environment_roles:
|
||||
environment = Environments.get(env_role.environment_id.data)
|
||||
new_role = None if env_role.disabled.data else env_role.data["role"]
|
||||
Environments.update_env_role(environment, app_role, new_role)
|
||||
|
||||
flash("application_member_updated", user_name=app_role.user_name)
|
||||
|
||||
except GeneralCSPException as exc:
|
||||
log_error(exc)
|
||||
flash(
|
||||
"application_member_update_error", user_name=app_role.user_name,
|
||||
)
|
||||
else:
|
||||
pass
|
||||
# TODO: flash error message
|
||||
|
||||
|
||||
def handle_update_environment(form, application=None, environment=None):
|
||||
if form.validate():
|
||||
try:
|
||||
if environment:
|
||||
environment = Environments.update(
|
||||
environment=environment, name=form.name.data
|
||||
)
|
||||
flash("application_environments_updated")
|
||||
else:
|
||||
environment = Environments.create(
|
||||
g.current_user, application=application, name=form.name.data
|
||||
)
|
||||
flash("environment_added", environment_name=form.name.data)
|
||||
|
||||
return environment
|
||||
|
||||
except AlreadyExistsError:
|
||||
flash("application_environments_name_error", name=form.name.data)
|
||||
return False
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def handle_update_application(form, application_id=None, portfolio_id=None):
|
||||
if form.validate():
|
||||
application = None
|
||||
|
||||
try:
|
||||
if application_id:
|
||||
application = Applications.get(application_id)
|
||||
application = Applications.update(application, form.data)
|
||||
flash("application_updated", application_name=application.name)
|
||||
else:
|
||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||
application = Applications.create(
|
||||
g.current_user, portfolio, **form.data
|
||||
)
|
||||
flash("application_created", application_name=application.name)
|
||||
|
||||
return application
|
||||
|
||||
except AlreadyExistsError:
|
||||
flash("application_name_error", name=form.data["name"])
|
||||
return False
|
||||
|
||||
|
||||
@applications_bp.route("/applications/<application_id>/settings")
|
||||
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
|
||||
def settings(application_id):
|
||||
application = Applications.get(application_id)
|
||||
|
||||
return render_settings_page(application=application,)
|
||||
|
||||
|
||||
@applications_bp.route("/environments/<environment_id>/edit", methods=["POST"])
|
||||
@user_can(Permissions.EDIT_ENVIRONMENT, message="edit application environments")
|
||||
def update_environment(environment_id):
|
||||
environment = Environments.get(environment_id)
|
||||
application = environment.application
|
||||
|
||||
env_form = EditEnvironmentForm(obj=environment, formdata=http_request.form)
|
||||
updated_environment = handle_update_environment(
|
||||
form=env_form, application=application, environment=environment
|
||||
)
|
||||
|
||||
if updated_environment:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=application.id,
|
||||
fragment="application-environments",
|
||||
_anchor="application-environments",
|
||||
)
|
||||
)
|
||||
else:
|
||||
return (render_settings_page(application=application, show_flash=True), 400)
|
||||
|
||||
|
||||
@applications_bp.route(
|
||||
"/applications/<application_id>/environments/new", methods=["POST"]
|
||||
)
|
||||
@user_can(Permissions.CREATE_ENVIRONMENT, message="create application environment")
|
||||
def new_environment(application_id):
|
||||
application = Applications.get(application_id)
|
||||
env_form = EditEnvironmentForm(formdata=http_request.form)
|
||||
environment = handle_update_environment(form=env_form, application=application)
|
||||
|
||||
if environment:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=application.id,
|
||||
fragment="application-environments",
|
||||
_anchor="application-environments",
|
||||
)
|
||||
)
|
||||
else:
|
||||
return (render_settings_page(application=application, show_flash=True), 400)
|
||||
|
||||
|
||||
@applications_bp.route("/applications/<application_id>/edit", methods=["POST"])
|
||||
@user_can(Permissions.EDIT_APPLICATION, message="update application")
|
||||
def update(application_id):
|
||||
application = Applications.get(application_id)
|
||||
form = NameAndDescriptionForm(http_request.form)
|
||||
updated_application = handle_update_application(form, application_id)
|
||||
|
||||
if updated_application:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.portfolio_applications",
|
||||
portfolio_id=application.portfolio_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return (
|
||||
render_settings_page(application=application, show_flash=True),
|
||||
400,
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route("/environments/<environment_id>/delete", methods=["POST"])
|
||||
@user_can(Permissions.DELETE_ENVIRONMENT, message="delete environment")
|
||||
def delete_environment(environment_id):
|
||||
environment = Environments.get(environment_id)
|
||||
Environments.delete(environment=environment, commit=True)
|
||||
|
||||
flash("environment_deleted", environment_name=environment.name)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=environment.application_id,
|
||||
_anchor="application-environments",
|
||||
fragment="application-environments",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@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):
|
||||
handle_create_member(application_id, http_request.form)
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=application_id,
|
||||
fragment="application-members",
|
||||
_anchor="application-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route(
|
||||
"/applications/<application_id>/members/<application_role_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_can(Permissions.DELETE_APPLICATION_MEMBER, message="remove application member")
|
||||
def remove_member(application_id, application_role_id):
|
||||
application_role = ApplicationRoles.get_by_id(application_role_id)
|
||||
ApplicationRoles.disable(application_role)
|
||||
|
||||
flash(
|
||||
"application_member_removed",
|
||||
user_name=application_role.user_name,
|
||||
application_name=g.application.name,
|
||||
)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
_anchor="application-members",
|
||||
application_id=g.application.id,
|
||||
fragment="application-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route(
|
||||
"/applications/<application_id>/members/<application_role_id>/update",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_can(Permissions.EDIT_APPLICATION_MEMBER, message="update application member")
|
||||
def update_member(application_id, application_role_id):
|
||||
|
||||
handle_update_member(application_id, application_role_id, http_request.form)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=application_id,
|
||||
fragment="application-members",
|
||||
_anchor="application-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route(
|
||||
"/applications/<application_id>/members/<application_role_id>/revoke_invite",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_can(
|
||||
Permissions.DELETE_APPLICATION_MEMBER, message="revoke application invitation"
|
||||
)
|
||||
def revoke_invite(application_id, application_role_id):
|
||||
app_role = ApplicationRoles.get_by_id(application_role_id)
|
||||
invite = app_role.latest_invitation
|
||||
|
||||
if invite.is_pending:
|
||||
ApplicationInvitations.revoke(invite.token)
|
||||
flash(
|
||||
"invite_revoked",
|
||||
resource="Application",
|
||||
user_name=app_role.user_name,
|
||||
resource_name=g.application.name,
|
||||
)
|
||||
else:
|
||||
flash(
|
||||
"application_invite_error",
|
||||
user_name=app_role.user_name,
|
||||
application_name=g.application.name,
|
||||
)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=application_id,
|
||||
fragment="application-members",
|
||||
_anchor="application-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route(
|
||||
"/applications/<application_id>/members/<application_role_id>/resend_invite",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_can(Permissions.EDIT_APPLICATION_MEMBER, message="resend application invitation")
|
||||
def resend_invite(application_id, application_role_id):
|
||||
app_role = ApplicationRoles.get_by_id(application_role_id)
|
||||
invite = app_role.latest_invitation
|
||||
form = MemberForm(http_request.form)
|
||||
|
||||
if form.validate():
|
||||
new_invite = ApplicationInvitations.resend(
|
||||
g.current_user, invite.token, form.data
|
||||
)
|
||||
|
||||
send_application_invitation(
|
||||
invitee_email=new_invite.email,
|
||||
inviter_name=g.current_user.full_name,
|
||||
token=new_invite.token,
|
||||
)
|
||||
|
||||
flash("application_invite_resent", email=new_invite.email)
|
||||
else:
|
||||
flash(
|
||||
"application_invite_error",
|
||||
user_name=app_role.user_name,
|
||||
application_name=g.application.name,
|
||||
)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=application_id,
|
||||
fragment="application-members",
|
||||
_anchor="application-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def build_subscription_payload(environment) -> SubscriptionCreationCSPPayload:
|
||||
csp_data = environment.portfolio.csp_data
|
||||
parent_group_id = environment.cloud_id
|
||||
invoice_section_name = csp_data["billing_profile_properties"]["invoice_sections"][
|
||||
0
|
||||
]["invoice_section_name"]
|
||||
|
||||
display_name = (
|
||||
f"{environment.application.name}-{environment.name}-{token_urlsafe(6)}"
|
||||
)
|
||||
|
||||
return SubscriptionCreationCSPPayload(
|
||||
tenant_id=csp_data.get("tenant_id"),
|
||||
display_name=display_name,
|
||||
parent_group_id=parent_group_id,
|
||||
billing_account_name=csp_data.get("billing_account_name"),
|
||||
billing_profile_name=csp_data.get("billing_profile_name"),
|
||||
invoice_section_name=invoice_section_name,
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route(
|
||||
"/environments/<environment_id>/add_subscription", methods=["POST"]
|
||||
)
|
||||
@user_can(Permissions.EDIT_ENVIRONMENT, message="create new environment subscription")
|
||||
def create_subscription(environment_id):
|
||||
environment = Environments.get(environment_id)
|
||||
|
||||
try:
|
||||
payload = build_subscription_payload(environment)
|
||||
app.csp.cloud.create_subscription(payload)
|
||||
flash("environment_subscription_success", name=environment.displayname)
|
||||
|
||||
except GeneralCSPException:
|
||||
flash("environment_subscription_failure")
|
||||
return (
|
||||
render_settings_page(application=environment.application, show_flash=True),
|
||||
400,
|
||||
)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=environment.application.id,
|
||||
fragment="application-environments",
|
||||
_anchor="application-environments",
|
||||
)
|
||||
)
|
78
atat/routes/ccpo.py
Normal file
78
atat/routes/ccpo.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from flask import (
|
||||
Blueprint,
|
||||
render_template,
|
||||
redirect,
|
||||
url_for,
|
||||
request,
|
||||
current_app as app,
|
||||
)
|
||||
from atat.domain.users import Users
|
||||
from atat.domain.audit_log import AuditLog
|
||||
from atat.domain.common import Paginator
|
||||
from atat.domain.exceptions import NotFoundError
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.forms.ccpo_user import CCPOUserForm
|
||||
from atat.models.permissions import Permissions
|
||||
from atat.utils.context_processors import atat as atat_context_processor
|
||||
from atat.utils.flash import formatted_flash as flash
|
||||
|
||||
|
||||
bp = Blueprint("ccpo", __name__)
|
||||
bp.context_processor(atat_context_processor)
|
||||
|
||||
|
||||
@bp.route("/activity-history")
|
||||
@user_can(Permissions.VIEW_AUDIT_LOG, message="view activity log")
|
||||
def activity_history():
|
||||
if app.config.get("USE_AUDIT_LOG", False):
|
||||
pagination_opts = Paginator.get_pagination_opts(request)
|
||||
audit_events = AuditLog.get_all_events(pagination_opts)
|
||||
return render_template("audit_log/audit_log.html", audit_events=audit_events)
|
||||
else:
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@bp.route("/ccpo-users")
|
||||
@user_can(Permissions.VIEW_CCPO_USER, message="view ccpo users")
|
||||
def users():
|
||||
users = Users.get_ccpo_users()
|
||||
users_info = [(user, CCPOUserForm(obj=user)) for user in users]
|
||||
return render_template("ccpo/users.html", users_info=users_info)
|
||||
|
||||
|
||||
@bp.route("/ccpo-users/new")
|
||||
@user_can(Permissions.CREATE_CCPO_USER, message="create ccpo user")
|
||||
def add_new_user():
|
||||
form = CCPOUserForm()
|
||||
return render_template("ccpo/add_user.html", form=form)
|
||||
|
||||
|
||||
@bp.route("/ccpo-users/new", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_CCPO_USER, message="create ccpo user")
|
||||
def submit_new_user():
|
||||
try:
|
||||
new_user = Users.get_by_dod_id(request.form["dod_id"])
|
||||
form = CCPOUserForm(obj=new_user)
|
||||
except NotFoundError:
|
||||
flash("ccpo_user_not_found")
|
||||
return redirect(url_for("ccpo.users"))
|
||||
|
||||
return render_template("ccpo/confirm_user.html", new_user=new_user, form=form)
|
||||
|
||||
|
||||
@bp.route("/ccpo-users/confirm-new", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_CCPO_USER, message="create ccpo user")
|
||||
def confirm_new_user():
|
||||
user = Users.get_by_dod_id(request.form["dod_id"])
|
||||
Users.give_ccpo_perms(user)
|
||||
flash("ccpo_user_added", user_name=user.full_name)
|
||||
return redirect(url_for("ccpo.users"))
|
||||
|
||||
|
||||
@bp.route("/ccpo-users/remove-access/<user_id>", methods=["POST"])
|
||||
@user_can(Permissions.DELETE_CCPO_USER, message="remove ccpo user")
|
||||
def remove_access(user_id):
|
||||
user = Users.get(user_id)
|
||||
Users.revoke_ccpo_perms(user)
|
||||
flash("ccpo_user_removed", user_name=user.full_name)
|
||||
return redirect(url_for("ccpo.users"))
|
186
atat/routes/dev.py
Normal file
186
atat/routes/dev.py
Normal file
@@ -0,0 +1,186 @@
|
||||
import random
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
request,
|
||||
redirect,
|
||||
render_template,
|
||||
url_for,
|
||||
current_app as app,
|
||||
)
|
||||
import pendulum
|
||||
|
||||
from . import redirect_after_login_url, current_user_setup
|
||||
from atat.domain.exceptions import AlreadyExistsError, NotFoundError
|
||||
from atat.domain.users import Users
|
||||
from atat.domain.permission_sets import PermissionSets
|
||||
from atat.forms.data import SERVICE_BRANCHES
|
||||
from atat.jobs import send_mail
|
||||
from atat.utils import pick
|
||||
|
||||
|
||||
bp = Blueprint("dev", __name__)
|
||||
|
||||
_ALL_PERMS = [
|
||||
PermissionSets.VIEW_PORTFOLIO,
|
||||
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||
PermissionSets.VIEW_PORTFOLIO_FUNDING,
|
||||
PermissionSets.VIEW_PORTFOLIO_REPORTS,
|
||||
PermissionSets.VIEW_PORTFOLIO_ADMIN,
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||
PermissionSets.EDIT_PORTFOLIO_FUNDING,
|
||||
PermissionSets.EDIT_PORTFOLIO_REPORTS,
|
||||
PermissionSets.EDIT_PORTFOLIO_ADMIN,
|
||||
PermissionSets.PORTFOLIO_POC,
|
||||
PermissionSets.VIEW_AUDIT_LOG,
|
||||
PermissionSets.MANAGE_CCPO_USERS,
|
||||
]
|
||||
|
||||
|
||||
def random_service_branch():
|
||||
return random.choice([k for k, v in SERVICE_BRANCHES if k]) # nosec
|
||||
|
||||
|
||||
_DEV_USERS = {
|
||||
"sam": {
|
||||
"dod_id": "6346349876",
|
||||
"first_name": "Sam",
|
||||
"last_name": "Stevenson",
|
||||
"permission_sets": _ALL_PERMS,
|
||||
"email": "sam@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
"amanda": {
|
||||
"dod_id": "2345678901",
|
||||
"first_name": "Amanda",
|
||||
"last_name": "Adamson",
|
||||
"email": "amanda@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
"brandon": {
|
||||
"dod_id": "3456789012",
|
||||
"first_name": "Brandon",
|
||||
"last_name": "Buchannan",
|
||||
"email": "brandon@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
"christina": {
|
||||
"dod_id": "4567890123",
|
||||
"first_name": "Christina",
|
||||
"last_name": "Collins",
|
||||
"email": "christina@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
"dominick": {
|
||||
"dod_id": "5678901234",
|
||||
"first_name": "Dominick",
|
||||
"last_name": "Domingo",
|
||||
"email": "dominick@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
"erica": {
|
||||
"dod_id": "6789012345",
|
||||
"first_name": "Erica",
|
||||
"last_name": "Eichner",
|
||||
"email": "erica@example.com",
|
||||
"service_branch": random_service_branch(),
|
||||
"phone_number": "1234567890",
|
||||
"citizenship": "United States",
|
||||
"designation": "Military",
|
||||
"date_latest_training": pendulum.date(2018, 1, 1),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class IncompleteInfoError(Exception):
|
||||
@property
|
||||
def message(self):
|
||||
return "You must provide each of: first_name, last_name and dod_id"
|
||||
|
||||
|
||||
@bp.route("/login-dev")
|
||||
def login_dev():
|
||||
dod_id = request.args.get("dod_id", None)
|
||||
|
||||
if dod_id is not None:
|
||||
user = Users.get_by_dod_id(dod_id)
|
||||
else:
|
||||
role = request.args.get("username", "amanda")
|
||||
user_data = _DEV_USERS[role]
|
||||
user = Users.get_or_create_by_dod_id(
|
||||
user_data["dod_id"],
|
||||
**pick(
|
||||
[
|
||||
"permission_sets",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"service_branch",
|
||||
"phone_number",
|
||||
"citizenship",
|
||||
"designation",
|
||||
"date_latest_training",
|
||||
],
|
||||
user_data,
|
||||
),
|
||||
)
|
||||
|
||||
current_user_setup(user)
|
||||
return redirect(redirect_after_login_url())
|
||||
|
||||
|
||||
@bp.route("/dev-new-user")
|
||||
def dev_new_user():
|
||||
first_name = request.args.get("first_name", None)
|
||||
last_name = request.args.get("last_name", None)
|
||||
dod_id = request.args.get("dod_id", None)
|
||||
|
||||
if None in [first_name, last_name, dod_id]:
|
||||
raise IncompleteInfoError()
|
||||
|
||||
try:
|
||||
Users.get_by_dod_id(dod_id)
|
||||
raise AlreadyExistsError("User with dod_id {}".format(dod_id))
|
||||
except NotFoundError:
|
||||
pass
|
||||
|
||||
new_user = {"first_name": first_name, "last_name": last_name}
|
||||
|
||||
created_user = Users.create(dod_id, **new_user)
|
||||
|
||||
current_user_setup(created_user)
|
||||
return redirect(redirect_after_login_url())
|
||||
|
||||
|
||||
@bp.route("/test-email")
|
||||
def test_email():
|
||||
send_mail.delay(
|
||||
[request.args.get("to")], request.args.get("subject"), request.args.get("body")
|
||||
)
|
||||
|
||||
return redirect(url_for("dev.messages"))
|
||||
|
||||
|
||||
@bp.route("/messages")
|
||||
def messages():
|
||||
return render_template("dev/emails.html", messages=app.mailer.messages)
|
85
atat/routes/errors.py
Normal file
85
atat/routes/errors.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from flask import render_template, current_app, url_for, redirect, request
|
||||
from flask_wtf.csrf import CSRFError
|
||||
import werkzeug.exceptions as werkzeug_exceptions
|
||||
|
||||
import atat.domain.exceptions as exceptions
|
||||
from atat.domain.invitations import (
|
||||
InvitationError,
|
||||
ExpiredError as InvitationExpiredError,
|
||||
WrongUserError as InvitationWrongUserError,
|
||||
)
|
||||
from atat.domain.authnid.crl import CRLInvalidException
|
||||
from atat.domain.portfolios import PortfolioError
|
||||
from atat.utils.flash import formatted_flash as flash
|
||||
from atat.utils.localization import translate
|
||||
|
||||
NO_NOTIFY_STATUS_CODES = set([404, 401])
|
||||
|
||||
|
||||
def log_error(e):
|
||||
error_message = e.message if hasattr(e, "message") else str(e)
|
||||
current_app.logger.exception(error_message)
|
||||
|
||||
|
||||
def notify(e, message, code):
|
||||
if code not in NO_NOTIFY_STATUS_CODES:
|
||||
current_app.notification_sender.send(message)
|
||||
|
||||
|
||||
def handle_error(e, message=translate("errors.not_found"), code=404):
|
||||
log_error(e)
|
||||
notify(e, message, code)
|
||||
return (render_template("error.html", message=message, code=code), code)
|
||||
|
||||
|
||||
def make_error_pages(app):
|
||||
@app.errorhandler(werkzeug_exceptions.NotFound)
|
||||
@app.errorhandler(exceptions.NotFoundError)
|
||||
@app.errorhandler(exceptions.UnauthorizedError)
|
||||
@app.errorhandler(PortfolioError)
|
||||
@app.errorhandler(exceptions.NoAccessError)
|
||||
# pylint: disable=unused-variable
|
||||
def not_found(e):
|
||||
return handle_error(e)
|
||||
|
||||
@app.errorhandler(CRLInvalidException)
|
||||
# pylint: disable=unused-variable
|
||||
def missing_crl(e):
|
||||
return handle_error(e, message="Error Code 008", code=401)
|
||||
|
||||
@app.errorhandler(exceptions.UnauthenticatedError)
|
||||
# pylint: disable=unused-variable
|
||||
def unauthorized(e):
|
||||
return handle_error(e, message="Log in Failed", code=401)
|
||||
|
||||
@app.errorhandler(CSRFError)
|
||||
# pylint: disable=unused-variable
|
||||
def session_expired(e):
|
||||
log_error(e)
|
||||
url_args = {"next": request.path}
|
||||
flash("session_expired")
|
||||
if request.method == "POST":
|
||||
url_args[app.form_cache.PARAM_NAME] = app.form_cache.write(request.form)
|
||||
return redirect(url_for("atat.root", **url_args))
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
# pylint: disable=unused-variable
|
||||
def exception(e):
|
||||
if current_app.debug:
|
||||
raise e
|
||||
return handle_error(e, message="An Unexpected Error Occurred", code=500)
|
||||
|
||||
@app.errorhandler(InvitationError)
|
||||
@app.errorhandler(InvitationWrongUserError)
|
||||
# pylint: disable=unused-variable
|
||||
def invalid_invitation(e):
|
||||
return handle_error(e, message="The link you followed is invalid.", code=404)
|
||||
|
||||
@app.errorhandler(InvitationExpiredError)
|
||||
# pylint: disable=unused-variable
|
||||
def invalid_invitation(e):
|
||||
return handle_error(
|
||||
e, message="The invitation you followed has expired.", code=404
|
||||
)
|
||||
|
||||
return app
|
7
atat/routes/portfolios/__init__.py
Normal file
7
atat/routes/portfolios/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from flask import request as http_request, g, render_template
|
||||
from operator import attrgetter
|
||||
|
||||
from . import index
|
||||
from . import invitations
|
||||
from . import admin
|
||||
from .blueprint import portfolios_bp
|
212
atat/routes/portfolios/admin.py
Normal file
212
atat/routes/portfolios/admin.py
Normal file
@@ -0,0 +1,212 @@
|
||||
from flask import render_template, request as http_request, g, redirect, url_for
|
||||
|
||||
from .blueprint import portfolios_bp
|
||||
from atat.domain.portfolios import Portfolios
|
||||
from atat.domain.portfolio_roles import PortfolioRoles
|
||||
from atat.models.portfolio_role import Status as PortfolioRoleStatus
|
||||
from atat.domain.invitations import PortfolioInvitations
|
||||
from atat.domain.permission_sets import PermissionSets
|
||||
from atat.domain.audit_log import AuditLog
|
||||
from atat.domain.common import Paginator
|
||||
from atat.forms.portfolio import PortfolioForm
|
||||
import atat.forms.portfolio_member as member_forms
|
||||
from atat.models.permissions import Permissions
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.utils import first_or_none
|
||||
from atat.utils.flash import formatted_flash as flash
|
||||
from atat.domain.exceptions import UnauthorizedError
|
||||
|
||||
|
||||
def filter_perm_sets_data(member):
|
||||
perm_sets_data = {
|
||||
"perms_app_mgmt": bool(
|
||||
member.has_permission_set(
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
|
||||
)
|
||||
),
|
||||
"perms_funding": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_FUNDING)
|
||||
),
|
||||
"perms_reporting": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS)
|
||||
),
|
||||
"perms_portfolio_mgmt": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||
),
|
||||
}
|
||||
|
||||
return perm_sets_data
|
||||
|
||||
|
||||
def filter_members_data(members_list):
|
||||
members_data = []
|
||||
for member in members_list:
|
||||
permission_sets = filter_perm_sets_data(member)
|
||||
ppoc = (
|
||||
PermissionSets.get(PermissionSets.PORTFOLIO_POC) in member.permission_sets
|
||||
)
|
||||
member_data = {
|
||||
"role_id": member.id,
|
||||
"user_name": member.user_name,
|
||||
"permission_sets": filter_perm_sets_data(member),
|
||||
"status": member.display_status,
|
||||
"ppoc": ppoc,
|
||||
"form": member_forms.PermissionsForm(permission_sets),
|
||||
}
|
||||
|
||||
if not ppoc:
|
||||
member_data["update_invite_form"] = (
|
||||
member_forms.NewForm(user_data=member.latest_invitation)
|
||||
if member.latest_invitation and member.latest_invitation.can_resend
|
||||
else member_forms.NewForm()
|
||||
)
|
||||
member_data["invite_token"] = (
|
||||
member.latest_invitation.token
|
||||
if member.latest_invitation and member.latest_invitation.can_resend
|
||||
else None
|
||||
)
|
||||
|
||||
members_data.append(member_data)
|
||||
|
||||
return sorted(members_data, key=lambda member: member["user_name"])
|
||||
|
||||
|
||||
def render_admin_page(portfolio, form=None):
|
||||
pagination_opts = Paginator.get_pagination_opts(http_request)
|
||||
audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts)
|
||||
portfolio_form = PortfolioForm(obj=portfolio)
|
||||
member_list = portfolio.members
|
||||
assign_ppoc_form = member_forms.AssignPPOCForm()
|
||||
|
||||
for pf_role in portfolio.roles:
|
||||
if pf_role.user != portfolio.owner and pf_role.is_active:
|
||||
assign_ppoc_form.role_id.choices += [(pf_role.id, pf_role.full_name)]
|
||||
|
||||
current_member = first_or_none(
|
||||
lambda m: m.user_id == g.current_user.id, portfolio.members
|
||||
)
|
||||
current_member_id = current_member.id if current_member else None
|
||||
|
||||
return render_template(
|
||||
"portfolios/admin.html",
|
||||
form=form,
|
||||
portfolio_form=portfolio_form,
|
||||
members=filter_members_data(member_list),
|
||||
new_manager_form=member_forms.NewForm(),
|
||||
assign_ppoc_form=assign_ppoc_form,
|
||||
portfolio=portfolio,
|
||||
audit_events=audit_events,
|
||||
user=g.current_user,
|
||||
current_member_id=current_member_id,
|
||||
applications_count=len(portfolio.applications),
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/admin")
|
||||
@user_can(Permissions.VIEW_PORTFOLIO_ADMIN, message="view portfolio admin page")
|
||||
def admin(portfolio_id):
|
||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||
return render_admin_page(portfolio)
|
||||
|
||||
|
||||
# Updating PPoC is a post-MVP feature
|
||||
# @portfolios_bp.route("/portfolios/<portfolio_id>/update_ppoc", methods=["POST"])
|
||||
# @user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
|
||||
# def update_ppoc(portfolio_id): # pragma: no cover
|
||||
# role_id = http_request.form.get("role_id")
|
||||
#
|
||||
# portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
# new_ppoc_role = PortfolioRoles.get_by_id(role_id)
|
||||
#
|
||||
# PortfolioRoles.make_ppoc(portfolio_role=new_ppoc_role)
|
||||
#
|
||||
# flash("primary_point_of_contact_changed", ppoc_name=new_ppoc_role.full_name)
|
||||
#
|
||||
# return redirect(
|
||||
# url_for(
|
||||
# "portfolios.admin",
|
||||
# portfolio_id=portfolio.id,
|
||||
# fragment="primary-point-of-contact",
|
||||
# _anchor="primary-point-of-contact",
|
||||
# )
|
||||
# )
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/edit", methods=["POST"])
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_NAME, message="edit portfolio")
|
||||
def edit(portfolio_id):
|
||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||
form = PortfolioForm(http_request.form)
|
||||
if form.validate():
|
||||
Portfolios.update(portfolio, form.data)
|
||||
return redirect(
|
||||
url_for("applications.portfolio_applications", portfolio_id=portfolio.id)
|
||||
)
|
||||
else:
|
||||
# rerender portfolio admin page
|
||||
return render_admin_page(portfolio, form)
|
||||
|
||||
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/members/<portfolio_role_id>/delete", methods=["POST"]
|
||||
)
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="update portfolio members")
|
||||
def remove_member(portfolio_id, portfolio_role_id):
|
||||
portfolio_role = PortfolioRoles.get_by_id(portfolio_role_id)
|
||||
|
||||
if g.current_user.id == portfolio_role.user_id:
|
||||
raise UnauthorizedError(
|
||||
g.current_user, "you cant remove yourself from the portfolio"
|
||||
)
|
||||
|
||||
portfolio = Portfolios.get(user=g.current_user, portfolio_id=portfolio_id)
|
||||
if portfolio_role.user_id == portfolio.owner.id:
|
||||
raise UnauthorizedError(
|
||||
g.current_user, "you can't delete the portfolios PPoC from the portfolio"
|
||||
)
|
||||
|
||||
if (
|
||||
portfolio_role.latest_invitation
|
||||
and portfolio_role.status == PortfolioRoleStatus.PENDING
|
||||
):
|
||||
PortfolioInvitations.revoke(portfolio_role.latest_invitation.token)
|
||||
else:
|
||||
PortfolioRoles.disable(portfolio_role=portfolio_role)
|
||||
|
||||
flash("portfolio_member_removed", member_name=portfolio_role.full_name)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.admin",
|
||||
portfolio_id=portfolio_id,
|
||||
_anchor="portfolio-members",
|
||||
fragment="portfolio-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/members/<portfolio_role_id>", methods=["POST"]
|
||||
)
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="update portfolio members")
|
||||
def update_member(portfolio_id, portfolio_role_id):
|
||||
form_data = http_request.form
|
||||
form = member_forms.PermissionsForm(formdata=form_data)
|
||||
portfolio_role = PortfolioRoles.get_by_id(portfolio_role_id)
|
||||
portfolio = Portfolios.get(user=g.current_user, portfolio_id=portfolio_id)
|
||||
|
||||
if form.validate() and portfolio.owner_role != portfolio_role:
|
||||
PortfolioRoles.update(portfolio_role, form.data["permission_sets"])
|
||||
flash("update_portfolio_member", member_name=portfolio_role.full_name)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.admin",
|
||||
portfolio_id=portfolio_id,
|
||||
_anchor="portfolio-members",
|
||||
fragment="portfolio-members",
|
||||
)
|
||||
)
|
||||
else:
|
||||
flash("update_portfolio_member_error", member_name=portfolio_role.full_name)
|
||||
return (render_admin_page(portfolio), 400)
|
5
atat/routes/portfolios/blueprint.py
Normal file
5
atat/routes/portfolios/blueprint.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
from atat.utils.context_processors import portfolio as portfolio_context_processor
|
||||
|
||||
portfolios_bp = Blueprint("portfolios", __name__)
|
||||
portfolios_bp.context_processor(portfolio_context_processor)
|
57
atat/routes/portfolios/index.py
Normal file
57
atat/routes/portfolios/index.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import pendulum
|
||||
from flask import redirect, render_template, url_for, request as http_request, g
|
||||
|
||||
from .blueprint import portfolios_bp
|
||||
from atat.forms.portfolio import PortfolioCreationForm
|
||||
from atat.domain.reports import Reports
|
||||
from atat.domain.portfolios import Portfolios
|
||||
from atat.models.permissions import Permissions
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.utils.flash import formatted_flash as flash
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/new")
|
||||
def new_portfolio_step_1():
|
||||
form = PortfolioCreationForm()
|
||||
return render_template("portfolios/new/step_1.html", form=form)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios", methods=["POST"])
|
||||
def create_portfolio():
|
||||
form = PortfolioCreationForm(http_request.form)
|
||||
|
||||
if form.validate():
|
||||
portfolio = Portfolios.create(user=g.current_user, portfolio_attrs=form.data)
|
||||
return redirect(
|
||||
url_for("applications.portfolio_applications", portfolio_id=portfolio.id)
|
||||
)
|
||||
else:
|
||||
return render_template("portfolios/new/step_1.html", form=form), 400
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/reports")
|
||||
@user_can(Permissions.VIEW_PORTFOLIO_REPORTS, message="view portfolio reports")
|
||||
def reports(portfolio_id):
|
||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
spending = Reports.get_portfolio_spending(portfolio)
|
||||
obligated = portfolio.total_obligated_funds
|
||||
remaining = obligated - (spending["invoiced"] + spending["estimated"])
|
||||
|
||||
current_obligated_funds = {
|
||||
**spending,
|
||||
"obligated": obligated,
|
||||
"remaining": remaining,
|
||||
}
|
||||
|
||||
if current_obligated_funds["remaining"] < 0:
|
||||
flash("insufficient_funds")
|
||||
|
||||
return render_template(
|
||||
"portfolios/reports/index.html",
|
||||
portfolio=portfolio,
|
||||
# wrapped in str() because this sum returns a Decimal object
|
||||
total_portfolio_value=str(portfolio.total_obligated_funds),
|
||||
current_obligated_funds=current_obligated_funds,
|
||||
expired_task_orders=Reports.expired_task_orders(portfolio),
|
||||
retrieved=pendulum.now(), # mocked datetime of reporting data retrival
|
||||
)
|
123
atat/routes/portfolios/invitations.py
Normal file
123
atat/routes/portfolios/invitations.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from flask import g, redirect, url_for, render_template, request as http_request
|
||||
|
||||
from .blueprint import portfolios_bp
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.domain.exceptions import AlreadyExistsError
|
||||
from atat.domain.invitations import PortfolioInvitations
|
||||
from atat.domain.portfolios import Portfolios
|
||||
from atat.models import Permissions
|
||||
from atat.jobs import send_mail
|
||||
from atat.utils.flash import formatted_flash as flash
|
||||
from atat.utils.localization import translate
|
||||
import atat.forms.portfolio_member as member_forms
|
||||
|
||||
|
||||
def send_portfolio_invitation(invitee_email, inviter_name, token):
|
||||
body = render_template(
|
||||
"emails/portfolio/invitation.txt", owner=inviter_name, token=token
|
||||
)
|
||||
send_mail.delay(
|
||||
[invitee_email],
|
||||
translate("email.portfolio_invite", {"inviter_name": inviter_name}),
|
||||
body,
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/invitations/<portfolio_token>", methods=["GET"])
|
||||
def accept_invitation(portfolio_token):
|
||||
invite = PortfolioInvitations.accept(g.current_user, portfolio_token)
|
||||
|
||||
return redirect(
|
||||
url_for("applications.portfolio_applications", portfolio_id=invite.portfolio.id)
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/invitations/<portfolio_token>/revoke", methods=["POST"]
|
||||
)
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="revoke invitation")
|
||||
def revoke_invitation(portfolio_id, portfolio_token):
|
||||
invite = PortfolioInvitations.revoke(portfolio_token)
|
||||
|
||||
flash(
|
||||
"invite_revoked",
|
||||
resource="Portfolio",
|
||||
user_name=invite.user_name,
|
||||
resource_name=g.portfolio.name,
|
||||
)
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.admin",
|
||||
portfolio_id=portfolio_id,
|
||||
_anchor="portfolio-members",
|
||||
fragment="portfolio-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/invitations/<portfolio_token>/resend", methods=["POST"]
|
||||
)
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="resend invitation")
|
||||
def resend_invitation(portfolio_id, portfolio_token):
|
||||
form = member_forms.NewForm(http_request.form)
|
||||
|
||||
if form.validate():
|
||||
invite = PortfolioInvitations.resend(
|
||||
g.current_user, portfolio_token, form.data["user_data"]
|
||||
)
|
||||
send_portfolio_invitation(
|
||||
invitee_email=invite.email,
|
||||
inviter_name=g.current_user.full_name,
|
||||
token=invite.token,
|
||||
)
|
||||
flash("resend_portfolio_invitation", email=invite.email)
|
||||
else:
|
||||
user_name = f"{form['user_data']['first_name'].data} {form['user_data']['last_name'].data}"
|
||||
flash("resend_portfolio_invitation_error", user_name=user_name)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.admin",
|
||||
portfolio_id=portfolio_id,
|
||||
fragment="portfolio-members",
|
||||
_anchor="portfolio-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/members/new", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_PORTFOLIO_USERS, message="create new portfolio member")
|
||||
def invite_member(portfolio_id):
|
||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
form = member_forms.NewForm(http_request.form)
|
||||
|
||||
if form.validate():
|
||||
try:
|
||||
invite = Portfolios.invite(portfolio, g.current_user, form.data)
|
||||
send_portfolio_invitation(
|
||||
invitee_email=invite.email,
|
||||
inviter_name=g.current_user.full_name,
|
||||
token=invite.token,
|
||||
)
|
||||
|
||||
flash(
|
||||
"new_portfolio_member", user_name=invite.user_name, portfolio=portfolio
|
||||
)
|
||||
|
||||
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(
|
||||
"portfolios.admin",
|
||||
portfolio_id=portfolio_id,
|
||||
fragment="portfolio-members",
|
||||
_anchor="portfolio-members",
|
||||
)
|
||||
)
|
4
atat/routes/task_orders/__init__.py
Normal file
4
atat/routes/task_orders/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import index
|
||||
from . import new
|
||||
from . import downloads
|
||||
from .blueprint import task_orders_bp
|
5
atat/routes/task_orders/blueprint.py
Normal file
5
atat/routes/task_orders/blueprint.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
from atat.utils.context_processors import portfolio as portfolio_context_processor
|
||||
|
||||
task_orders_bp = Blueprint("task_orders", __name__)
|
||||
task_orders_bp.context_processor(portfolio_context_processor)
|
27
atat/routes/task_orders/downloads.py
Normal file
27
atat/routes/task_orders/downloads.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from flask import Response, current_app as app
|
||||
|
||||
from .blueprint import task_orders_bp
|
||||
from atat.domain.task_orders import TaskOrders
|
||||
from atat.domain.exceptions import NotFoundError
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.models.permissions import Permissions
|
||||
|
||||
|
||||
def send_file(attachment):
|
||||
generator = app.csp.files.download(attachment.object_name)
|
||||
return Response(
|
||||
generator,
|
||||
headers={
|
||||
"Content-Disposition": "attachment; filename={}".format(attachment.filename)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/pdf")
|
||||
@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="download task order PDF")
|
||||
def download_task_order_pdf(task_order_id):
|
||||
task_order = TaskOrders.get(task_order_id)
|
||||
if task_order.pdf:
|
||||
return send_file(task_order.pdf)
|
||||
else:
|
||||
raise NotFoundError("task_order pdf")
|
35
atat/routes/task_orders/index.py
Normal file
35
atat/routes/task_orders/index.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from flask import g, render_template, url_for, redirect
|
||||
|
||||
from .blueprint import task_orders_bp
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.domain.portfolios import Portfolios
|
||||
from atat.domain.task_orders import TaskOrders
|
||||
from atat.forms.task_order import SignatureForm
|
||||
from atat.models import Permissions
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>")
|
||||
@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="view task order details")
|
||||
def view_task_order(task_order_id):
|
||||
task_order = TaskOrders.get(task_order_id)
|
||||
if task_order.is_draft:
|
||||
return redirect(url_for("task_orders.edit", task_order_id=task_order.id))
|
||||
else:
|
||||
signature_form = SignatureForm()
|
||||
return render_template(
|
||||
"task_orders/view.html",
|
||||
task_order=task_order,
|
||||
signature_form=signature_form,
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/portfolios/<portfolio_id>/task_orders")
|
||||
@user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding")
|
||||
def portfolio_funding(portfolio_id):
|
||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
task_orders = TaskOrders.sort_by_status(portfolio.task_orders)
|
||||
to_count = len(portfolio.task_orders)
|
||||
# TODO: Get expended amount from the CSP
|
||||
return render_template(
|
||||
"task_orders/index.html", task_orders=task_orders, to_count=to_count
|
||||
)
|
311
atat/routes/task_orders/new.py
Normal file
311
atat/routes/task_orders/new.py
Normal file
@@ -0,0 +1,311 @@
|
||||
from flask import (
|
||||
g,
|
||||
redirect,
|
||||
render_template,
|
||||
request as http_request,
|
||||
url_for,
|
||||
current_app as app,
|
||||
jsonify,
|
||||
)
|
||||
|
||||
from .blueprint import task_orders_bp
|
||||
from atat.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atat.domain.exceptions import NoAccessError, AlreadyExistsError
|
||||
from atat.domain.task_orders import TaskOrders
|
||||
from atat.forms.task_order import TaskOrderForm, SignatureForm
|
||||
from atat.models.permissions import Permissions
|
||||
from atat.utils.flash import formatted_flash as flash
|
||||
|
||||
|
||||
def render_task_orders_edit(
|
||||
template, portfolio_id=None, task_order_id=None, form=None, extra_args=None
|
||||
):
|
||||
render_args = extra_args or {}
|
||||
|
||||
render_args["contract_start"] = app.config.get("CONTRACT_START_DATE")
|
||||
render_args["contract_end"] = app.config.get("CONTRACT_END_DATE")
|
||||
render_args["file_size_limit"] = int(app.config.get("FILE_SIZE_LIMIT"))
|
||||
|
||||
if task_order_id:
|
||||
task_order = TaskOrders.get(task_order_id)
|
||||
portfolio_id = task_order.portfolio_id
|
||||
render_args["form"] = form or TaskOrderForm(obj=task_order)
|
||||
render_args["task_order_id"] = task_order_id
|
||||
render_args["task_order"] = task_order
|
||||
else:
|
||||
render_args["form"] = form or TaskOrderForm()
|
||||
|
||||
render_args["cancel_save_url"] = url_for(
|
||||
"task_orders.cancel_edit",
|
||||
task_order_id=task_order_id,
|
||||
portfolio_id=portfolio_id,
|
||||
save=True,
|
||||
)
|
||||
render_args["cancel_discard_url"] = url_for(
|
||||
"task_orders.cancel_edit",
|
||||
task_order_id=task_order_id,
|
||||
portfolio_id=portfolio_id,
|
||||
save=False,
|
||||
)
|
||||
|
||||
return render_template(template, **render_args)
|
||||
|
||||
|
||||
def update_task_order(form, portfolio_id=None, task_order_id=None, flash_invalid=True):
|
||||
if form.validate(flash_invalid=flash_invalid):
|
||||
task_order = None
|
||||
try:
|
||||
if task_order_id:
|
||||
task_order = TaskOrders.update(task_order_id, **form.data)
|
||||
portfolio_id = task_order.portfolio_id
|
||||
else:
|
||||
task_order = TaskOrders.create(portfolio_id, **form.data)
|
||||
|
||||
return task_order
|
||||
|
||||
except AlreadyExistsError:
|
||||
flash("task_order_number_error", to_number=form.data["number"])
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def update_and_render_next(
|
||||
form_data,
|
||||
next_page,
|
||||
current_template,
|
||||
portfolio_id=None,
|
||||
task_order_id=None,
|
||||
previous=False,
|
||||
):
|
||||
form = None
|
||||
if task_order_id:
|
||||
task_order = TaskOrders.get(task_order_id)
|
||||
form = TaskOrderForm(form_data, obj=task_order)
|
||||
else:
|
||||
form = TaskOrderForm(form_data)
|
||||
|
||||
task_order = update_task_order(
|
||||
form, portfolio_id, task_order_id, flash_invalid=(not previous)
|
||||
)
|
||||
if task_order or previous:
|
||||
to_id = task_order.id if task_order else task_order_id
|
||||
return redirect(url_for(next_page, task_order_id=to_id))
|
||||
else:
|
||||
return (
|
||||
render_task_orders_edit(
|
||||
current_template, portfolio_id, task_order_id, form
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<portfolio_id>/upload_token")
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form")
|
||||
def upload_token(portfolio_id):
|
||||
(token, object_name) = app.csp.files.get_token()
|
||||
render_args = {"token": token, "objectName": object_name}
|
||||
|
||||
return jsonify(render_args)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<portfolio_id>/download_link")
|
||||
@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="view task order download link")
|
||||
def download_link(portfolio_id):
|
||||
filename = http_request.args.get("filename")
|
||||
object_name = http_request.args.get("objectName")
|
||||
render_args = {
|
||||
"downloadLink": app.csp.files.generate_download_link(object_name, filename)
|
||||
}
|
||||
|
||||
return jsonify(render_args)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/edit")
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="edit task order form")
|
||||
def edit(task_order_id):
|
||||
task_order = TaskOrders.get(task_order_id)
|
||||
|
||||
if not task_order.pdf:
|
||||
return redirect(
|
||||
url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order_id)
|
||||
)
|
||||
elif not task_order.number:
|
||||
return redirect(
|
||||
url_for("task_orders.form_step_two_add_number", task_order_id=task_order_id)
|
||||
)
|
||||
elif not task_order.clins_are_completed:
|
||||
return redirect(
|
||||
url_for(
|
||||
"task_orders.form_step_three_add_clins", task_order_id=task_order_id
|
||||
)
|
||||
)
|
||||
elif task_order.is_completed:
|
||||
return redirect(
|
||||
url_for("task_orders.form_step_four_review", task_order_id=task_order_id)
|
||||
)
|
||||
else:
|
||||
return redirect(
|
||||
url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order_id)
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/portfolios/<portfolio_id>/task_orders/form/step_1")
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_1")
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="view task order form")
|
||||
def form_step_one_add_pdf(portfolio_id=None, task_order_id=None):
|
||||
return render_task_orders_edit(
|
||||
"task_orders/step_1.html",
|
||||
portfolio_id=portfolio_id,
|
||||
task_order_id=task_order_id,
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route(
|
||||
"/portfolios/<portfolio_id>/task_orders/form/step-1", methods=["POST"]
|
||||
)
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_1", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form")
|
||||
def submit_form_step_one_add_pdf(portfolio_id=None, task_order_id=None):
|
||||
form_data = {**http_request.form}
|
||||
next_page = "task_orders.form_step_two_add_number"
|
||||
current_template = "task_orders/step_1.html"
|
||||
|
||||
return update_and_render_next(
|
||||
form_data,
|
||||
next_page,
|
||||
current_template,
|
||||
portfolio_id=portfolio_id,
|
||||
task_order_id=task_order_id,
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route(
|
||||
"/portfolios/<portfolio_id>/task_orders/form/cancel", methods=["POST"]
|
||||
)
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/cancel", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="cancel task order form")
|
||||
def cancel_edit(task_order_id=None, portfolio_id=None):
|
||||
# Either save the currently entered data, or delete the TO
|
||||
save = http_request.args.get("save", "True").lower() == "true"
|
||||
|
||||
if save:
|
||||
form_data = {**http_request.form}
|
||||
form = None
|
||||
if task_order_id:
|
||||
task_order = TaskOrders.get(task_order_id)
|
||||
form = TaskOrderForm(form_data, obj=task_order)
|
||||
else:
|
||||
form = TaskOrderForm(form_data)
|
||||
|
||||
update_task_order(form, portfolio_id, task_order_id, flash_invalid=False)
|
||||
|
||||
elif not save and task_order_id:
|
||||
TaskOrders.delete(task_order_id)
|
||||
|
||||
return redirect(
|
||||
url_for("task_orders.portfolio_funding", portfolio_id=g.portfolio.id)
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_2")
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="view task order form")
|
||||
def form_step_two_add_number(task_order_id):
|
||||
return render_task_orders_edit(
|
||||
"task_orders/step_2.html", task_order_id=task_order_id
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_2", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form")
|
||||
def submit_form_step_two_add_number(task_order_id):
|
||||
previous = http_request.args.get("previous", "False").lower() == "true"
|
||||
form_data = {**http_request.form}
|
||||
next_page = (
|
||||
"task_orders.form_step_three_add_clins"
|
||||
if not previous
|
||||
else "task_orders.form_step_one_add_pdf"
|
||||
)
|
||||
current_template = "task_orders/step_2.html"
|
||||
|
||||
return update_and_render_next(
|
||||
form_data,
|
||||
next_page,
|
||||
current_template,
|
||||
task_order_id=task_order_id,
|
||||
previous=previous,
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_3")
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="view task order form")
|
||||
def form_step_three_add_clins(task_order_id):
|
||||
return render_task_orders_edit(
|
||||
"task_orders/step_3.html", task_order_id=task_order_id
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_3", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form")
|
||||
def submit_form_step_three_add_clins(task_order_id):
|
||||
previous = http_request.args.get("previous", "False").lower() == "true"
|
||||
form_data = {**http_request.form}
|
||||
next_page = (
|
||||
"task_orders.form_step_four_review"
|
||||
if not previous
|
||||
else "task_orders.form_step_two_add_number"
|
||||
)
|
||||
current_template = "task_orders/step_3.html"
|
||||
|
||||
return update_and_render_next(
|
||||
form_data,
|
||||
next_page,
|
||||
current_template,
|
||||
task_order_id=task_order_id,
|
||||
previous=previous,
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_4")
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="view task order form")
|
||||
def form_step_four_review(task_order_id):
|
||||
task_order = TaskOrders.get(task_order_id)
|
||||
|
||||
extra_args = {
|
||||
"pdf_download_url": app.csp.files.generate_download_link(
|
||||
task_order.pdf.object_name, task_order.pdf.filename
|
||||
)
|
||||
}
|
||||
|
||||
if task_order.is_completed == False:
|
||||
raise NoAccessError("task order form review")
|
||||
|
||||
return render_task_orders_edit(
|
||||
"task_orders/step_4.html", task_order_id=task_order_id, extra_args=extra_args
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_5")
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="view task order form")
|
||||
def form_step_five_confirm_signature(task_order_id):
|
||||
task_order = TaskOrders.get(task_order_id)
|
||||
|
||||
if task_order.is_completed == False:
|
||||
raise NoAccessError("task order form signature")
|
||||
|
||||
return render_task_orders_edit(
|
||||
"task_orders/step_5.html", task_order_id=task_order_id, form=SignatureForm()
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/submit", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, "submit task order")
|
||||
def submit_task_order(task_order_id):
|
||||
task_order = TaskOrders.get(task_order_id)
|
||||
TaskOrders.sign(task_order=task_order, signer_dod_id=g.current_user.dod_id)
|
||||
|
||||
flash("task_order_submitted", task_order=task_order)
|
||||
|
||||
return redirect(
|
||||
url_for("task_orders.portfolio_funding", portfolio_id=task_order.portfolio_id)
|
||||
)
|
49
atat/routes/users.py
Normal file
49
atat/routes/users.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import pendulum
|
||||
from flask import Blueprint, render_template, g, request as http_request, redirect
|
||||
from atat.forms.edit_user import EditUserForm
|
||||
from atat.domain.users import Users
|
||||
from atat.utils.flash import formatted_flash as flash
|
||||
from atat.routes import match_url_pattern
|
||||
|
||||
|
||||
bp = Blueprint("users", __name__)
|
||||
|
||||
|
||||
@bp.route("/user")
|
||||
def user():
|
||||
user = g.current_user
|
||||
form = EditUserForm(data=user.to_dictionary())
|
||||
next_ = http_request.args.get("next")
|
||||
|
||||
if next_:
|
||||
flash("user_must_complete_profile")
|
||||
|
||||
return render_template(
|
||||
"user/edit.html",
|
||||
next=next_,
|
||||
form=form,
|
||||
user=user,
|
||||
mindate=pendulum.now(tz="utc").subtract(days=365),
|
||||
maxdate=pendulum.now(tz="utc"),
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/user", methods=["POST"])
|
||||
def update_user():
|
||||
user = g.current_user
|
||||
form = EditUserForm(http_request.form)
|
||||
next_url = http_request.args.get("next")
|
||||
if form.validate():
|
||||
Users.update(user, form.data)
|
||||
flash("user_updated")
|
||||
if match_url_pattern(next_url):
|
||||
return redirect(next_url)
|
||||
|
||||
return render_template(
|
||||
"user/edit.html",
|
||||
form=form,
|
||||
user=user,
|
||||
next=next_url,
|
||||
mindate=pendulum.now(tz="utc").subtract(days=365),
|
||||
maxdate=pendulum.now(tz="utc"),
|
||||
)
|
Reference in New Issue
Block a user