Merge branch 'staging' into azure-custom-integration
This commit is contained in:
@@ -159,6 +159,7 @@ def map_config(config):
|
||||
"ENV": config["default"]["ENVIRONMENT"],
|
||||
"BROKER_URL": config["default"]["REDIS_URI"],
|
||||
"DEBUG": config["default"].getboolean("DEBUG"),
|
||||
"DEBUG_MAILER": config["default"].getboolean("DEBUG_MAILER"),
|
||||
"SQLALCHEMY_ECHO": config["default"].getboolean("SQLALCHEMY_ECHO"),
|
||||
"PORT": int(config["default"]["PORT"]),
|
||||
"SQLALCHEMY_DATABASE_URI": config["default"]["DATABASE_URI"],
|
||||
@@ -221,7 +222,7 @@ def make_config(direct_config=None):
|
||||
config.read_dict({"default": direct_config})
|
||||
|
||||
# Assemble DATABASE_URI value
|
||||
database_uri = "postgres://{}:{}@{}:{}/{}".format( # pragma: allowlist secret
|
||||
database_uri = "postgresql://{}:{}@{}:{}/{}".format( # pragma: allowlist secret
|
||||
config.get("default", "PGUSER"),
|
||||
config.get("default", "PGPASSWORD"),
|
||||
config.get("default", "PGHOST"),
|
||||
@@ -289,7 +290,7 @@ def make_crl_validator(app):
|
||||
|
||||
|
||||
def make_mailer(app):
|
||||
if app.config["DEBUG"]:
|
||||
if app.config["DEBUG"] or app.config["DEBUG_MAILER"]:
|
||||
mailer_connection = mailer.RedisConnection(app.redis)
|
||||
else:
|
||||
mailer_connection = mailer.SMTPConnection(
|
||||
|
@@ -1,4 +1,13 @@
|
||||
from flask import g, redirect, url_for, session, request, current_app as app
|
||||
from flask import (
|
||||
g,
|
||||
redirect,
|
||||
url_for,
|
||||
session,
|
||||
request,
|
||||
current_app as app,
|
||||
_request_ctx_stack as request_ctx_stack,
|
||||
)
|
||||
from werkzeug.datastructures import ImmutableTypeConversionDict
|
||||
|
||||
from atst.domain.users import Users
|
||||
|
||||
@@ -10,7 +19,6 @@ UNPROTECTED_ROUTES = [
|
||||
"atst.login_redirect",
|
||||
"atst.logout",
|
||||
"atst.unauthorized",
|
||||
"atst.helpdocs",
|
||||
"static",
|
||||
"atst.about",
|
||||
]
|
||||
@@ -57,12 +65,26 @@ def get_last_login():
|
||||
return session.get("user_id") and session.get("last_login")
|
||||
|
||||
|
||||
def _nullify_session(session):
|
||||
session_key = f"{app.config.get('SESSION_KEY_PREFIX')}{session.sid}"
|
||||
app.redis.delete(session_key)
|
||||
request.cookies = ImmutableTypeConversionDict()
|
||||
request_ctx_stack.top.session = app.session_interface.open_session(app, request)
|
||||
|
||||
|
||||
def _current_dod_id():
|
||||
return g.current_user.dod_id if session.get("user_id") else None
|
||||
|
||||
|
||||
def logout():
|
||||
if session.get("user_id"): # pragma: no branch
|
||||
dod_id = g.current_user.dod_id
|
||||
del session["user_id"]
|
||||
del session["last_login"]
|
||||
dod_id = _current_dod_id()
|
||||
|
||||
_nullify_session(session)
|
||||
|
||||
if dod_id:
|
||||
app.logger.info(f"user with EDIPI {dod_id} has logged out")
|
||||
else:
|
||||
app.logger.info("unauthenticated user has logged out")
|
||||
|
||||
|
||||
def _unprotected_route(request):
|
||||
|
@@ -490,6 +490,12 @@ class CloudProviderInterface:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_subscription(self, environment):
|
||||
"""Returns True if a new subscription has been created or raises an
|
||||
exception if an error occurs while creating a subscription.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class MockCloudProvider(CloudProviderInterface):
|
||||
|
||||
@@ -763,6 +769,11 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
|
||||
return self._maybe(12)
|
||||
|
||||
def create_subscription(self, environment):
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
||||
|
||||
return True
|
||||
|
||||
def get_calculator_url(self):
|
||||
return "https://www.rackspace.com/en-us/calculator"
|
||||
|
||||
|
@@ -1,14 +1,14 @@
|
||||
from .forms import BaseForm, remove_empty_string
|
||||
from wtforms.fields import StringField, TextAreaField, FieldList
|
||||
from wtforms.validators import Required, Optional
|
||||
from atst.forms.validators import ListItemRequired, ListItemsUnique
|
||||
from wtforms.validators import Required, Optional, Length
|
||||
from atst.forms.validators import ListItemRequired, ListItemsUnique, Name, AlphaNumeric
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
class EditEnvironmentForm(BaseForm):
|
||||
name = StringField(
|
||||
label=translate("forms.environments.name_label"),
|
||||
validators=[Required()],
|
||||
validators=[Required(), Name(), Length(max=100)],
|
||||
filters=[remove_empty_string],
|
||||
)
|
||||
|
||||
@@ -16,12 +16,12 @@ class EditEnvironmentForm(BaseForm):
|
||||
class NameAndDescriptionForm(BaseForm):
|
||||
name = StringField(
|
||||
label=translate("forms.application.name_label"),
|
||||
validators=[Required()],
|
||||
validators=[Required(), Name(), Length(max=100)],
|
||||
filters=[remove_empty_string],
|
||||
)
|
||||
description = TextAreaField(
|
||||
label=translate("forms.application.description_label"),
|
||||
validators=[Optional()],
|
||||
validators=[Optional(), Length(max=1_000)],
|
||||
filters=[remove_empty_string],
|
||||
)
|
||||
|
||||
@@ -31,6 +31,7 @@ class EnvironmentsForm(BaseForm):
|
||||
StringField(
|
||||
label=translate("forms.application.environment_names_label"),
|
||||
filters=[remove_empty_string],
|
||||
validators=[AlphaNumeric(), Length(max=100)],
|
||||
),
|
||||
validators=[
|
||||
ListItemRequired(
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms.fields import FormField, FieldList, HiddenField, BooleanField
|
||||
from wtforms.validators import UUID
|
||||
from wtforms import Form
|
||||
|
||||
from .member import NewForm as BaseNewMemberForm
|
||||
@@ -7,11 +8,13 @@ from .data import ENV_ROLES, ENV_ROLE_NO_ACCESS as NO_ACCESS
|
||||
from atst.forms.fields import SelectField
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from atst.utils.localization import translate
|
||||
from atst.forms.validators import AlphaNumeric
|
||||
from wtforms.validators import Length
|
||||
|
||||
|
||||
class EnvironmentForm(Form):
|
||||
environment_id = HiddenField()
|
||||
environment_name = HiddenField()
|
||||
environment_id = HiddenField(validators=[UUID()])
|
||||
environment_name = HiddenField(validators=[AlphaNumeric(), Length(max=100)])
|
||||
role = SelectField(
|
||||
environment_name,
|
||||
choices=ENV_ROLES,
|
||||
@@ -43,13 +46,6 @@ class PermissionsForm(FlaskForm):
|
||||
"portfolios.applications.members.form.team_mgmt.description"
|
||||
),
|
||||
)
|
||||
perms_del_env = BooleanField(
|
||||
translate("portfolios.applications.members.form.del_env.label"),
|
||||
default=False,
|
||||
description=translate(
|
||||
"portfolios.applications.members.form.del_env.description"
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
@@ -63,9 +59,6 @@ class PermissionsForm(FlaskForm):
|
||||
if _data["perms_team_mgmt"]:
|
||||
perm_sets.append(PermissionSets.EDIT_APPLICATION_TEAM)
|
||||
|
||||
if _data["perms_del_env"]:
|
||||
perm_sets.append(PermissionSets.DELETE_APPLICATION_ENVIRONMENTS)
|
||||
|
||||
_data["permission_sets"] = perm_sets
|
||||
return _data
|
||||
|
||||
|
@@ -2,12 +2,12 @@ from flask_wtf import FlaskForm
|
||||
from wtforms.validators import Required, Length
|
||||
from wtforms.fields import StringField
|
||||
|
||||
from atst.forms.validators import IsNumber
|
||||
from atst.forms.validators import Number
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
class CCPOUserForm(FlaskForm):
|
||||
dod_id = StringField(
|
||||
translate("forms.new_member.dod_id_label"),
|
||||
validators=[Required(), Length(min=10, max=10), IsNumber()],
|
||||
validators=[Required(), Length(min=10, max=10), Number()],
|
||||
)
|
||||
|
@@ -9,22 +9,26 @@ from .forms import BaseForm
|
||||
from .data import SERVICE_BRANCHES
|
||||
from atst.models.user import User
|
||||
from atst.utils.localization import translate
|
||||
from wtforms.validators import Length
|
||||
from atst.forms.validators import Number
|
||||
|
||||
from .validators import Name, DateRange, PhoneNumber
|
||||
|
||||
|
||||
USER_FIELDS = {
|
||||
"first_name": StringField(
|
||||
translate("forms.edit_user.first_name_label"), validators=[Name()]
|
||||
translate("forms.edit_user.first_name_label"),
|
||||
validators=[Name(), Length(max=100)],
|
||||
),
|
||||
"last_name": StringField(
|
||||
translate("forms.edit_user.last_name_label"), validators=[Name()]
|
||||
translate("forms.edit_user.last_name_label"),
|
||||
validators=[Name(), Length(max=100)],
|
||||
),
|
||||
"email": EmailField(translate("forms.edit_user.email_label"), validators=[Email()]),
|
||||
"phone_number": TelField(
|
||||
translate("forms.edit_user.phone_number_label"), validators=[PhoneNumber()]
|
||||
),
|
||||
"phone_ext": StringField("Extension"),
|
||||
"phone_ext": StringField("Extension", validators=[Number(), Length(max=10)]),
|
||||
"service_branch": SelectField(
|
||||
translate("forms.edit_user.service_branch_label"), choices=SERVICE_BRANCHES
|
||||
),
|
||||
|
@@ -3,16 +3,18 @@ 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.forms.validators import Number, PhoneNumber, Name
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
class NewForm(FlaskForm):
|
||||
first_name = StringField(
|
||||
label=translate("forms.new_member.first_name_label"), validators=[Required()]
|
||||
label=translate("forms.new_member.first_name_label"),
|
||||
validators=[Required(), Name(), Length(max=100)],
|
||||
)
|
||||
last_name = StringField(
|
||||
label=translate("forms.new_member.last_name_label"), validators=[Required()]
|
||||
label=translate("forms.new_member.last_name_label"),
|
||||
validators=[Required(), Name(), Length(max=100)],
|
||||
)
|
||||
email = EmailField(
|
||||
translate("forms.new_member.email_label"), validators=[Required(), Email()]
|
||||
@@ -21,8 +23,8 @@ class NewForm(FlaskForm):
|
||||
translate("forms.new_member.phone_number_label"),
|
||||
validators=[Optional(), PhoneNumber()],
|
||||
)
|
||||
phone_ext = StringField("Extension")
|
||||
phone_ext = StringField("Extension", validators=[Number(), Length(max=10)])
|
||||
dod_id = StringField(
|
||||
translate("forms.new_member.dod_id_label"),
|
||||
validators=[Required(), Length(min=10), IsNumber()],
|
||||
validators=[Required(), Length(min=10), Number()],
|
||||
)
|
||||
|
@@ -4,6 +4,7 @@ from wtforms.fields import (
|
||||
TextAreaField,
|
||||
)
|
||||
from wtforms.validators import Length, InputRequired
|
||||
from atst.forms.validators import Name
|
||||
from wtforms.widgets import ListWidget, CheckboxInput
|
||||
|
||||
from .forms import BaseForm
|
||||
@@ -20,14 +21,18 @@ class PortfolioForm(BaseForm):
|
||||
min=4,
|
||||
max=100,
|
||||
message=translate("forms.portfolio.name.length_validation_message"),
|
||||
)
|
||||
),
|
||||
Name(),
|
||||
],
|
||||
)
|
||||
description = TextAreaField(translate("forms.portfolio.description.label"),)
|
||||
description = TextAreaField(
|
||||
translate("forms.portfolio.description.label"), validators=[Length(max=1_000)]
|
||||
)
|
||||
|
||||
|
||||
class PortfolioCreationForm(PortfolioForm):
|
||||
defense_component = SelectMultipleField(
|
||||
translate("forms.portfolio.defense_component.title"),
|
||||
choices=SERVICE_BRANCHES,
|
||||
widget=ListWidget(prefix_label=False),
|
||||
option_widget=CheckboxInput(),
|
||||
|
@@ -7,9 +7,15 @@ from wtforms.fields import (
|
||||
HiddenField,
|
||||
)
|
||||
from wtforms.fields.html5 import DateField
|
||||
from wtforms.validators import Required, Length, NumberRange, ValidationError
|
||||
from wtforms.validators import (
|
||||
Required,
|
||||
Length,
|
||||
NumberRange,
|
||||
ValidationError,
|
||||
)
|
||||
from flask_wtf import FlaskForm
|
||||
from numbers import Number
|
||||
import numbers
|
||||
from atst.forms.validators import Number, AlphaNumeric
|
||||
|
||||
from .data import JEDI_CLIN_TYPES
|
||||
from .fields import SelectField
|
||||
@@ -17,7 +23,7 @@ from .forms import BaseForm, remove_empty_string
|
||||
from atst.utils.localization import translate
|
||||
from flask import current_app as app
|
||||
|
||||
MAX_CLIN_AMOUNT = 1000000000
|
||||
MAX_CLIN_AMOUNT = 1_000_000_000
|
||||
|
||||
|
||||
def coerce_enum(enum_inst):
|
||||
@@ -29,8 +35,8 @@ def coerce_enum(enum_inst):
|
||||
|
||||
def validate_funding(form, field):
|
||||
if (
|
||||
isinstance(form.total_amount.data, Number)
|
||||
and isinstance(field.data, Number)
|
||||
isinstance(form.total_amount.data, numbers.Number)
|
||||
and isinstance(field.data, numbers.Number)
|
||||
and form.total_amount.data < field.data
|
||||
):
|
||||
raise ValidationError(
|
||||
@@ -61,7 +67,10 @@ class CLINForm(FlaskForm):
|
||||
coerce=coerce_enum,
|
||||
)
|
||||
|
||||
number = StringField(label=translate("task_orders.form.clin_number_label"))
|
||||
number = StringField(
|
||||
label=translate("task_orders.form.clin_number_label"),
|
||||
validators=[Number(), Length(max=4)],
|
||||
)
|
||||
start_date = DateField(
|
||||
translate("task_orders.form.pop_start"),
|
||||
description=translate("task_orders.form.pop_example"),
|
||||
@@ -116,7 +125,10 @@ class AttachmentForm(BaseForm):
|
||||
filename = HiddenField(
|
||||
id="attachment_filename",
|
||||
validators=[
|
||||
Length(max=100, message=translate("forms.attachment.filename.length_error"))
|
||||
Length(
|
||||
max=100, message=translate("forms.attachment.filename.length_error")
|
||||
),
|
||||
AlphaNumeric(),
|
||||
],
|
||||
)
|
||||
object_name = HiddenField(
|
||||
@@ -124,7 +136,8 @@ class AttachmentForm(BaseForm):
|
||||
validators=[
|
||||
Length(
|
||||
max=40, message=translate("forms.attachment.object_name.length_error")
|
||||
)
|
||||
),
|
||||
AlphaNumeric(),
|
||||
],
|
||||
)
|
||||
accept = ".pdf,application/pdf"
|
||||
@@ -137,6 +150,7 @@ class TaskOrderForm(BaseForm):
|
||||
number = StringField(
|
||||
label=translate("forms.task_order.number_description"),
|
||||
filters=[remove_empty_string],
|
||||
validators=[Number(), Length(max=13)],
|
||||
)
|
||||
pdf = FormField(
|
||||
AttachmentForm,
|
||||
|
@@ -2,7 +2,7 @@ from datetime import datetime
|
||||
import re
|
||||
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from wtforms.validators import ValidationError
|
||||
from wtforms.validators import ValidationError, Regexp
|
||||
import pendulum
|
||||
|
||||
from atst.utils.localization import translate
|
||||
@@ -31,12 +31,13 @@ def DateRange(lower_bound=None, upper_bound=None, message=None):
|
||||
return _date_range
|
||||
|
||||
|
||||
def IsNumber(message=translate("forms.validators.is_number_message")):
|
||||
def Number(message=translate("forms.validators.is_number_message")):
|
||||
def _is_number(form, field):
|
||||
try:
|
||||
int(field.data)
|
||||
except (ValueError, TypeError):
|
||||
raise ValidationError(message)
|
||||
if field.data:
|
||||
try:
|
||||
int(field.data)
|
||||
except (ValueError, TypeError):
|
||||
raise ValidationError(message)
|
||||
|
||||
return _is_number
|
||||
|
||||
@@ -97,3 +98,7 @@ def FileLength(max_length=50000000, message=None):
|
||||
field.data.seek(0)
|
||||
|
||||
return _file_length
|
||||
|
||||
|
||||
def AlphaNumeric(message=translate("forms.validators.alpha_numeric_message")):
|
||||
return Regexp(regex=r"^[A-Za-z0-9\-_ \.]*$", message=message)
|
||||
|
@@ -42,29 +42,11 @@ def root():
|
||||
return render_template("login.html", redirect_url=redirect_url)
|
||||
|
||||
|
||||
@bp.route("/help")
|
||||
@bp.route("/help/<path:doc>")
|
||||
def helpdocs(doc=None):
|
||||
docs = [os.path.splitext(file)[0] for file in os.listdir("templates/help/docs")]
|
||||
if doc:
|
||||
return render_template("help/docs/{}.html".format(doc), docs=docs, doc=doc)
|
||||
else:
|
||||
return render_template("help/index.html", docs=docs, doc=doc)
|
||||
|
||||
|
||||
@bp.route("/home")
|
||||
def home():
|
||||
return render_template("home.html")
|
||||
|
||||
|
||||
@bp.route("/<path:path>")
|
||||
def catch_all(path):
|
||||
try:
|
||||
return render_template("{}.html".format(path))
|
||||
except TemplateNotFound:
|
||||
raise NotFound()
|
||||
|
||||
|
||||
def _client_s_dn():
|
||||
return request.environ.get("HTTP_X_SSL_CLIENT_S_DN")
|
||||
|
||||
|
@@ -26,7 +26,7 @@ def has_portfolio_applications(_user, portfolio=None, **_kwargs):
|
||||
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 for env_role in user_env_roles
|
||||
env_role.environment_id: env_role.role.value for env_role in user_env_roles
|
||||
}
|
||||
|
||||
return render_template(
|
||||
|
@@ -1,9 +1,10 @@
|
||||
from flask import (
|
||||
current_app as app,
|
||||
g,
|
||||
redirect,
|
||||
render_template,
|
||||
request as http_request,
|
||||
url_for,
|
||||
g,
|
||||
)
|
||||
|
||||
from .blueprint import applications_bp
|
||||
@@ -64,9 +65,6 @@ def filter_perm_sets_data(member):
|
||||
"perms_env_mgmt": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_APPLICATION_ENVIRONMENTS)
|
||||
),
|
||||
"perms_del_env": bool(
|
||||
member.has_permission_set(PermissionSets.DELETE_APPLICATION_ENVIRONMENTS)
|
||||
),
|
||||
}
|
||||
|
||||
return perm_sets_data
|
||||
@@ -509,11 +507,7 @@ def resend_invite(application_id, application_role_id):
|
||||
token=new_invite.token,
|
||||
)
|
||||
|
||||
flash(
|
||||
"application_invite_resent",
|
||||
user_name=new_invite.user_name,
|
||||
application_name=app_role.application.name,
|
||||
)
|
||||
flash("application_invite_resent", email=new_invite.email)
|
||||
else:
|
||||
flash(
|
||||
"application_invite_error",
|
||||
@@ -529,3 +523,31 @@ def resend_invite(application_id, application_role_id):
|
||||
_anchor="application-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@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:
|
||||
app.csp.cloud.create_subscription(environment)
|
||||
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",
|
||||
)
|
||||
)
|
||||
|
@@ -19,9 +19,6 @@ from atst.domain.exceptions import UnauthorizedError
|
||||
|
||||
def filter_perm_sets_data(member):
|
||||
perm_sets_data = {
|
||||
"perms_portfolio_mgmt": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||
),
|
||||
"perms_app_mgmt": bool(
|
||||
member.has_permission_set(
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
|
||||
@@ -33,24 +30,43 @@ def filter_perm_sets_data(member):
|
||||
"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, portfolio):
|
||||
def filter_members_data(members_list):
|
||||
members_data = []
|
||||
for member in members_list:
|
||||
members_data.append(
|
||||
{
|
||||
"role_id": member.id,
|
||||
"user_name": member.user_name,
|
||||
"permission_sets": filter_perm_sets_data(member),
|
||||
"status": member.display_status,
|
||||
"ppoc": PermissionSets.PORTFOLIO_POC in member.permission_sets,
|
||||
# add in stuff here for forms
|
||||
}
|
||||
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"])
|
||||
|
||||
@@ -75,7 +91,7 @@ def render_admin_page(portfolio, form=None):
|
||||
"portfolios/admin.html",
|
||||
form=form,
|
||||
portfolio_form=portfolio_form,
|
||||
members=filter_members_data(member_list, portfolio),
|
||||
members=filter_members_data(member_list),
|
||||
new_manager_form=member_forms.NewForm(),
|
||||
assign_ppoc_form=assign_ppoc_form,
|
||||
portfolio=portfolio,
|
||||
@@ -93,26 +109,27 @@ def admin(portfolio_id):
|
||||
return render_admin_page(portfolio)
|
||||
|
||||
|
||||
@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):
|
||||
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",
|
||||
)
|
||||
)
|
||||
# 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"])
|
||||
@@ -166,3 +183,30 @@ def remove_member(portfolio_id, portfolio_role_id):
|
||||
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)
|
||||
|
@@ -54,13 +54,22 @@ def revoke_invitation(portfolio_id, portfolio_token):
|
||||
)
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="resend invitation")
|
||||
def resend_invitation(portfolio_id, portfolio_token):
|
||||
invite = PortfolioInvitations.resend(g.current_user, portfolio_token)
|
||||
send_portfolio_invitation(
|
||||
invitee_email=invite.email,
|
||||
inviter_name=g.current_user.full_name,
|
||||
token=invite.token,
|
||||
)
|
||||
flash("resend_portfolio_invitation", user_name=invite.user_name)
|
||||
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",
|
||||
|
@@ -29,7 +29,7 @@ MESSAGES = {
|
||||
"category": "error",
|
||||
},
|
||||
"application_invite_resent": {
|
||||
"title": "flash.application_invite.resent.title",
|
||||
"title": None,
|
||||
"message": "flash.application_invite.resent.message",
|
||||
"category": "success",
|
||||
},
|
||||
@@ -83,6 +83,16 @@ MESSAGES = {
|
||||
"message": "flash.environment.deleted.message",
|
||||
"category": "success",
|
||||
},
|
||||
"environment_subscription_failure": {
|
||||
"title": "flash.environment.subscription_failure.title",
|
||||
"message": "flash.environment.subscription_failure.message",
|
||||
"category": "error",
|
||||
},
|
||||
"environment_subscription_success": {
|
||||
"title": "flash.environment.subscription_success.title",
|
||||
"message": "flash.environment.subscription_success.message",
|
||||
"category": "success",
|
||||
},
|
||||
"form_errors": {
|
||||
"title": "flash.form.errors.title",
|
||||
"message": "flash.form.errors.message",
|
||||
@@ -90,7 +100,7 @@ MESSAGES = {
|
||||
},
|
||||
"insufficient_funds": {
|
||||
"title": "flash.task_order.insufficient_funds.title",
|
||||
"message": "",
|
||||
"message": None,
|
||||
"category": "warning",
|
||||
},
|
||||
"logged_out": {
|
||||
@@ -109,8 +119,8 @@ MESSAGES = {
|
||||
"category": "success",
|
||||
},
|
||||
"new_portfolio_member": {
|
||||
"title": "flash.success",
|
||||
"message": "flash.new_portfolio_member",
|
||||
"title": "flash.new_portfolio_member.title",
|
||||
"message": "flash.new_portfolio_member.message",
|
||||
"category": "success",
|
||||
},
|
||||
"portfolio_member_removed": {
|
||||
@@ -124,10 +134,15 @@ MESSAGES = {
|
||||
"category": "success",
|
||||
},
|
||||
"resend_portfolio_invitation": {
|
||||
"title": "flash.portfolio_invite.resent.title",
|
||||
"title": None,
|
||||
"message": "flash.portfolio_invite.resent.message",
|
||||
"category": "success",
|
||||
},
|
||||
"resend_portfolio_invitation_error": {
|
||||
"title": "flash.portfolio_invite.error.title",
|
||||
"message": "flash.portfolio_invite.error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"revoked_portfolio_access": {
|
||||
"title": "flash.portfolio_member.revoked.title",
|
||||
"message": "flash.portfolio_member.revoked.message",
|
||||
@@ -153,6 +168,16 @@ MESSAGES = {
|
||||
"message": "flash.task_order.submitted.message",
|
||||
"category": "success",
|
||||
},
|
||||
"update_portfolio_member": {
|
||||
"title": "flash.portfolio_member.update.title",
|
||||
"message": "flash.portfolio_member.update.message",
|
||||
"category": "success",
|
||||
},
|
||||
"update_portfolio_member_error": {
|
||||
"title": "flash.portfolio_member.update_error.title",
|
||||
"message": "flash.portfolio_member.update_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"updated_application_team_settings": {
|
||||
"title": "flash.success",
|
||||
"message": "flash.updated_application_team_settings",
|
||||
|
@@ -4,6 +4,7 @@ from atst.domain.users import Users
|
||||
class SessionLimiter(object):
|
||||
def __init__(self, config, session, redis):
|
||||
self.limit_logins = config["LIMIT_CONCURRENT_SESSIONS"]
|
||||
self.session_prefix = config.get("SESSION_KEY_PREFIX", "session:")
|
||||
self.session = session
|
||||
self.redis = redis
|
||||
|
||||
@@ -16,4 +17,4 @@ class SessionLimiter(object):
|
||||
Users.update_last_session_id(user, session_id)
|
||||
|
||||
def _delete_session(self, session_id):
|
||||
self.redis.delete("session:{}".format(session_id))
|
||||
self.redis.delete(f"{self.session_prefix}{session_id}")
|
||||
|
Reference in New Issue
Block a user