commit
a2d1c470c1
@ -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)
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
@ -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',
|
||||
},
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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 doesn’t 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 %}
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
36
templates/task_orders/edit.html
Normal file
36
templates/task_orders/edit.html
Normal 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 %}
|
@ -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>
|
@ -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 %}
|
@ -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 Provider’s 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 %}
|
@ -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 %}
|
@ -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>
|
@ -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 %}
|
@ -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 %}
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 won’t be included in the final task order.'
|
||||
|
Loading…
x
Reference in New Issue
Block a user