use flask flash for notifications

This commit is contained in:
dandds 2018-11-30 15:53:35 -05:00
parent 6509c5a249
commit a2d6d59ca4
31 changed files with 203 additions and 209 deletions

View File

@ -5,13 +5,14 @@ from flask import current_app as app
from jinja2.exceptions import TemplateNotFound
import pendulum
import os
from werkzeug.exceptions import NotFound
from atst.domain.requests import Requests
from atst.domain.users import Users
from atst.domain.authnid import AuthenticationContext
from atst.domain.audit_log import AuditLog
from atst.domain.auth import logout as _logout
from werkzeug.exceptions import NotFound
from atst.utils.flash import formatted_flash as flash
bp = Blueprint("atst", __name__)
@ -28,10 +29,9 @@ def root():
redirect_url,
"?{}".format(url.urlencode({"next": request.args.get("next")})),
)
flash("login_next")
return render_template(
"login.html", redirect=bool(request.args.get("next")), redirect_url=redirect_url
)
return render_template("login.html", redirect_url=redirect_url)
@bp.route("/help")

View File

@ -9,6 +9,7 @@ from atst.domain.invitations import (
WrongUserError as InvitationWrongUserError,
)
from atst.domain.workspaces import WorkspaceError
from atst.utils.flash import formatted_flash as flash
def log_error(e):
@ -39,7 +40,8 @@ def make_error_pages(app):
# pylint: disable=unused-variable
def session_expired(e):
log_error(e)
url_args = {"sessionExpired": True, "next": request.path}
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("atst.root", **url_args))

View File

@ -13,6 +13,7 @@ from atst.domain.requests import Requests
from atst.domain.exceptions import NotFoundError
from atst.forms.ccpo_review import CCPOReviewForm
from atst.forms.internal_comment import InternalCommentForm
from atst.utils.flash import formatted_flash as flash
def map_ccpo_authorizing(user):
@ -63,6 +64,7 @@ def submit_approval(request_id):
return redirect(url_for("requests.requests_index"))
else:
flash("form_errors")
return render_approval(request, form)
@ -94,4 +96,5 @@ def create_internal_comment(request_id):
url_for("requests.approval", request_id=request_id, _anchor="ccpo-notes")
)
else:
flash("form_errors")
return render_approval(request, internal_comment_form=form)

View File

@ -13,6 +13,7 @@ from atst.domain.requests.financial_verification import (
)
from atst.models.attachment import Attachment
from atst.domain.task_orders import TaskOrders
from atst.utils.flash import formatted_flash as flash
def fv_extended(_http_request):
@ -201,11 +202,13 @@ def financial_verification(request_id):
g.current_user, request, is_extended=is_extended
).execute()
if request.review_comment:
flash("request_review_comment", {"comment": request.review_comment})
return render_template(
"requests/financial_verification.html",
f=form,
jedi_request=request,
review_comment=request.review_comment,
extended=is_extended,
saved_draft=saved_draft,
)
@ -236,11 +239,8 @@ def update_financial_verification(request_id):
if updated_request.task_order.verified:
workspace = Requests.auto_approve_and_create_workspace(updated_request)
return redirect(
url_for(
"workspaces.new_project", workspace_id=workspace.id, newWorkspace=True
)
)
flash("new_workspace")
return redirect(url_for("workspaces.new_project", workspace_id=workspace.id))
else:
return redirect(url_for("requests.requests_index", modal="pendingCCPOApproval"))

View File

@ -5,6 +5,7 @@ from . import requests_bp
from atst.domain.requests import Requests
from atst.models.permissions import Permissions
from atst.forms.data import SERVICE_BRANCHES
from atst.utils.flash import formatted_flash as flash
class RequestsIndex(object):
@ -99,4 +100,8 @@ class RequestsIndex(object):
@requests_bp.route("/requests", methods=["GET"])
def requests_index():
context = RequestsIndex(g.current_user).execute()
if context.get("num_action_required"):
flash("requests_action_required", {"count": context.get("num_action_required")})
return render_template("requests/index.html", **context)

View File

@ -124,6 +124,10 @@ class JEDIRequestFlow(object):
},
]
@property
def is_review_screen(self):
return self.screens[-1] == self.current_screen
def create_or_update_request(self):
request_data = self.map_request_data(self.form_section, self.form.data)
if self.request_id:

View File

@ -13,6 +13,7 @@ from atst.forms.data import (
FUNDING_TYPES,
TASK_ORDER_SOURCES,
)
from atst.utils.flash import formatted_flash as flash
@requests_bp.context_processor
@ -31,6 +32,9 @@ def option_data():
def requests_form_new(screen):
jedi_flow = JEDIRequestFlow(screen, request=None, current_user=g.current_user)
if jedi_flow.is_review_screen and not jedi_flow.can_submit:
flash("request_incomplete")
return render_template(
"requests/screen-%d.html" % int(screen),
f=jedi_flow.form,
@ -54,6 +58,12 @@ def requests_form_update(screen=1, request_id=None):
screen, request=request, request_id=request_id, current_user=g.current_user
)
if jedi_flow.is_review_screen and not jedi_flow.can_submit:
flash("request_incomplete")
if request.review_comment:
flash("request_review_comment", {"comment": request.review_comment})
return render_template(
"requests/screen-%d.html" % int(screen),
f=jedi_flow.form,
@ -63,7 +73,6 @@ def requests_form_update(screen=1, request_id=None):
next_screen=screen + 1,
request_id=request_id,
jedi_request=jedi_flow.request,
review_comment=request.review_comment,
can_submit=jedi_flow.can_submit,
)
@ -103,6 +112,7 @@ def requests_update(screen=1, request_id=None):
where = "/requests"
return redirect(where)
else:
flash("form_errors")
rerender_args = dict(
f=jedi_flow.form,
data=post_data,

View File

@ -1,6 +1,7 @@
from flask import Blueprint, render_template, g, request as http_request, redirect
from atst.forms.edit_user import EditUserForm
from atst.domain.users import Users
from atst.utils.flash import formatted_flash as flash
bp = Blueprint("users", __name__)
@ -10,9 +11,12 @@ bp = Blueprint("users", __name__)
def user():
user = g.current_user
form = EditUserForm(data=user.to_dictionary())
return render_template(
"user/edit.html", next=http_request.args.get("next"), form=form, user=user
)
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)
@bp.route("/user", methods=["POST"])
@ -20,11 +24,12 @@ def update_user():
user = g.current_user
form = EditUserForm(http_request.form)
next_url = http_request.args.get("next")
rerender_args = {"form": form, "user": user, "next": next_url}
if form.validate():
Users.update(user, form.data)
rerender_args["updated"] = True
flash("user_updated")
if next_url:
return redirect(next_url)
else:
flash("form_errors")
return render_template("user/edit.html", **rerender_args)
return render_template("user/edit.html", form=form, user=user, next=next_url)

View File

@ -8,6 +8,7 @@ from atst.domain.workspaces import Workspaces
from atst.forms.workspace import WorkspaceForm
from atst.domain.authz import Authorization
from atst.models.permissions import Permissions
from atst.utils.flash import formatted_flash as flash
@workspaces_bp.route("/workspaces")
@ -33,6 +34,7 @@ def edit_workspace(workspace_id):
url_for("workspaces.workspace_projects", workspace_id=workspace.id)
)
else:
flash("form_errors")
return render_template("workspaces/edit.html", form=form, workspace=workspace)

View File

@ -4,6 +4,7 @@ from . import workspaces_bp
from atst.domain.workspaces import Workspaces
from atst.domain.invitations import Invitations
from atst.queue import queue
from atst.utils.flash import formatted_flash as flash
def send_invite_email(owner_name, token, new_member_email):
@ -40,10 +41,5 @@ def revoke_invitation(workspace_id, token):
def resend_invitation(workspace_id, token):
invite = Invitations.resend(g.current_user, workspace_id, token)
send_invite_email(g.current_user.full_name, invite.token, invite.email)
return redirect(
url_for(
"workspaces.workspace_members",
workspace_id=workspace_id,
resentInvitationTo=invite.user_name,
)
)
flash("resent_workspace_invitation", {"user_name": invite.user_name})
return redirect(url_for("workspaces.workspace_members", workspace_id=workspace_id))

View File

@ -21,12 +21,13 @@ from atst.domain.authz import Authorization
from atst.models.permissions import Permissions
from atst.domain.invitations import Invitations
from atst.utils.flash import formatted_flash as flash
@workspaces_bp.route("/workspaces/<workspace_id>/members")
def workspace_members(workspace_id):
workspace = Workspaces.get_with_members(g.current_user, workspace_id)
new_member_name = http_request.args.get("newMemberName")
resent_invitation_to = http_request.args.get("resentInvitationTo")
new_member = next(
filter(lambda m: m.user_name == new_member_name, workspace.members), None
)
@ -51,7 +52,6 @@ def workspace_members(workspace_id):
status_choices=MEMBER_STATUS_CHOICES,
members=members_list,
new_member=new_member,
resent_invitation_to=resent_invitation_to,
)
@ -76,12 +76,13 @@ def create_member(workspace_id):
invite = Invitations.create(user, new_member, form.data["email"])
send_invite_email(g.current_user.full_name, invite.token, invite.email)
return redirect(
url_for(
"workspaces.workspace_members",
workspace_id=workspace.id,
newMemberName=new_member.user_name,
flash(
"new_workspace_member",
{"new_member": new_member, "workspace": workspace},
)
return redirect(
url_for("workspaces.workspace_members", workspace_id=workspace.id)
)
except AlreadyExistsError:
return render_template(
@ -107,6 +108,10 @@ def view_member(workspace_id, member_id):
form = EditMemberForm(workspace_role=member.role_name)
editable = g.current_user == member.user
can_revoke_access = Workspaces.can_revoke_access_for(workspace, member)
if member.has_dod_id_error:
flash("workspace_member_dod_id_error")
return render_template(
"workspaces/members/edit.html",
workspace=workspace,
@ -155,13 +160,13 @@ def update_member(workspace_id, member_id):
g.current_user, workspace, member, ids_and_roles
)
return redirect(
url_for(
"workspaces.workspace_members",
workspace_id=workspace.id,
memberName=member.user_name,
updatedRole=new_role_name,
flash(
"workspace_role_updated",
{"member_name": member.user_name, "updated_role": new_role_name},
)
return redirect(
url_for("workspaces.workspace_members", workspace_id=workspace.id)
)
else:
return render_template(
@ -177,10 +182,5 @@ def update_member(workspace_id, member_id):
)
def revoke_access(workspace_id, member_id):
revoked_role = Workspaces.revoke_access(g.current_user, workspace_id, member_id)
return redirect(
url_for(
"workspaces.workspace_members",
workspace_id=workspace_id,
revokedMemberName=revoked_role.user_name,
)
)
flash("revoked_workspace_access", {"member_name": revoked_role.user.full_name})
return redirect(url_for("workspaces.workspace_members", workspace_id=workspace_id))

107
atst/utils/flash.py Normal file
View File

@ -0,0 +1,107 @@
from flask import flash, render_template_string
MESSAGES = {
"new_workspace_member": {
"title_template": "Member added successfully",
"message_template": """
<p>{{ new_member.user_name }} was successfully invited via email to this workspace. They do not yet have access to any environments.</p>
<p><a href="{{ url_for('workspaces.update_member', workspace_id=workspace.id, member_id=new_member.user_id) }}">Add environment access.</a></p>
""",
"category": "success",
},
"revoked_workspace_access": {
"title_template": "Removed workspace access",
"message_template": """
<p>Removed {{ member_name }} from this workspace.</p>
""",
"category": "success",
},
"resent_workspace_invitation": {
"title_template": "Invitation resent",
"message_template": """
<p>Successfully sent a new invitation to {{ user_name }}.</p>
""",
"category": "success",
},
"workspace_role_updated": {
"title_template": "Workspace role updated successfully",
"message_template": """
<p>{{ member_name }}'s role was successfully updated to {{ updated_role }}</p>
""",
"category": "success",
},
"session_expired": {
"title_template": "Session Expired",
"message_template": """
Your session expired due to inactivity. Please log in again to continue.
""",
"category": "error",
},
"login_next": {
"title_template": "Log in Required.",
"message_template": """
After you log in, you will be redirected to your destination page.
""",
"category": "warning",
},
"new_workspace": {
"title_template": "Workspace created!",
"message_template": """
<p>You are now ready to create projects and environments within the JEDI Cloud.</p>
""",
"category": "success",
},
"workspace_member_dod_id_error": {
"title_template": "CAC ID Error",
"message_template": """
The member attempted to accept this invite, but their CAC ID did not match the CAC ID you specified on the invite. Please confirm that the DOD ID is accurate.
""",
"category": "error",
},
"form_errors": {
"title_template": "There were some errors",
"message_template": "<p>Please see below.</p>",
"category": "error",
},
"user_must_complete_profile": {
"title_template": "You must complete your profile",
"message_template": "<p>Before continuing, you must complete your profile</p>",
"category": "info",
},
"user_updated": {
"title_template": "User information updated.",
"message_template": "",
"category": "success",
},
"request_incomplete": {
"title_template": "Please complete all sections",
"message_template": """
<p>In order to submit your JEDI Cloud request, you'll need to complete all required sections of this form without error. Missing or invalid fields are noted below.</p>
""",
"category": "error",
},
"requests_action_required": {
"title_template": "Action required on {{ count }} requests.",
"message_template": "",
"category": "info",
},
"request_review_comment": {
"title_template": "Changes Requested",
"message_template": """
<p>CCPO has requested changes to your submission with the following notes:
<br>
{{ comment }}
<br>
Please contact info@jedi.cloud or 123-123-4567 for further discussion.</p>
""",
"category": "warning",
},
}
def formatted_flash(message_name, message_args=None):
config = MESSAGES[message_name]
args = message_args or {}
title = render_template_string(config["title_template"], **args)
message = render_template_string(config["message_template"], **args)
flash({"title": title, "message": message}, config["category"])

View File

@ -1,5 +1,4 @@
{% from "components/text_input.html" import TextInput %}
{% from "components/alert.html" import Alert %}
{% set title_text = 'Edit {} project'.format(project.name) if project else 'Add a new project' %}

View File

@ -0,0 +1,9 @@
{% from "components/alert.html" import Alert %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message_config in messages %}
{{ Alert(message_config["title"], message=message_config.get("message"), level=category) }}
{% endfor %}
{% endif %}
{% endwith %}

View File

@ -1,6 +1,5 @@
{% extends "base_public.html" %}
{% from "components/sidenav_item.html" import SidenavItem %}
{% from "components/alert.html" import Alert %}
{% block title %}Help | JEDI Cloud{% endblock %}

View File

@ -11,29 +11,14 @@
<div class='col'>
<div class='login-banner'>
{% if request.args.get("sessionExpired") %}
{{ Alert('Session Expired',
message='Your session expired due to inactivity. Please log in again to continue.',
level='error'
) }}
{% endif %}
<h1 class="login-banner__heading">Access the JEDI Cloud</h1>
<img class="login-banner__logo" src="{{url_for('static', filename='img/ccpo-logo.svg')}}" alt="Cloud Computing Program Office Logo">
<a class="usa-button usa-button-big login-banner__button" href='{{ redirect_url }}'><span>Sign in with CAC</span></a>
{% if False %}
<a class="usa-button usa-button-big usa-button-secondary" href='{{ url_for("dev.login_dev", **request.args) }}'><span>DEV Login</span></a>
{% endif %}
</div>
{% if redirect %}
{{ Alert('Log in Required.',
message='After you log in, you will be redirected to your destination page.',
level='warning'
) }}
{% endif %}
{% include "fragments/flash.html" %}
{{ Alert('Certificate Selection',
message='When you are prompted to select a certificate, please select <strong>E-mail Certificate</strong> from the provided choices.',

View File

@ -6,9 +6,7 @@
{% include 'requests/menu.html' %}
{% if review_comment %}
{% include 'requests/comment.html' %}
{% endif %}
{% include "fragments/flash.html" %}
{% block form_action %}
{% if request_id %}

View File

@ -1,7 +1,6 @@
{% extends "base.html" %}
{% from "components/icon.html" import Icon %}
{% from "components/alert.html" import Alert %}
{% from "components/text_input.html" import TextInput %}
{% from "components/phone_input.html" import PhoneInput %}
@ -9,12 +8,7 @@
<article class='col col--grow request-approval'>
{% if review_form.errors or internal_comment_form.errors %}
{{ Alert('There were some errors',
message="<p>Please see below.</p>",
level='error'
) }}
{% endif %}
{% include "fragments/flash.html" %}
<section class='panel'>
<header class='panel__heading panel__heading--divider request-approval__heading'>

View File

@ -1,9 +0,0 @@
{% from "components/alert.html" import Alert %}
{% call Alert('Changes Requested', level='warning') %}
<p>CCPO has requested changes to your submission with the following notes:
<br>
{{ review_comment }}
<br>
Please contact info@jedi.cloud or 123-123-4567 for further discussion.</p>
{% endcall %}

View File

@ -9,6 +9,8 @@
{% include 'requests/review_menu.html' %}
{% include "fragments/flash.html" %}
{% if saved_draft %}
{% call Alert('Draft saved', level='success') %}
{% endcall %}
@ -19,10 +21,6 @@
{{ Alert('Pending Financial Verification', fragment="fragments/pending_financial_verification.html") }}
{% endif %}
{% if review_comment %}
{% include 'requests/comment.html' %}
{% endif %}
<financial inline-template v-bind:initial-data='{{ f.data|mixedContentToJson }}'>
<div class="col">
{% if extended %}

View File

@ -1,6 +1,5 @@
{% extends "base.html" %}
{% from "components/alert.html" import Alert %}
{% from "components/modal.html" import Modal %}
{% from "components/empty_state.html" import EmptyState %}
{% from "components/icon.html" import Icon %}
@ -46,13 +45,7 @@
>
<div>
{% if num_action_required %}
{% set title -%}
Action required on {{ num_action_required }} requests.
{%- endset %}
{{ Alert (title)}}
{% endif %}
{% include "fragments/flash.html" %}
{% if not requests %}

View File

@ -1,6 +1,5 @@
{% extends 'requests/_new.html' %}
{% from "components/alert.html" import Alert %}
{% from "components/text_input.html" import TextInput %}
{% from "components/options_input.html" import OptionsInput %}
{% from "components/date_input.html" import DateInput %}
@ -11,12 +10,7 @@
{% block form %}
{% if f.errors %}
{{ Alert('There were some errors',
message="<p>Please see below.</p>",
level='error'
) }}
{% endif %}
{% include "fragments/flash.html" %}
<details-of-use inline-template v-bind:initial-data='{{ f.data|tojson }}'>
<div>

View File

@ -1,6 +1,5 @@
{% extends 'requests/_new.html' %}
{% from "components/alert.html" import Alert %}
{% from "components/text_input.html" import TextInput %}
{% from "components/options_input.html" import OptionsInput %}
{% from "components/date_input.html" import DateInput %}
@ -12,12 +11,7 @@
{% block form %}
{% if f.errors %}
{{ Alert('There were some errors',
message="<p>Please see below.</p>",
level='error'
) }}
{% endif %}
{% include "fragments/flash.html" %}
<p>Please tell us more about you.</p>

View File

@ -1,6 +1,5 @@
{% extends 'requests/_new.html' %}
{% from "components/alert.html" import Alert %}
{% from "components/text_input.html" import TextInput %}
{% from "components/checkbox_input.html" import CheckboxInput %}
@ -10,12 +9,7 @@
{% block form %}
{% if f.errors %}
{{ Alert('There were some errors',
message="<p>Please see below.</p>",
level='error'
) }}
{% endif %}
{% include "fragments/flash.html" %}
<poc inline-template v-bind:initial-data='{{ f.data|tojson }}'>
<div>

View File

@ -4,7 +4,6 @@
{% extends 'requests/_new.html' %}
{% from "components/alert.html" import Alert %}
{% from "components/text_input.html" import TextInput %}
{% from "components/icon.html" import Icon %}
@ -21,12 +20,7 @@
<p>Before you can submit your request, please take a moment to review the information entered in the form. You may make changes by clicking the edit link on each section. When all information looks right, go ahead and submit.</p>
{% if f.errors or not can_submit %}
{{ Alert('Please complete all sections',
message="<p>In order to submit your JEDI Cloud request, you'll need to complete all required sections of this form without error. Missing or invalid fields are noted below.</p>",
level='error'
) }}
{% endif %}
{% include "fragments/flash.html" %}
{% with editable=True %}
{% include "requests/_review.html" %}

View File

@ -1,26 +1,9 @@
{% extends "base.html" %}
{% from "components/alert.html" import Alert %}
{% block content %}
<div class='col'>
{% if next is not none %}
{{ Alert('You must complete your profile',
message='<p>Before continuing, you must complete your profile</p>',
level='info'
) }}
{% endif %}
{% if form.errors %}
{{ Alert('There were some errors',
message="<p>Please see below.</p>",
level='error'
) }}
{% endif %}
{% if updated %}
{{ Alert('User information updated.', level='success') }}
{% endif %}
{% include "fragments/flash.html" %}
<div class='panel'>
<div class='panel__heading'>

View File

@ -1,18 +1,11 @@
{% extends "workspaces/base.html" %}
{% from "components/icon.html" import Icon %}
{% from "components/alert.html" import Alert %}
{% from "components/text_input.html" import TextInput %}
{% block workspace_content %}
{% if form.errors %}
{{ Alert('There were some errors',
message="<p>Please see below.</p>",
level='error'
) }}
{% endif %}
{% include "fragments/flash.html" %}
<form method="POST" action="{{ url_for('workspaces.edit_workspace', workspace_id=workspace.id) }}" autocomplete="false">
{{ form.csrf_token }}

View File

@ -4,14 +4,11 @@
{% from "components/modal.html" import Modal %}
{% from "components/selector.html" import Selector %}
{% from "components/options_input.html" import OptionsInput %}
{% from "components/alert.html" import Alert %}
{% from "components/confirmation_button.html" import ConfirmationButton %}
{% block content %}
{% if member.has_dod_id_error %}
{{ Alert('CAC ID Error', message='The member attempted to accept this invite, but their CAC ID did not match the CAC ID you specified on the invite. Please confirm that the DOD ID is accurate.', level='error') }}
{% endif %}
{% include "fragments/flash.html" %}
<form method="POST" action="{{ url_for('workspaces.update_member', workspace_id=workspace.id, member_id=member.user_id) }}" autocomplete="false">
{{ form.csrf_token }}

View File

@ -1,7 +1,6 @@
{% extends "workspaces/base.html" %}
{% from "components/empty_state.html" import EmptyState %}
{% from "components/alert.html" import Alert %}
{% from "components/icon.html" import Icon %}
{% block workspace_content %}
@ -21,53 +20,7 @@
{% else %}
{% if new_member %}
{% set message -%}
<p>{{ new_member.user_name }} was successfully invited via email to this workspace. They do not yet have access to any environments.</p>
<p><a href="{{ url_for('workspaces.update_member', workspace_id=workspace.id, member_id=new_member.user_id) }}">Add environment access.</a></p>
{%- endset %}
{{ Alert('Member added successfully',
message=message,
level='success'
) }}
{% endif %}
{% if resent_invitation_to %}
{% set message -%}
<p>Successfully sent a new invitation to {{ resent_invitation_to }}.</p>
{%- endset %}
{{ Alert('Invitation resent',
message=message,
level='success'
) }}
{% endif %}
{% if revoked_member_name %}
{% set message -%}
<p>Removed {{ revoked_member_name }} from this workspace.</p>
{%- endset %}
{{ Alert('Removed workspace access',
message=message,
level='success'
) }}
{% endif %}
{% set member_name = request.args.get("memberName") %}
{% set updated_role = request.args.get("updatedRole") %}
{% if updated_role %}
{% set message -%}
<p>{{ member_name }}'s role was successfully updated to {{ updated_role }}</p>
{%- endset %}
{{ Alert('Workspace role updated successfully',
message=message,
level='success'
) }}
{% endif %}
{% include "fragments/flash.html" %}
<members-list
inline-template

View File

@ -1,5 +1,4 @@
{% from "components/icon.html" import Icon %}
{% from "components/alert.html" import Alert %}
{% from "components/empty_state.html" import EmptyState %}
{% extends "workspaces/base.html" %}

View File

@ -8,14 +8,7 @@
{% block workspace_content %}
{% set modalName = "newProjectConfirmation" %}
{% if request.args.get("newWorkspace") %}
{{ Alert('Workspace created!',
message="\
<p>You are now ready to create projects and environments within the JEDI Cloud.</p>
",
level='success'
) }}
{% endif %}
{% include "fragments/flash.html" %}
<new-project inline-template v-bind:initial-data='{{ form.data|tojson }}' modal-name='{{ modalName }}'>
<form method="POST" action="{{ url_for('workspaces.create_project', workspace_id=workspace.id) }}" v-on:submit="handleSubmit">