Merge pull request #861 from dod-ccpo/new-to-form

New TO form
This commit is contained in:
dandds 2019-06-05 14:56:43 -04:00 committed by GitHub
commit a2d1c470c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 323 additions and 1528 deletions

View File

@ -18,8 +18,10 @@ class TaskOrders(BaseDomainClass):
UNCLASSIFIED_FUNDING = []
@classmethod
def create(cls, creator, portfolio):
task_order = TaskOrder(portfolio=portfolio, creator=creator)
def create(cls, creator, portfolio_id, **kwargs):
task_order = TaskOrder(portfolio_id=portfolio_id, creator=creator)
for key, value in kwargs.items():
setattr(task_order, key, value)
db.session.add(task_order)
db.session.commit()
@ -27,7 +29,8 @@ class TaskOrders(BaseDomainClass):
return task_order
@classmethod
def update(cls, task_order, **kwargs):
def update(cls, task_order_id, **kwargs):
task_order = TaskOrders.get(task_order_id)
for key, value in kwargs.items():
setattr(task_order, key, value)

View File

@ -1,131 +1,26 @@
from wtforms.fields import (
BooleanField,
DecimalField,
RadioField,
SelectField,
SelectMultipleField,
StringField,
TextAreaField,
FileField,
)
from wtforms.fields.html5 import DateField, TelField
from wtforms.widgets import ListWidget, CheckboxInput
from wtforms.validators import Email, Length, Required, Optional
from flask_wtf.file import FileAllowed
from atst.forms.validators import IsNumber, PhoneNumber, RequiredIf
from wtforms.fields import BooleanField, DecimalField, StringField
from wtforms.fields.html5 import DateField
from wtforms.validators import Required, Optional
from .forms import BaseForm
from .data import (
SERVICE_BRANCHES,
APP_MIGRATION,
APPLICATION_COMPLEXITY,
DEV_TEAM,
TEAM_EXPERIENCE,
PERIOD_OF_PERFORMANCE_LENGTH,
)
from atst.utils.localization import translate
class AppInfoWithExistingPortfolioForm(BaseForm):
scope = TextAreaField(
translate("forms.task_order.scope_label"),
description=translate("forms.task_order.scope_description"),
)
app_migration = RadioField(
translate("forms.task_order.app_migration.label"),
description=translate("forms.task_order.app_migration.description"),
choices=APP_MIGRATION,
default="",
validators=[Optional()],
)
native_apps = RadioField(
translate("forms.task_order.native_apps.label"),
description=translate("forms.task_order.native_apps.description"),
choices=[("yes", "Yes"), ("no", "No"), ("not_sure", "Not Sure")],
default="",
validators=[Optional()],
)
complexity = SelectMultipleField(
translate("forms.task_order.complexity.label"),
description=translate("forms.task_order.complexity.description"),
choices=APPLICATION_COMPLEXITY,
default=None,
filters=[BaseForm.remove_empty_string],
widget=ListWidget(prefix_label=False),
option_widget=CheckboxInput(),
)
complexity_other = StringField(
translate("forms.task_order.complexity_other_label"),
default=None,
filters=[BaseForm.remove_empty_string],
)
dev_team = SelectMultipleField(
translate("forms.task_order.dev_team.label"),
description=translate("forms.task_order.dev_team.description"),
choices=DEV_TEAM,
default=None,
filters=[BaseForm.remove_empty_string],
widget=ListWidget(prefix_label=False),
option_widget=CheckboxInput(),
)
dev_team_other = StringField(
translate("forms.task_order.dev_team_other_label"),
default=None,
filters=[BaseForm.remove_empty_string],
)
team_experience = RadioField(
translate("forms.task_order.team_experience.label"),
description=translate("forms.task_order.team_experience.description"),
choices=TEAM_EXPERIENCE,
default="",
validators=[Optional()],
)
class AppInfoForm(AppInfoWithExistingPortfolioForm):
portfolio_name = StringField(
translate("forms.task_order.portfolio_name_label"),
description=translate("forms.task_order.portfolio_name_description"),
filters=[BaseForm.remove_empty_string],
validators=[
Required(),
Length(
min=4,
max=100,
message=translate("forms.portfolio.name_length_validation_message"),
),
],
)
defense_component = SelectField(
translate("forms.task_order.defense_component_label"),
choices=SERVICE_BRANCHES,
default="",
filters=[BaseForm.remove_empty_string],
class TaskOrderForm(BaseForm):
number = StringField(
translate("forms.task_order.number_label"),
description=translate("forms.task_order.number_description"),
validators=[Required()],
)
class FundingForm(BaseForm):
performance_length = SelectField(
translate("forms.task_order.performance_length.label"),
choices=PERIOD_OF_PERFORMANCE_LENGTH,
)
start_date = DateField(
translate("forms.task_order.start_date_label"), format="%m/%d/%Y"
)
end_date = DateField(
translate("forms.task_order.end_date_label"), format="%m/%d/%Y"
)
csp_estimate = FileField(
translate("forms.task_order.csp_estimate_label"),
description=translate("forms.task_order.csp_estimate_description"),
validators=[
FileAllowed(
["pdf", "png"], translate("forms.task_order.file_format_not_allowed")
)
],
render_kw={"accept": ".pdf,.png,application/pdf,image/png"},
)
clin_01 = DecimalField(
translate("forms.task_order.clin_01_label"), validators=[Optional()]
)
@ -151,135 +46,7 @@ class UnclassifiedFundingForm(FundingForm):
)
class OversightForm(BaseForm):
ko_first_name = StringField(
translate("forms.task_order.oversight_first_name_label"),
filters=[BaseForm.remove_empty_string],
)
ko_last_name = StringField(
translate("forms.task_order.oversight_last_name_label"),
filters=[BaseForm.remove_empty_string],
)
ko_email = StringField(
translate("forms.task_order.oversight_email_label"),
validators=[Optional(), Email()],
filters=[BaseForm.remove_empty_string],
)
ko_phone_number = TelField(
translate("forms.task_order.oversight_phone_label"),
validators=[Optional(), PhoneNumber()],
filters=[BaseForm.remove_empty_string],
)
ko_dod_id = StringField(
translate("forms.task_order.oversight_dod_id_label"),
filters=[BaseForm.remove_empty_string],
validators=[
RequiredIf(lambda form: form._fields.get("ko_invite").data),
Length(min=10),
IsNumber(),
],
)
am_cor = BooleanField(translate("forms.task_order.oversight_am_cor_label"))
cor_first_name = StringField(
translate("forms.task_order.oversight_first_name_label"),
filters=[BaseForm.remove_empty_string],
)
cor_last_name = StringField(
translate("forms.task_order.oversight_last_name_label"),
filters=[BaseForm.remove_empty_string],
)
cor_email = StringField(
translate("forms.task_order.oversight_email_label"),
filters=[BaseForm.remove_empty_string],
validators=[Optional(), Email()],
)
cor_phone_number = TelField(
translate("forms.task_order.oversight_phone_label"),
filters=[BaseForm.remove_empty_string],
validators=[
RequiredIf(lambda form: not form._fields.get("am_cor").data),
Optional(),
PhoneNumber(),
],
)
cor_dod_id = StringField(
translate("forms.task_order.oversight_dod_id_label"),
filters=[BaseForm.remove_empty_string],
validators=[
RequiredIf(
lambda form: not form._fields.get("am_cor").data
and form._fields.get("cor_invite").data
),
Length(min=10),
IsNumber(),
],
)
so_first_name = StringField(
translate("forms.task_order.oversight_first_name_label"),
filters=[BaseForm.remove_empty_string],
)
so_last_name = StringField(
translate("forms.task_order.oversight_last_name_label"),
filters=[BaseForm.remove_empty_string],
)
so_email = StringField(
translate("forms.task_order.oversight_email_label"),
filters=[BaseForm.remove_empty_string],
validators=[Optional(), Email()],
)
so_phone_number = TelField(
translate("forms.task_order.oversight_phone_label"),
filters=[BaseForm.remove_empty_string],
validators=[Optional(), PhoneNumber()],
)
so_dod_id = StringField(
translate("forms.task_order.oversight_dod_id_label"),
filters=[BaseForm.remove_empty_string],
validators=[
RequiredIf(lambda form: form._fields.get("so_invite").data),
Length(min=10),
IsNumber(),
],
)
ko_invite = BooleanField(
translate("forms.task_order.ko_invite_label"),
description=translate("forms.task_order.skip_invite_description"),
)
cor_invite = BooleanField(
translate("forms.task_order.cor_invite_label"),
description=translate("forms.task_order.skip_invite_description"),
)
so_invite = BooleanField(
translate("forms.task_order.so_invite_label"),
description=translate("forms.task_order.skip_invite_description"),
)
class ReviewForm(BaseForm):
pass
class SignatureForm(BaseForm):
level_of_warrant = DecimalField(
translate("task_orders.sign.level_of_warrant_label"),
description=translate("task_orders.sign.level_of_warrant_description"),
validators=[
RequiredIf(
lambda form: (
form._fields.get("unlimited_level_of_warrant").data is not True
)
)
],
)
unlimited_level_of_warrant = BooleanField(
translate("task_orders.sign.unlimited_level_of_warrant_description"),
validators=[Optional()],
)
signature = BooleanField(
translate("task_orders.sign.digital_signature_label"),
description=translate("task_orders.sign.digital_signature_description"),

View File

@ -1,355 +1,59 @@
from copy import deepcopy
from flask import (
request as http_request,
render_template,
g,
redirect,
url_for,
current_app as app,
)
from flask import g, redirect, render_template, request as http_request, url_for
from . import task_orders_bp
from atst.domain.task_orders import TaskOrders
from atst.domain.portfolios import Portfolios
from atst.utils.flash import formatted_flash as flash
import atst.forms.task_order as task_order_form
from atst.domain.authz.decorator import user_can_access_decorator as user_can
from atst.domain.task_orders import TaskOrders
from atst.forms.task_order import TaskOrderForm
from atst.models.permissions import Permissions
from atst.utils.localization import translate
from atst.utils.flash import formatted_flash as flash
TASK_ORDER_SECTIONS = [
{
"section": "app_info",
"title": translate("forms.task_order.first_step_title"),
"template": "task_orders/new/app_info.html",
"form": task_order_form.AppInfoForm,
},
{
"section": "funding",
"title": "Funding",
"template": "task_orders/new/funding.html",
"form": task_order_form.FundingForm,
"unclassified_form": task_order_form.UnclassifiedFundingForm,
},
{
"section": "oversight",
"title": "Oversight",
"template": "task_orders/new/oversight.html",
"form": task_order_form.OversightForm,
},
{
"section": "review",
"title": "Review",
"template": "task_orders/new/review.html",
"form": task_order_form.ReviewForm,
},
]
@task_orders_bp.route("/portfolios/<portfolio_id>/task_orders/new")
@task_orders_bp.route("/portfolios/<portfolio_id>/task_orders/<task_order_id>/edit")
@user_can(Permissions.CREATE_TASK_ORDER, message="view new task order form")
def edit(portfolio_id, task_order_id=None):
form = None
class ShowTaskOrderWorkflow:
def __init__(self, user, screen=1, task_order_id=None, portfolio_id=None):
self.user = user
self.screen = screen
self.task_order_id = task_order_id
self._task_order = None
self.portfolio_id = portfolio_id
self._portfolio = None
self._section = TASK_ORDER_SECTIONS[screen - 1]
self._form = None
@property
def task_order(self):
if not self._task_order and self.task_order_id:
if self.portfolio_id:
self._task_order = TaskOrders.get(
self.task_order_id, portfolio_id=self.portfolio_id
)
else:
self._task_order = TaskOrders.get(self.task_order_id)
return self._task_order
@property
def portfolio(self):
if not self._portfolio:
if self.task_order:
self._portfolio = self.task_order.portfolio
elif self.portfolio_id:
self._portfolio = Portfolios.get(self.user, self.portfolio_id)
return self._portfolio
@property
def form(self):
form_type = (
"unclassified_form"
if "unclassified_form" in self._section and not app.config.get("CLASSIFIED")
else "form"
)
if self._form:
pass
elif self.task_order:
if self.pf_attributes_read_only and self.screen == 1:
self._form = task_order_form.AppInfoWithExistingPortfolioForm(
obj=self.task_order
)
else:
self._form = self._section[form_type](obj=self.task_order)
# manually set SelectMultipleFields
if self._section["section"] == "app_info":
self._form.complexity.data = self.task_order.complexity
self._form.dev_team.data = self.task_order.dev_team
elif self._section["section"] == "oversight":
if self.user.dod_id == self.task_order.cor_dod_id:
self._form.am_cor.data = True
if self.task_order.contracting_officer or self.task_order.ko_invite:
self._form.ko_invite.data = True
if (
self.task_order.contracting_officer_representative
or self.task_order.cor_invite
):
self._form.cor_invite.data = True
if self.task_order.security_officer or self.task_order.so_invite:
self._form.so_invite.data = True
else:
self._form = self._section[form_type]()
return self._form
@property
def template(self):
return self._section["template"]
@property
def display_screens(self):
screen_info = deepcopy(TASK_ORDER_SECTIONS)
if self.task_order:
for section in screen_info:
section["completion"] = TaskOrders.section_completion_status(
self.task_order, section["section"]
)
return screen_info
@property
def is_complete(self):
if self.task_order and TaskOrders.all_sections_complete(self.task_order):
return True
else:
return False
@property
def pf_attributes_read_only(self):
if self.task_order and self.portfolio.num_task_orders > 1:
return True
elif self.portfolio_id:
return True
else:
return False
class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
def __init__(
self, user, form_data, screen=1, task_order_id=None, portfolio_id=None
):
self.user = user
self.form_data = form_data
self.screen = screen
self.task_order_id = task_order_id
self.portfolio_id = portfolio_id
self._task_order = None
self._section = TASK_ORDER_SECTIONS[screen - 1]
self._form = None
@property
def form(self):
if not self._form:
form_type = (
"unclassified_form"
if "unclassified_form" in self._section
and not app.config.get("CLASSIFIED")
else "form"
)
if self.pf_attributes_read_only and self.screen == 1:
self._form = task_order_form.AppInfoWithExistingPortfolioForm(
self.form_data
)
else:
self._form = self._section[form_type](
self.form_data, obj=self.task_order
)
return self._form
@property
def portfolio(self):
if self.task_order:
return self.task_order.portfolio
@property
def task_order_form_data(self):
to_data = self.form.data.copy()
if "portfolio_name" in to_data:
to_data.pop("portfolio_name")
if "defense_component" in to_data:
to_data.pop("defense_component")
# don't save other text in DB unless "other" is checked
if (
"complexity" in to_data
and bool(to_data["complexity"])
and "other" not in to_data["complexity"]
):
to_data["complexity_other"] = None
if (
"dev_team" in to_data
and bool(to_data["dev_team"])
and "other" not in to_data["dev_team"]
):
to_data["dev_team_other"] = None
if self.form_data.get("am_cor"):
cor_data = {
"cor_first_name": self.user.first_name,
"cor_last_name": self.user.last_name,
"cor_email": self.user.email,
"cor_phone_number": self.user.phone_number,
"cor_dod_id": self.user.dod_id,
"cor_id": self.user.id,
}
to_data = {**to_data, **cor_data}
return to_data
def validate(self):
return self.form.validate()
def update(self):
if self.task_order:
if "portfolio_name" in self.form.data:
new_name = self.form.data["portfolio_name"]
old_name = self.task_order.portfolio_name
if not new_name == old_name:
Portfolios.update(self.task_order.portfolio, {"name": new_name})
TaskOrders.update(self.task_order, **self.task_order_form_data)
else:
if self.portfolio_id:
pf = Portfolios.get(self.user, self.portfolio_id)
else:
pf = Portfolios.create(
user=self.user,
portfolio_attrs={
"name": self.form.portfolio_name.data,
"defense_component": self.form.defense_component.data,
},
)
self._task_order = TaskOrders.create(portfolio=pf, creator=self.user)
TaskOrders.update(self.task_order, **self.task_order_form_data)
return self.task_order
@task_orders_bp.route("/task_orders/new/get_started")
def get_started():
return render_template("task_orders/new/get_started.html") # pragma: no cover
def is_new_task_order(*_args, **kwargs):
return (
"screen" in kwargs
and kwargs["screen"] == 1
and "task_order_id" not in kwargs
and "portfolio_id" not in kwargs
)
# TODO: /task_orders/new/<int:screen>/<task_order_id> should not exist
@task_orders_bp.route("/task_orders/new/<int:screen>")
@task_orders_bp.route("/task_orders/new/<int:screen>/<task_order_id>")
@task_orders_bp.route("/portfolios/<portfolio_id>/task_orders/new/<int:screen>")
@user_can(
Permissions.CREATE_TASK_ORDER,
override=is_new_task_order,
message="view new task order form",
)
def new(screen, task_order_id=None, portfolio_id=None):
workflow = ShowTaskOrderWorkflow(
g.current_user, screen, task_order_id, portfolio_id
)
template_args = {
"current": screen,
"task_order_id": task_order_id,
"screens": workflow.display_screens,
"form": workflow.form,
"complete": workflow.is_complete,
}
if task_order_id and screen is 4:
if not TaskOrders.all_sections_complete(workflow.task_order):
flash("task_order_draft")
if workflow.pf_attributes_read_only:
template_args["portfolio"] = workflow.portfolio
url_args = {"screen": screen}
if task_order_id:
url_args["task_order_id"] = task_order_id
task_order = TaskOrders.get(task_order_id)
form = TaskOrderForm(number=task_order.number)
else:
url_args["portfolio_id"] = portfolio_id
form = TaskOrderForm()
if workflow.task_order:
template_args["task_order"] = workflow.task_order
if http_request.args.get("ko_edit"):
template_args["ko_edit"] = True
template_args["next"] = url_for(
"task_orders.ko_review", task_order_id=task_order_id
)
url_args["next"] = template_args["next"]
template_args["action_url"] = url_for("task_orders.update", **url_args)
return render_template(workflow.template, **template_args)
# TODO: /task_orders/new/<int:screen>/<task_order_id> should not exist
@task_orders_bp.route("/task_orders/new/<int:screen>", methods=["POST"])
@task_orders_bp.route("/task_orders/new/<int:screen>/<task_order_id>", methods=["POST"])
@task_orders_bp.route(
"/portfolios/<portfolio_id>/task_orders/new/<int:screen>", methods=["POST"]
)
@user_can(
Permissions.CREATE_TASK_ORDER,
override=is_new_task_order,
message="update task order",
)
def update(screen, task_order_id=None, portfolio_id=None):
form_data = {**http_request.form, **http_request.files}
workflow = UpdateTaskOrderWorkflow(
g.current_user, form_data, screen, task_order_id, portfolio_id
cancel_url = (
http_request.referrer
if http_request.referrer
else url_for("task_orders.portfolio_funding", portfolio_id=portfolio_id)
)
if workflow.validate():
workflow.update()
if http_request.args.get("next"):
redirect_url = http_request.args.get("next")
return render_template("task_orders/edit.html", form=form, cancel_url=cancel_url)
@task_orders_bp.route("/portfolios/<portfolio_id>/task_orders/new", methods=["POST"])
@task_orders_bp.route(
"/portfolios/<portfolio_id>/task_orders/<task_order_id>", methods=["POST"]
)
@user_can(Permissions.CREATE_TASK_ORDER, message="create new task order")
def update(portfolio_id, task_order_id=None):
form_data = http_request.form
form = TaskOrderForm(form_data)
if form.validate():
task_order = None
if task_order_id:
task_order = TaskOrders.update(task_order_id, **form.data)
else:
redirect_url = url_for(
"task_orders.new",
screen=screen + 1,
task_order_id=workflow.task_order.id,
task_order = TaskOrders.create(g.current_user, portfolio_id, **form.data)
flash("task_order_draft")
return redirect(
url_for(
"task_orders.edit",
portfolio_id=portfolio_id,
task_order_id=task_order.id,
)
return redirect(redirect_url)
else:
return render_template(
workflow.template,
current=screen,
task_order_id=task_order_id,
portfolio_id=portfolio_id,
screens=workflow.display_screens,
form=workflow.form,
)
else:
flash("form_errors")
return render_template("task_orders/edit.html", form=form)

View File

@ -2,19 +2,32 @@ from flask import flash, render_template_string
from atst.utils.localization import translate
MESSAGES = {
"application_environment_members_updated": {
"title_template": "Application environment members updated",
"message_template": "Application environment members have been updated",
"category": "success",
},
"application_deleted": {
"title_template": translate("flash.success"),
"message_template": """
{{ "flash.application.deleted" | translate({"application_name": application_name}) }}
<a href="#">{{ "common.undo" | translate }}</a>
""",
"category": "success",
},
"application_environments_updated": {
"title_template": "Application environments updated",
"message_template": "Application environments have been updated",
"category": "success",
},
"application_member_removed": {
"title_template": "Team member removed from application",
"message_template": "You have successfully deleted {{ user_name }} from {{ application_name }}",
"category": "success",
},
"environment_deleted": {
"title_template": "{{ environment_name }} deleted",
"message_template": 'The environment "{{ environment_name }}" has been deleted',
"category": "success",
},
"application_environment_members_updated": {
"title_template": "Application environment members updated",
"message_template": "Application environment members have been updated",
"environment_access_changed": {
"title_template": "User access successfully changed.",
"message_template": "",
"category": "success",
},
"environment_added": {
@ -24,37 +37,44 @@ MESSAGES = {
""",
"category": "success",
},
"application_environments_updated": {
"title_template": "Application environments updated",
"message_template": "Application environments have been updated",
"environment_deleted": {
"title_template": "{{ environment_name }} deleted",
"message_template": 'The environment "{{ environment_name }}" has been deleted',
"category": "success",
},
"primary_point_of_contact_changed": {
"title_template": translate("flash.new_ppoc_title"),
"message_template": """{{ "flash.new_ppoc_message" | translate({ "ppoc_name": ppoc_name }) }}""",
"category": "success",
"form_errors": {
"title_template": "There were some errors",
"message_template": "<p>Please see below.</p>",
"category": "error",
},
"invitation_resent": {
"title_template": "Invitation resent",
"message_template": "The {{ officer_type }} has been resent instructions to join this portfolio.",
"category": "success",
},
"task_order_draft": {
"title_template": translate("task_orders.form.draft_alert_title"),
"message_template": translate("task_orders.form.draft_alert_message"),
"logged_out": {
"title_template": translate("flash.logged_out"),
"message_template": """
You've been logged out.
""",
"category": "info",
},
"login_next": {
"title_template": translate("flash.login_required_title"),
"message_template": translate("flash.login_required_message"),
"category": "warning",
},
"task_order_signed": {
"title_template": "Task Order Signed",
"new_application_member": {
"title_template": translate("flash.success"),
"message_template": """
<p>Task order has been signed successfully</p>
<p>{{ "flash.new_application_member" | translate({ "user_name": new_member.user_name }) }}</p>
""",
"category": "success",
},
"update_portfolio_members": {
"title_template": "Success!",
"new_portfolio": {
"title_template": "Portfolio created!",
"message_template": """
<p>You have successfully updated access permissions for members of {{ portfolio.name }}.</p>
<p>You are now ready to create applications and environments within the JEDI Cloud.</p>
""",
"category": "success",
},
@ -65,17 +85,17 @@ MESSAGES = {
""",
"category": "success",
},
"revoked_portfolio_access": {
"title_template": "Removed portfolio access",
"portfolio_member_dod_id_error": {
"title_template": "CAC ID Error",
"message_template": """
<p>Portfolio access successfully removed from {{ member_name }}.</p>
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": "success",
"category": "error",
},
"resend_portfolio_invitation": {
"title_template": "Invitation resent",
"portfolio_member_removed": {
"title_template": translate("flash.deleted_member"),
"message_template": """
<p>Successfully sent a new invitation to {{ user_name }}.</p>
{{ "flash.delete_member_success" | translate({ "member_name": member_name }) }}
""",
"category": "success",
},
@ -86,6 +106,25 @@ MESSAGES = {
""",
"category": "success",
},
"primary_point_of_contact_changed": {
"title_template": translate("flash.new_ppoc_title"),
"message_template": """{{ "flash.new_ppoc_message" | translate({ "ppoc_name": ppoc_name }) }}""",
"category": "success",
},
"resend_portfolio_invitation": {
"title_template": "Invitation resent",
"message_template": """
<p>Successfully sent a new invitation to {{ user_name }}.</p>
""",
"category": "success",
},
"revoked_portfolio_access": {
"title_template": "Removed portfolio access",
"message_template": """
<p>Portfolio access successfully removed from {{ member_name }}.</p>
""",
"category": "success",
},
"session_expired": {
"title_template": "Session Expired",
"message_template": """
@ -93,52 +132,6 @@ MESSAGES = {
""",
"category": "error",
},
"login_next": {
"title_template": translate("flash.login_required_title"),
"message_template": translate("flash.login_required_message"),
"category": "warning",
},
"new_portfolio": {
"title_template": "Portfolio created!",
"message_template": """
<p>You are now ready to create applications and environments within the JEDI Cloud.</p>
""",
"category": "success",
},
"portfolio_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",
},
"environment_access_changed": {
"title_template": "User access successfully changed.",
"message_template": "",
"category": "success",
},
"task_order_submitted": {
"title_template": "Task Order Form Submitted",
"message_template": """
Your task order form for {{ task_order.portfolio_name }} has been submitted.
""",
"category": "success",
},
"task_order_congrats": {
"title_template": translate("flash.congrats"),
"message_template": translate("flash.new_portfolio"),
@ -157,6 +150,11 @@ MESSAGES = {
""",
"category": "success",
},
"task_order_draft": {
"title_template": translate("task_orders.form.draft_alert_title"),
"message_template": translate("task_orders.form.draft_alert_message"),
"category": "warning",
},
"task_order_incomplete": {
"title_template": "Task Order Incomplete",
"message_template": """
@ -164,25 +162,24 @@ MESSAGES = {
""",
"category": "error",
},
"portfolio_member_removed": {
"title_template": translate("flash.deleted_member"),
"task_order_signed": {
"title_template": "Task Order Signed",
"message_template": """
{{ "flash.delete_member_success" | translate({ "member_name": member_name }) }}
<p>Task order has been signed successfully</p>
""",
"category": "success",
},
"application_deleted": {
"title_template": translate("flash.success"),
"task_order_submitted": {
"title_template": "Task Order Form Submitted",
"message_template": """
{{ "flash.application.deleted" | translate({"application_name": application_name}) }}
<a href="#">{{ "common.undo" | translate }}</a>
Your task order form for {{ task_order.portfolio_name }} has been submitted.
""",
"category": "success",
},
"new_application_member": {
"title_template": translate("flash.success"),
"update_portfolio_members": {
"title_template": "Success!",
"message_template": """
<p>{{ "flash.new_application_member" | translate({ "user_name": new_member.user_name }) }}</p>
<p>You have successfully updated access permissions for members of {{ portfolio.name }}.</p>
""",
"category": "success",
},
@ -193,13 +190,16 @@ MESSAGES = {
""",
"category": "success",
},
"logged_out": {
"title_template": translate("flash.logged_out"),
"message_template": """
You've been logged out.
""",
"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",
},
}

View File

@ -9,36 +9,6 @@ export default {
unmask: [],
validationError: 'Please enter a response',
},
requiredField: {
mask: false,
match: /.+/,
unmask: [],
validationError: 'This field is required',
},
integer: {
mask: createNumberMask({ prefix: '', allowDecimal: false }),
match: /^[1-9]\d*$/,
unmask: [','],
validationError: 'Please enter a number',
},
dollars: {
mask: createNumberMask({ prefix: '$', allowDecimal: true }),
match: /^-?\d+\.?\d*$/,
unmask: ['$', ','],
validationError: 'Please enter a dollar amount',
},
gigabytes: {
mask: createNumberMask({ prefix: '', suffix: ' GB', allowDecimal: false }),
match: /^[1-9]\d*$/,
unmask: [',', ' GB'],
validationError: 'Please enter an amount of data in gigabytes',
},
email: {
mask: emailMask,
match: /^.+@[^.].*\.[a-z]{2,10}$/,
unmask: [],
validationError: 'Please enter a valid e-mail address',
},
date: {
mask: [/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/],
match: /(0[1-9]|1[012])[- \/.](0[1-9]|[12][0-9]|3[01])[- \/.](19|20)\d\d/,
@ -47,6 +17,65 @@ export default {
keepCharPositions: true,
validationError: 'Please enter a valid date in the form MM/DD/YYYY',
},
dodId: {
mask: createNumberMask({
prefix: '',
allowDecimal: false,
allowLeadingZeroes: true,
includeThousandsSeparator: false,
}),
match: /^\d{10}$/,
unmask: [],
validationError: 'Please enter a 10-digit DoD ID number',
},
dollars: {
mask: createNumberMask({ prefix: '$', allowDecimal: true }),
match: /^-?\d+\.?\d*$/,
unmask: ['$', ','],
validationError: 'Please enter a dollar amount',
},
email: {
mask: emailMask,
match: /^.+@[^.].*\.[a-z]{2,10}$/,
unmask: [],
validationError: 'Please enter a valid e-mail address',
},
integer: {
mask: createNumberMask({ prefix: '', allowDecimal: false }),
match: /^[1-9]\d*$/,
unmask: [','],
validationError: 'Please enter a number',
},
phoneExt: {
mask: createNumberMask({
prefix: '',
allowDecimal: false,
integerLimit: 10,
allowLeadingZeroes: true,
includeThousandsSeparator: false,
}),
match: /^\d{0,10}$/,
unmask: [],
validationError: 'Optional: Please enter up to 10 digits',
},
portfolioName: {
mask: false,
match: /^.{4,100}$/,
unmask: [],
validationError: 'Portfolio names can be between 4-100 characters',
},
requiredField: {
mask: false,
match: /.+/,
unmask: [],
validationError: 'This field is required',
},
taskOrderNumber: {
mask: false,
match: /^.{10}$/,
unmask: [],
validationError: 'TO number must be 10 digits',
},
usPhone: {
mask: [
'(',
@ -68,59 +97,4 @@ export default {
unmask: ['(', ')', '-', ' '],
validationError: 'Please enter a 10-digit phone number',
},
phoneExt: {
mask: createNumberMask({
prefix: '',
allowDecimal: false,
integerLimit: 10,
allowLeadingZeroes: true,
includeThousandsSeparator: false,
}),
match: /^\d{0,10}$/,
unmask: [],
validationError: 'Optional: Please enter up to 10 digits',
},
dodId: {
mask: createNumberMask({
prefix: '',
allowDecimal: false,
allowLeadingZeroes: true,
includeThousandsSeparator: false,
}),
match: /^\d{10}$/,
unmask: [],
validationError: 'Please enter a 10-digit DoD ID number',
},
peNumber: {
mask: false,
match: /(0\d)(0\d)(\d{3})([a-z,A-Z]{1,3})/,
unmask: ['_'],
validationError:
'Please enter a valid PE number. Note that it should be 7 digits followed by 1-3 letters, and should have a zero as the first and third digits.',
},
treasuryCode: {
mask: createNumberMask({
prefix: '',
allowDecimal: false,
allowLeadingZeroes: true,
includeThousandsSeparator: false,
}),
match: /^0*([1-9]{4}|[1-9]{6})$/,
unmask: [],
validationError:
'Please enter a valid Program Treasury Code. Note that it should be a four digit or six digit number, optionally prefixed by one or more zeros.',
},
baCode: {
mask: false,
match: /[0-9]{2}\w?$/,
unmask: [],
validationError:
'Please enter a valid BA Code. Note that it should be two digits, followed by an optional letter.',
},
portfolioName: {
mask: false,
match: /^.{4,100}$/,
unmask: [],
validationError: 'Portfolio names can be between 4-100 characters',
},
}

View File

@ -29,12 +29,6 @@
<li><span class="sidenav__text">You have no portfolios yet</span></li>
{% endif %}
</ul>
<div class="sidenav__divider--small"></div>
<a class="sidenav__link sidenav__link--add" href="{{ url_for("task_orders.get_started") }}" title="Fund a New Portfolio">
<span class="sidenav__link-label">Fund a new portfolio</span>
{{ Icon("plus", classes="sidenav__link-icon") }}
</a>
</div>
</div>
</template>
</div>

View File

@ -6,11 +6,10 @@
{% block content %}
{{
EmptyState(
action_href=url_for("task_orders.get_started"),
action_href="#",
action_label=("portfolios.index.empty.start_button" | translate),
icon="cloud",
message=("portfolios.index.empty.title" | translate),
)
}}
{% endblock %}

View File

@ -93,7 +93,7 @@
{% call StickyCTA(text="Funding") %}
<div class='portfolio-funding__header row'>
<a href="{{ url_for("task_orders.new", screen=1, portfolio_id=portfolio.id) }}" class="usa-button">Start a new task order</a>
<a href="{{ url_for("task_orders.edit", portfolio_id=portfolio.id) }}" class="usa-button">Start a new task order</a>
</div>
{% endcall %}
@ -125,7 +125,7 @@
{{ EmptyState(
'This portfolio doesnt have any active or pending task orders.',
action_label='Add a New Task Order',
action_href=url_for('task_orders.new', screen=1, portfolio_id=portfolio.id),
action_href=url_for('task_orders.edit', portfolio_id=portfolio.id),
icon='cloud',
) }}
{% endif %}

View File

@ -6,48 +6,6 @@
{% block portfolio_content %}
{% macro officer_name(officer) -%}
{%- if not officer -%}
not yet invited
{%- elif officer == g.current_user -%}
you
{%- else -%}
{{ officer.full_name }}
{%- endif -%}
{%- endmacro -%}
{% macro Step(description="", complete=True, button_text=None, button_url=None) %}
<div class="task-order-next-steps__step panel__content">
<div class="row">
<div class="task-order-next-steps__icon col">
{% if complete %}
<span class="label label--success">Completed</span>
{% else %}
<span class="label">Not started</span>
{% endif %}
</div>
<div class="task-order-next-steps__text col col--grow">
<div class="task-order-next-steps__heading row">
<span>{{ description }}</span>
</div>
</div>
<div class="task-order-next-steps__action col">
{% if not task_order.is_active and button_text and button_url %}
<a
href="{{ button_url }}"
class="usa-button usa-button-primary">
{{ button_text }}
</a>
{% endif %}
</div>
</div>
{% if caller %}
{{ caller() }}
{% endif %}
</div>
{% endmacro %}
{% macro DocumentLink(title="", link_url="", description="") %}
{% set disabled = not link_url %}
<div class="task-order-document-link">
@ -99,16 +57,6 @@
<div id="next-steps" class="task-order-next-steps">
<div class="panel">
<h3 class="task-order-next-steps__panel-head panel__content">{{ "task_orders.view.whats_next" | translate }}</h3>
{{
Step(
button_text='Edit',
button_url=url_for("task_orders.new", screen=1, task_order_id=task_order.id),
complete=to_form_complete,
description="task_orders.view.steps.draft" | translate({
"contact": officer_name(task_order.creator)
})| safe,
)
}}
</div>
</div>
<div class="task-order-sidebar col">

View File

@ -1,47 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="col task-order-form">
{% include 'task_orders/new/menu.html' %}
{% include "fragments/flash.html" %}
{% block form_action %}
<form method='POST' action="{{ action_url }}" autocomplete="off" enctype="multipart/form-data">
{% endblock %}
<div class="panel">
<div class="panel__heading">
<h1 class="task-order-form__heading subheading">
<div class="h2">Task Order Builder</div>
{% block heading %}{% endblock %}
</h1>
</div>
<div class="panel__content">
{{ form.csrf_token }}
{% block form %}
form goes here
{% endblock %}
</div>
</div>
{% block next %}
<div class='action-group'>
<input type='submit' class='usa-button usa-button-primary' value='{{ "Save" if ko_edit else "Save & Continue" }}' />
</div>
{% endblock %}
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% from 'components/save_button.html' import SaveButton %}
{% from 'components/text_input.html' import TextInput %}
{% block content %}
<div class="col task-order-form">
{% include "fragments/flash.html" %}
<div class="panel">
{% block portfolio_header %}
{% include "portfolios/header.html" %}
{% endblock %}
<base-form inline-template>
<form id="new-task-order" action='{{ url_for("task_orders.update", portfolio_id=portfolio.id) }}' method="POST" autocomplete="off">
{{ form.csrf_token }}
<div class="panel__content">
<!-- TODO: implement save bar with component -->
<span class="h3">Add Funding</span>
<a
href="{{ cancel_url }}"
class="action-group__action icon-link">
<span class="icon icon--x"></span>
{{ "common.cancel" | translate }}
</a>
{{ SaveButton(text=('common.save' | translate), element='input', form='new-task-order') }}
</div>
<div class="panel__content">
{{ "task_orders.new.form_help_text" | translate }}
<hr>
{{ TextInput(form.number, validation='taskOrderNumber') }}
</div>
</form>
</base-form>
</div>
</div>
{% endblock %}

View File

@ -1,19 +0,0 @@
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(first_name) }}
</div>
<div class='form-col form-col--half'>
{{ TextInput(last_name) }}
</div>
</div>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(email, placeholder='name@mail.mil') }}
</div>
<div class='form-col form-col--half'>
{{ TextInput(dod_id, placeholder='1234567890') }}
</div>
</div>

View File

@ -1,58 +0,0 @@
{% extends 'task_orders/_new.html' %}
{% from "components/text_input.html" import TextInput %}
{% from "components/options_input.html" import OptionsInput %}
{% from "components/date_input.html" import DateInput %}
{% from "components/multi_checkbox_input.html" import MultiCheckboxInput %}
{% from "components/review_field.html" import ReviewField %}
{% block heading %}
{{ "task_orders.new.app_info.section_title"| translate }}
{% endblock %}
{% block form %}
<!-- App Info Section -->
<p>{{ "forms.task_order.basic_intro" | translate }}</p>
<h3 class="task-order-form__heading subheading">{{ "task_orders.new.app_info.basic_info_title"| translate }}</h3>
{% if portfolio %}
{{ ReviewField(heading="forms.portfolio.name_label" | translate, field=portfolio.name) }}
{% else %}
{{ TextInput(form.portfolio_name, placeholder="The name of your office or organization", validation="portfolioName") }}
{% endif %}
{{ TextInput(form.scope, paragraph=True) }}
<div class="subheading--black">
{% if portfolio %}
{{ ReviewField(heading="forms.task_order.defense_component_label" | translate, field=portfolio.defense_component) }}
{% else %}
{{ OptionsInput(form.defense_component) }}
{% endif %}
</div>
<hr>
<h3 id="reporting" class="subheading">{{ "task_orders.new.app_info.project_title" | translate }}</h3>
<p>{{ "task_orders.new.app_info.details_description" | translate }}</p>
{{ OptionsInput(form.app_migration) }}
<p>{{ "forms.task_order.app_migration.not_sure_help" | translate }}</p>
{{ OptionsInput(form.native_apps) }}
<p>{{ "forms.task_order.native_apps.not_sure_help" | translate }}</p>
{{ MultiCheckboxInput(form.complexity, form.complexity_other) }}
<hr>
<h3 class="subheading">{{ "task_orders.new.app_info.team_title" | translate }}</h3>
<p>{{ "task_orders.new.app_info.subtitle" | translate }}</p>
{{ MultiCheckboxInput(form.dev_team, form.dev_team_other) }}
{{ OptionsInput(form.team_experience) }}
<hr>
<h3 class="subheading">{{ "task_orders.new.app_info.market_research_title" | translate }}</h3>
<p>{{ "task_orders.new.app_info.market_research_paragraph" | translate | safe }}</p>
{% endblock %}

View File

@ -1,68 +0,0 @@
{% extends 'task_orders/_new.html' %}
{% from "components/text_input.html" import TextInput %}
{% from "components/options_input.html" import OptionsInput %}
{% from "components/date_input.html" import DateInput %}
{% from "components/upload_input.html" import UploadInput %}
{% from "components/icon.html" import Icon %}
{% block heading %}
{{ "task_orders.new.funding.section_title" | translate }}
{% endblock %}
{% block form %}
<funding
inline-template
v-bind:initial-data='{{ form.data|tojson }}'
v-bind:upload-errors='{{ form.csp_estimate.errors | list }}'
>
<div>
<p>{{ "task_orders.new.funding.subtitle" | translate }}</p>
<!-- Get Funding Section -->
<h3 class="subheading">{{ "task_orders.new.funding.performance_period_title" | translate }}</h3>
<p>{{ "task_orders.new.funding.performance_period_description" | translate }}</p>
<p>{{ "task_orders.new.funding.performance_period_paragraph" | translate }}</p>
{{ OptionsInput(form.performance_length) }}
<hr>
<h3 class="subheading">{{ "task_orders.new.funding.estimate_usage_title" | translate }}</h3>
<p>{{ "task_orders.new.funding.estimate_usage_description" | translate }}</p>
<p><a class="icon-link" target="_blank" href="{{ url_for('atst.jedi_csp_calculator') }}">
{{ Icon("link")}} Go to Cloud Service Providers estimate calculator
</a></p>
<p>{{ "task_orders.new.funding.estimate_usage_paragraph" | translate }}</p>
{{ UploadInput(form.csp_estimate, show_label=True) }}
<hr>
<h3 class="subheading">{{ "task_orders.new.funding.cloud_calculations_title" | translate }}</h3>
<p>{{ "task_orders.new.funding.cloud_calculations_paragraph" | translate }}</p>
<h4 class="task-order-form__heading subheading">{{ "task_orders.new.funding.cloud_offerings_title" | translate }}</h4>
<p>{{ "task_orders.new.funding.cloud_offerings_paragraph" | translate }}</p>
{{ TextInput(form.clin_01, validation='dollars', placeholder="$0.00") }}
{{ TextInput(form.clin_02, validation='dollars', placeholder="$0.00", disabled=(not config.CLASSIFIED)) }}
<h4 class="task-order-form__heading subheading">{{ "task_orders.new.funding.support_assistance_title" | translate }}</h4>
<p>{{ "task_orders.new.funding.support_assistance_paragraph" | translate }}</p>
{{ TextInput(form.clin_03, validation='dollars', tooltip='The cloud support and assistance packages cannot be used as a primary development resource.', placeholder="$0.00") }}
{{ TextInput(form.clin_04, validation='dollars', tooltip='The cloud support and assistance packages cannot be used as a primary development resource.', placeholder="$0.00", disabled=(not config.CLASSIFIED)) }}
</div>
</funding>
{% endblock %}
{% block next %}
<div class="row">
<div class="col col--grow">
<p><strong><span class="task-order-form__heading subheading">{{ "task_orders.new.funding.total" | translate }}</span><br><span id="to-target"></span></strong></p>
</div>
<div class="col col--grow">
{{ super() }}
</div>
</div>
{% endblock %}

View File

@ -1,78 +0,0 @@
{% from "components/icon.html" import Icon %}
{% extends "base.html" %}
{% macro Help(icon_name="", name="", description="", link_text="") %}
<div class="panel__content col">
{{ Icon(icon_name, classes="task-order-help__icon") }}
<h3>{{ name }}</h3>
<p>{{ description }}</p>
<a class="icon-link">{{ link_text }}</a>
</div>
{% endmacro %}
{% block content %}
<div class="col col--grow task-order-get-started">
<div class="panel task-order-get-started__panel">
<h1 class="panel__content">{{ "task_orders.new.get_started.title" | translate }}</h1>
<div class="panel__content">
<p class="centered">
{{ "task_orders.new.get_started.intro" | translate }}
</p>
<p class="centered">
{{ "task_orders.new.get_started.intro2" | translate }}
</p>
</div>
<span class="task-order-get-started__list panel__content">
<ul>
<li>Statement of work</li>
<li>Market research</li>
<li>Security documentation</li>
<li>Various approvals</li>
</ul>
</span>
<div class="panel__content">
<p class="centered">
{{ "task_orders.new.get_started.intro3" | translate }}
</p>
<p class="centered">
{{ "task_orders.new.get_started.intro4" | translate }}
</p>
</div>
</div>
<div class="panel task-order-needs">
<h1 class="panel__content">{{ "task_orders.new.get_started.team_header" | translate }}</h1>
<div class="panel__content task-order-needs__list">
{{ Help(
name="Development Lead",
icon_name="computer",
description="Your development lead will decide and estimate what types of cloud offerings your group needs.",
link_text="Share cloud estimate link") }}
{{ Help(
name="Security Lead",
icon_name="shield",
description="Your security lead will review and approve your security classification needs. They will also review and complete a standardized DD-254.",
link_text="You'll need their DoD ID number") }}
{{ Help(
name="Contracting Officer",
icon_name="dollar-sign",
description="Your contracting officer will review your funding needs and ultimately approve your task order.",
link_text="You'll need their DoD ID number") }}
</div>
</div>
<div class="task-order-get-started__actions">
<div class="task-order-get-started__button-row">
<a href="{{ request.referrer or url_for("atst.home") }}" class="icon-link">
{{ Icon("caret_left") }}
<span>Cancel</span>
</a>
<a href="{{ url_for("task_orders.new", screen=1) }}" class="usa-button usa-button-big">{{ "task_orders.new.get_started.button" | translate }}</a>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,22 +0,0 @@
<div class="progress-menu progress-menu--four">
<ul>
{% for s in screens %}
<li class="progress-menu__item progress-menu__item--{{ s.completion }}">
<a
{% set class='' %}
{% if task_order_id %}
href="{{ url_for('task_orders.new', screen=loop.index, task_order_id=task_order_id) }}"
{% else %}
{% set class="disabled"%}
{% endif %}
{% if g.matchesPath(url_for('task_orders.new', screen=loop.index)) %}
{% set class="active" %}
{% endif %}
class={{class}}
>
{{ s['title'] }}
</a>
</li>
{% endfor %}
</ul>
</div>

View File

@ -1,59 +0,0 @@
{% extends 'task_orders/_new.html' %}
{% from "components/user_info.html" import UserInfo %}
{% from "components/checkbox_input.html" import CheckboxInput %}
{% from "components/text_input.html" import TextInput %}
{% block heading %}
{{ "task_orders.new.oversight.section_title" | translate }}
{% endblock %}
{% block form %}
<!-- Oversight Section -->
<h3 class="subheading">{{ "task_orders.new.oversight.ko_info_title" | translate }}</h3>
<p>{{ "task_orders.new.oversight.ko_info_paragraph" | translate }}</p>
<oversight inline-template v-bind:initial-data='{{ form.data|tojson }}'>
<div class='usa-input'>
{{ UserInfo(form.ko_first_name, form.ko_last_name, form.ko_email, form.ko_phone_number) }}
{{ CheckboxInput(form.ko_invite) }}
<keep-alive>
<dodid v-bind:initial-invite="ko_invite" inline-template v-if="ko_invite">
{{ TextInput(form.ko_dod_id, placeholder="1234567890", tooltip="task_orders.new.oversight.dod_id_tooltip" | translate, tooltip_title='Why', validation='dodId', classes="task-order__invite-officer")}}
</dodid>
</keep-alive>
<hr />
<h3 class="subheading">{{ "task_orders.new.oversight.cor_info_title" | translate }}</h3>
<p>{{ "task_orders.new.oversight.cor_info_paragraph" | translate }}</p>
{{ CheckboxInput(form.am_cor, classes="normal") }}
<keep-alive>
<cordata v-bind:initial-cor-invite="cor_invite" inline-template v-if="!am_cor">
<div>
{{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_phone_number) }}
{{ CheckboxInput(form.cor_invite) }}
<template v-if="cor_invite">
{{ TextInput(form.cor_dod_id, placeholder="1234567890", tooltip="task_orders.new.oversight.dod_id_tooltip" | translate, tooltip_title='Why', validation='dodId', classes="task-order__invite-officer")}}
</template>
</div>
</cordata>
</keep-alive>
<hr />
<h3 class="subheading">{{ "task_orders.new.oversight.so_info_title" | translate }}</h3>
<p>{{ "task_orders.new.oversight.so_info_paragraph" | translate }}</p>
{{ UserInfo(form.so_first_name, form.so_last_name, form.so_email, form.so_phone_number) }}
{{ CheckboxInput(form.so_invite) }}
<keep-alive>
<dodid v-bind:initial-invite="so_invite" inline-template v-if="so_invite">
{{ TextInput(form.so_dod_id, placeholder="1234567890", tooltip="task_orders.new.oversight.dod_id_tooltip" | translate, tooltip_title='Why', validation='dodId', classes="task-order__invite-officer")}}
</dodid>
</keep-alive>
</div>
</oversight>
{% endblock %}

View File

@ -1,106 +0,0 @@
{% extends 'task_orders/_new.html' %}
{% from "components/edit_link.html" import EditLink %}
{% from "components/required_label.html" import RequiredLabel %}
{% from "components/icon.html" import Icon %}
{% from "components/review_field.html" import ReviewField %}
{% block heading %}
{{ "task_orders.new.review.section_title"| translate }}
{% endblock %}
{% block form %}
<h3 class="subheading">{{ "task_orders.new.review.app_info"| translate }} {{ EditLink(url_for("task_orders.new", screen=1, task_order_id=task_order.id)) }}
</h3>
{% include "fragments/task_order_review/app_info.html" %}
<hr>
<h3 class="subheading">{{ "task_orders.new.review.reporting"| translate }} {{ EditLink(url_for("task_orders.new", screen=1, task_order_id=task_order.id, _anchor="reporting")) }}</h3>
<div class="row">
{{
ReviewField(
("forms.task_order.app_migration.label" | translate),
(
("forms.task_order.app_migration.{}".format(task_order.app_migration) | translate) if task_order.app_migration
),
filter='safe'
)
}}
{{
ReviewField(
("forms.task_order.native_apps.label" | translate),
(
("forms.task_order.native_apps.{}".format(task_order.native_apps) | translate) if task_order.native_apps
)
)
}}
</div>
<h4 class='task-order-form__heading'>{{ "task_orders.new.review.complexity"| translate }}</h4>
{% if task_order.complexity %}
<ul class="checklist">
{% for item in task_order.complexity %}
<li>
{{ Icon('ok', classes='icon--gray icon--medium') }}{{ "forms.task_order.complexity.{}".format(item) | translate }}{% if item == 'other' %}: {{ task_order.complexity_other }}{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ RequiredLabel() }}</p>
{% endif %}
<div class="row">
<div class="col col--grow">
<h4 class='task-order-form__heading'>{{ "task_orders.new.review.team"| translate }}</h4>
{% if task_order.dev_team %}
<ul class="checklist">
{% for item in task_order.dev_team %}
<li>
{% if item == 'other' %}
{{ Icon('ok', classes='icon--gray icon--medium') }}Other: {{ task_order.dev_team_other }}
{% else %}
{{ Icon('ok', classes='icon--gray icon--medium') }}{{ "forms.task_order.dev_team.{}".format(item) | translate }}
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ RequiredLabel() }}</p>
{% endif %}
</div>
{{
ReviewField(
("forms.task_order.team_experience.label" |translate),
(
("forms.task_order.team_experience.{}".format(task_order.team_experience) | translate) if task_order.team_experience
)
)
}}
</div>
<hr>
<h3 class="subheading">{{ "task_orders.new.review.funding"| translate }} {{ EditLink(url_for("task_orders.new", screen=2, task_order_id=task_order.id)) }}</h3>
{% include "fragments/task_order_review/funding.html" %}
<hr>
<h3 class="subheading">{{ "task_orders.new.review.oversight"| translate }} {{ EditLink(url_for("task_orders.new", screen=3, task_order_id=task_order.id)) }}</h3>
{% include "fragments/task_order_review/oversight.html" %}
{% endblock %}
{% block next %}
<div class='action-group'>
<input type='submit' class='usa-button usa-button-primary' value='Done'
{% if not complete %}disabled{% endif %}/>
</div>
{% endblock %}
{% block form_action %}
{% if task_order_id %}
<form method='POST' action="{{ url_for('task_orders.invite', task_order_id=task_order_id) }}" autocomplete="off">
{% endif %}
{% endblock %}

View File

@ -2,21 +2,21 @@ import datetime
import pytest
import re
from atst.domain.invitations import (
PortfolioInvitations,
InvitationError,
WrongUserError,
ExpiredError,
NotFoundError,
)
from atst.domain.audit_log import AuditLog
from atst.domain.invitations import (
ExpiredError,
InvitationError,
NotFoundError,
PortfolioInvitations,
WrongUserError,
)
from atst.models import InvitationStatus
from tests.factories import (
PortfolioFactory,
PortfolioInvitationFactory,
PortfolioRoleFactory,
UserFactory,
PortfolioInvitationFactory,
)

View File

@ -1,20 +1,28 @@
import pytest
from flask import url_for
from atst.domain.task_orders import TaskOrders
from atst.domain.permission_sets import PermissionSets
from atst.domain.task_orders import TaskOrders
from atst.models.attachment import Attachment
from atst.routes.task_orders.new import ShowTaskOrderWorkflow, UpdateTaskOrderWorkflow
from atst.utils.localization import translate
from tests.factories import (
UserFactory,
TaskOrderFactory,
PortfolioFactory,
PortfolioRoleFactory,
TaskOrderFactory,
UserFactory,
)
@pytest.fixture
def task_order():
user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user)
attachment = Attachment(filename="sample_attachment", object_name="sample")
return TaskOrderFactory.create(creator=user, portfolio=portfolio)
@pytest.fixture
def portfolio():
return PortfolioFactory.create()
@ -25,143 +33,38 @@ def user():
return UserFactory.create()
class TestShowTaskOrderWorkflow:
def test_portfolio_when_task_order_exists(self):
portfolio = PortfolioFactory.create()
task_order = TaskOrderFactory(portfolio=portfolio)
assert portfolio.num_task_orders > 0
workflow = ShowTaskOrderWorkflow(
user=task_order.creator, task_order_id=task_order.id
)
assert portfolio == workflow.portfolio
def test_portfolio_with_portfolio_id(self):
user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user)
workflow = ShowTaskOrderWorkflow(
user=portfolio.owner, portfolio_id=portfolio.id
)
assert portfolio == workflow.portfolio
def test_new_task_order(client, user_session):
creator = UserFactory.create()
user_session()
response = client.get(url_for("task_orders.new", screen=1))
def test_task_orders_new(client, user_session, portfolio):
user_session(portfolio.owner)
response = client.get(url_for("task_orders.edit", portfolio_id=portfolio.id))
assert response.status_code == 200
def post_to_task_order_step(client, data, screen, task_order_id=None):
return client.post(
url_for("task_orders.update", screen=screen, task_order_id=task_order_id),
data=data,
follow_redirects=False,
)
def slice_data_for_section(task_order_data, section):
attrs = TaskOrders.SECTIONS[section]
return {k: v for k, v in task_order_data.items() if k in attrs}
def serialize_dates(data):
if not data:
return data
dates = {
k: v.strftime("%m/%d/%Y") for k, v in data.items() if hasattr(v, "strftime")
}
data.update(dates)
return data
def test_new_to_can_edit_pf_attributes_screen_1():
portfolio = PortfolioFactory.create()
workflow = ShowTaskOrderWorkflow(user=portfolio.owner)
assert not workflow.pf_attributes_read_only
def test_new_pf_can_edit_pf_attributes_on_back_navigation():
portfolio = PortfolioFactory.create()
pf_task_order = TaskOrderFactory(portfolio=portfolio)
pf_workflow = ShowTaskOrderWorkflow(
user=pf_task_order.creator, task_order_id=pf_task_order.id
)
assert not pf_workflow.pf_attributes_read_only
def test_to_on_pf_cannot_edit_pf_attributes():
portfolio = PortfolioFactory.create()
pf_task_order = TaskOrderFactory(portfolio=portfolio)
workflow = ShowTaskOrderWorkflow(user=portfolio.owner, portfolio_id=portfolio.id)
assert portfolio.num_task_orders == 1
assert workflow.pf_attributes_read_only
second_task_order = TaskOrderFactory(portfolio=portfolio)
second_workflow = ShowTaskOrderWorkflow(
user=portfolio.owner, task_order_id=second_task_order.id
)
assert portfolio.num_task_orders > 1
assert second_workflow.pf_attributes_read_only
@pytest.mark.skip(reason="Reimplement after TO form is updated")
def test_create_new_task_order(client, user_session, pdf_upload):
creator = UserFactory.create()
user_session(creator)
task_order_data = TaskOrderFactory.dictionary()
app_info_data = slice_data_for_section(task_order_data, "app_info")
def test_task_orders_create(client, user_session, portfolio):
user_session(portfolio.owner)
response = client.post(
url_for("task_orders.update", screen=1),
data=app_info_data,
follow_redirects=False,
url_for("task_orders.update", portfolio_id=portfolio.id),
data={"number": "0123456789"},
)
assert url_for("task_orders.new", screen=2) in response.headers["Location"]
assert response.status_code == 302
created_task_order_id = response.headers["Location"].split("/")[-1]
created_task_order = TaskOrders.get(created_task_order_id)
assert created_task_order.portfolio is not None
funding_data = slice_data_for_section(task_order_data, "funding")
funding_data = serialize_dates(funding_data)
def test_task_orders_create_invalid_data(client, user_session, portfolio):
user_session(portfolio.owner)
num_task_orders = len(portfolio.task_orders)
response = client.post(
response.headers["Location"], data=funding_data, follow_redirects=False
url_for("task_orders.update", portfolio_id=portfolio.id), data={"number": ""}
)
assert url_for("task_orders.new", screen=3) in response.headers["Location"]
oversight_data = slice_data_for_section(task_order_data, "oversight")
response = client.post(
response.headers["Location"], data=oversight_data, follow_redirects=False
)
assert url_for("task_orders.new", screen=4) in response.headers["Location"]
assert response.status_code == 200
assert num_task_orders == len(portfolio.task_orders)
assert "There were some errors" in response.data.decode()
def test_create_new_task_order_for_portfolio(client, user_session):
portfolio = PortfolioFactory.create()
creator = portfolio.owner
user_session(creator)
def test_task_orders_edit():
pass
task_order_data = TaskOrderFactory.dictionary()
app_info_data = slice_data_for_section(task_order_data, "app_info")
app_info_data["portfolio_name"] = portfolio.name
response = client.post(
url_for("task_orders.update", screen=1, portfolio_id=portfolio.id),
data=app_info_data,
follow_redirects=False,
)
assert url_for("task_orders.new", screen=2) in response.headers["Location"]
created_task_order_id = response.headers["Location"].split("/")[-1]
created_task_order = TaskOrders.get(created_task_order_id)
assert created_task_order.portfolio_name == portfolio.name
assert created_task_order.portfolio == portfolio
def test_task_orders_update():
pass
@pytest.mark.skip(reason="Update after implementing new TO form")
@ -175,7 +78,9 @@ def test_task_order_form_shows_errors(client, user_session, task_order):
funding_data.update({"clin_01": "one milllllion dollars"})
response = client.post(
url_for("task_orders.update", screen=2, task_order_id=task_order.id),
url_for(
"task_orders.update", portfolio_id=portfolio.id, task_order_id=task_order.id
),
data=funding_data,
follow_redirects=False,
)
@ -186,87 +91,15 @@ def test_task_order_form_shows_errors(client, user_session, task_order):
@pytest.mark.skip(reason="Update after implementing new TO form")
def test_review_screen_when_all_sections_complete(client, user_session, task_order):
user_session(task_order.creator)
response = client.get(
url_for("task_orders.new", screen=4, task_order_id=task_order.id)
)
body = response.data.decode()
assert translate("task_orders.form.draft_alert_title") not in body
assert response.status_code == 200
def test_task_order_review_when_complete(client, user_session, task_order):
pass
@pytest.mark.skip(reason="Update after implementing new TO form")
def test_review_screen_when_not_all_sections_complete(client, user_session, task_order):
TaskOrders.update(task_order, clin_01=None)
user_session(task_order.creator)
response = client.get(
url_for("task_orders.new", screen=4, task_order_id=task_order.id)
)
body = response.data.decode()
assert translate("task_orders.form.draft_alert_title") in body
assert response.status_code == 200
@pytest.fixture
def task_order():
user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user)
attachment = Attachment(filename="sample_attachment", object_name="sample")
return TaskOrderFactory.create(creator=user, portfolio=portfolio)
def test_show_task_order(task_order):
workflow = ShowTaskOrderWorkflow(task_order.creator)
assert workflow.task_order is None
another_workflow = ShowTaskOrderWorkflow(
task_order.creator, task_order_id=task_order.id
)
assert another_workflow.task_order == task_order
def test_show_task_order_display_screen(task_order):
workflow = ShowTaskOrderWorkflow(task_order.creator, task_order_id=task_order.id)
screens = workflow.display_screens
# every form section is complete
for i in range(2):
assert screens[i]["completion"] == "complete"
# the review section is not
assert screens[3]["completion"] == "incomplete"
def test_task_order_review_when_not_complete(client, user_session, task_order):
pass
@pytest.mark.skip(reason="Update after implementing new TO form")
def test_update_task_order_with_existing_task_order(task_order):
to_data = serialize_dates(TaskOrderFactory.dictionary())
workflow = UpdateTaskOrderWorkflow(
task_order.creator, to_data, screen=2, task_order_id=task_order.id
)
assert workflow.task_order.start_date != to_data["start_date"]
workflow.update()
assert workflow.task_order.start_date.strftime("%m/%d/%Y") == to_data["start_date"]
@pytest.mark.skip(reason="Update after implementing new TO form")
def test_review_task_order_form(client, user_session, task_order):
user_session(task_order.creator)
for idx, section in enumerate(TaskOrders.SECTIONS):
response = client.get(
url_for("task_orders.new", screen=idx + 1, task_order_id=task_order.id)
)
assert response.status_code == 200
@pytest.mark.skip(reason="Reimplement after TO form is updated")
def test_mo_redirected_to_build_page(client, user_session, portfolio):
user_session(portfolio.owner)
task_order = TaskOrderFactory.create(portfolio=portfolio)
response = client.get(
url_for("task_orders.new", screen=1, task_order_id=task_order.id)
)
assert response.status_code == 200
def test_task_order_review_and_sign(client, user_session, task_order):
pass

View File

@ -3,11 +3,11 @@ from urllib.parse import quote
from tests.factories import UserFactory
PROTECTED_URL = "/task_orders/new/get_started"
# TODO: update w/ new home url
PROTECTED_URL = "/home"
def test_request_page_with_complete_profile(client, user_session):
def test_home_page_with_complete_profile(client, user_session):
user = UserFactory.create()
user_session(user)
response = client.get(PROTECTED_URL, follow_redirects=False)

View File

@ -13,21 +13,21 @@ from atst.models import CSPRole, PortfolioRoleStatus, ApplicationRoleStatus
from tests.factories import *
_NO_ACCESS_CHECK_REQUIRED = _NO_LOGIN_REQUIRED + [
"task_orders.get_started", # all users can start a new TO
"atst.csp_environment_access", # internal redirect
"atst.jedi_csp_calculator", # internal redirect
"atst.styleguide", # dev reference
"dev.test_email", # dev tool
"dev.messages", # dev tool
"atst.home", # available to all users
"users.user", # available to all users
"users.update_user", # available to all users
"portfolios.accept_invitation", # available to all users; access control is built into invitation logic
"applications.accept_invitation", # available to all users; access control is built into invitation logic
"atst.catch_all", # available to all users
"portfolios.portfolios", # the portfolios list is scoped to the user separately
"portfolios.new_portfolio", # all users can create a portfolio
"atst.csp_environment_access", # internal redirect
"atst.home", # available to all users
"atst.jedi_csp_calculator", # internal redirect
"atst.styleguide", # dev reference
"dev.messages", # dev tool
"dev.test_email", # dev tool
"portfolios.accept_invitation", # available to all users; access control is built into invitation logic
"portfolios.create_portfolio", # create a portfolio
"portfolios.new_portfolio", # all users can create a portfolio
"portfolios.portfolios", # the portfolios list is scoped to the user separately
"task_orders.get_started", # all users can start a new TO
"users.update_user", # available to all users
"users.user", # available to all users
]
@ -478,26 +478,15 @@ def test_task_orders_download_task_order_pdf_access(get_url_assert_status, monke
get_url_assert_status(rando, url, 404)
# task_orders.new
# task_orders.edit
@pytest.mark.skip(reason="Update after new TO form implemented")
def test_task_orders_new_access(get_url_assert_status):
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_FUNDING)
owner = user_with()
rando = user_with()
url = url_for("task_orders.new", screen=1)
get_url_assert_status(owner, url, 200)
get_url_assert_status(ccpo, url, 200)
get_url_assert_status(rando, url, 200)
portfolio = PortfolioFactory.create(owner=owner)
task_order = TaskOrderFactory.create(portfolio=portfolio)
url = url_for("task_orders.new", screen=2, task_order_id=task_order.id)
get_url_assert_status(owner, url, 200)
get_url_assert_status(ccpo, url, 200)
get_url_assert_status(rando, url, 404)
url = url_for("task_orders.new", screen=1, portfolio_id=portfolio.id)
url = url_for("task_orders.edit", portfolio_id=portfolio.id)
get_url_assert_status(owner, url, 200)
get_url_assert_status(ccpo, url, 200)
get_url_assert_status(rando, url, 404)
@ -547,21 +536,23 @@ def test_task_orders_update_access(post_url_assert_status):
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_FUNDING)
owner = user_with()
rando = user_with()
portfolio = PortfolioFactory.create(owner=owner)
url = url_for("task_orders.update", screen=1)
url = url_for("task_orders.update", portfolio_id=portfolio.id)
post_url_assert_status(owner, url, 200)
post_url_assert_status(ccpo, url, 200)
post_url_assert_status(rando, url, 200)
portfolio = PortfolioFactory.create(owner=owner)
task_order = TaskOrderFactory.create(portfolio=portfolio)
url = url_for("task_orders.update", screen=2, task_order_id=task_order.id)
url = url_for(
"task_orders.update", portfolio_id=portfolio.id, task_order_id=task_order.id
)
post_url_assert_status(owner, url, 302)
post_url_assert_status(ccpo, url, 302)
post_url_assert_status(rando, url, 404)
url = url_for("task_orders.update", screen=1, portfolio_id=portfolio.id)
url = url_for("task_orders.update", portfolio_id=portfolio.id)
post_url_assert_status(owner, url, 302)
post_url_assert_status(ccpo, url, 302)
post_url_assert_status(rando, url, 404)

View File

@ -298,6 +298,8 @@ forms:
not_sure: 'Not sure, unsure if planning to develop natively in the cloud'
'yes': 'Yes, planning to develop natively in the cloud'
not_sure_help: Not sure? Talk to your technical lead about where and how they plan on developing your application.
number_description: Task order number (10 digit number from your system of record)
number_label: Add your task order
oversight_am_cor_label: I am the Contracting Officer Representative (COR) for this task order
oversight_dod_id_label: DoD ID
oversight_email_label: Email
@ -619,6 +621,7 @@ task_orders:
task_order_information: Task order information
title: Verify task order information
new:
form_help_text: Before you can begin work in the cloud, you'll need to complete the information below and upload your approved task order for reference by the CCPO.
app_info:
basic_info_title: Basic information
details_description: 'Provide a few more details about the work you will be doing. The CCPO will use this section for reporting purposes, but it wont be included in the final task order.'