From 91e41199b7e7be12030535b20ea127297abb28a6 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Sat, 1 Jun 2019 17:02:18 -0400 Subject: [PATCH 1/6] Update TO form based on new model --- atst/domain/task_orders.py | 9 +- atst/forms/task_order.py | 242 +----------- atst/routes/task_orders/new.py | 360 ++---------------- templates/portfolios/task_orders/show.html | 52 --- .../{_new.html => _new-delete-later.html} | 0 templates/task_orders/edit.html | 0 templates/task_orders/new.html | 0 tests/domain/test_invitations.py | 16 +- tests/routes/task_orders/test_new.py | 239 ++---------- tests/routes/test_auth.py | 6 +- tests/test_access.py | 38 +- 11 files changed, 92 insertions(+), 870 deletions(-) rename templates/task_orders/{_new.html => _new-delete-later.html} (100%) create mode 100644 templates/task_orders/edit.html create mode 100644 templates/task_orders/new.html diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 37f8fc74..0a2caeda 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -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) diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 997d71ff..d6a8406e 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -1,131 +1,25 @@ 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.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(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 +45,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"), diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index ad9b2826..4ca8cdc0 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -1,355 +1,59 @@ from copy import deepcopy from flask import ( - request as http_request, - render_template, g, redirect, + render_template, + request as http_request, url_for, - current_app as app, ) 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.flash import formatted_flash as flash from atst.utils.localization import translate -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, - }, -] - - -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") +# TODO: see if this route still exists in new design 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 - ) +@task_orders_bp.route("/portfolios//task_orders/new") +@user_can(Permissions.CREATE_TASK_ORDER, message="view new task order form") +def new(portfolio_id): + return render_template("task_orders/new", form=TaskOrderForm()) -# TODO: /task_orders/new// should not exist -@task_orders_bp.route("/task_orders/new/") -@task_orders_bp.route("/task_orders/new//") -@task_orders_bp.route("/portfolios//task_orders/new/") -@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, - } +@task_orders_bp.route("/portfolios//task_orders/new", methods=["POST"]) +@user_can(Permissions.CREATE_TASK_ORDER, message="create new task order") +def create(portfolio_id): + form_data = http_request.form + form = TaskOrderForm(form_data) - 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 - else: - url_args["portfolio_id"] = portfolio_id - - 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) + if form.validate(): + TaskOrders.create(g.current_user, portfolio_id, **form.data) + # TODO: ask UX where do you go after save + + +@task_orders_bp.route("/portfolios//task_orders//edit") +@user_can(Permissions.CREATE_TASK_ORDER, message="update task order") +def edit(portfolio_id, taks_order_id): + return render_template("task_orders/edit", form=TaskOrderForm()) -# TODO: /task_orders/new// should not exist -@task_orders_bp.route("/task_orders/new/", methods=["POST"]) -@task_orders_bp.route("/task_orders/new//", methods=["POST"]) @task_orders_bp.route( - "/portfolios//task_orders/new/", methods=["POST"] + "/portfolios//task_orders/", 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 - ) +@user_can(Permissions.CREATE_TASK_ORDER, message="update task order") +def update(portfolio_id, task_order_id=None): + form_data = http_request.form + form = TaskOrderForm(form_data) - if workflow.validate(): - workflow.update() - if http_request.args.get("next"): - redirect_url = http_request.args.get("next") - else: - redirect_url = url_for( - "task_orders.new", - screen=screen + 1, - task_order_id=workflow.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, - ) + if form.validate(): + TaskOrders.update(task_order_id, **form.data) + # TODO: ask UX where do you go after save diff --git a/templates/portfolios/task_orders/show.html b/templates/portfolios/task_orders/show.html index 48afe940..7ac320a1 100644 --- a/templates/portfolios/task_orders/show.html +++ b/templates/portfolios/task_orders/show.html @@ -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) %} -
-
-
- {% if complete %} - Completed - {% else %} - Not started - {% endif %} -
-
-
- {{ description }} -
-
-
- {% if not task_order.is_active and button_text and button_url %} - - {{ button_text }} - - {% endif %} -
-
- - {% if caller %} - {{ caller() }} - {% endif %} -
-{% endmacro %} - {% macro DocumentLink(title="", link_url="", description="") %} {% set disabled = not link_url %} diff --git a/templates/portfolios/blank_slate.html b/templates/portfolios/blank_slate.html index 94c19015..62c59aa5 100644 --- a/templates/portfolios/blank_slate.html +++ b/templates/portfolios/blank_slate.html @@ -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 %} - diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index e95a7a7f..a10f35ee 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -93,7 +93,7 @@ {% call StickyCTA(text="Funding") %} {% 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.new', portfolio_id=portfolio.id), icon='cloud', ) }} {% endif %} diff --git a/templates/task_orders/new.html b/templates/task_orders/new.html index e69de29b..41c6e5bc 100644 --- a/templates/task_orders/new.html +++ b/templates/task_orders/new.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% from 'components/save_button.html' import SaveButton %} +{% from 'components/text_input.html' import TextInput %} + +{% block content %} +
+ {% include "fragments/flash.html" %} +
+ {% block portfolio_header %} + {% include "portfolios/header.html" %} + {% endblock %} + +
+ {{ form.csrf_token }} +
+ + Add Funding + {{ SaveButton(text=('common.save' | translate), element='input', form='new-task-order') }} +
+
+ {{ "task_orders.new.form_help_text" | translate }} +
+ {{ TextInput(form.number) }} +
+
+
+
+
+{% endblock %} diff --git a/templates/task_orders/new/app_info.html b/templates/task_orders/new/app_info.html index 04578c83..39f5fbb5 100644 --- a/templates/task_orders/new/app_info.html +++ b/templates/task_orders/new/app_info.html @@ -1,5 +1,3 @@ -{% 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 %} diff --git a/templates/task_orders/new/funding.html b/templates/task_orders/new/funding.html index 3eecf718..6c6ab3fb 100644 --- a/templates/task_orders/new/funding.html +++ b/templates/task_orders/new/funding.html @@ -1,5 +1,3 @@ -{% 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 %} diff --git a/templates/task_orders/new/oversight.html b/templates/task_orders/new/oversight.html index 626ecef0..4c7dc5ac 100644 --- a/templates/task_orders/new/oversight.html +++ b/templates/task_orders/new/oversight.html @@ -1,5 +1,3 @@ -{% 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 %} diff --git a/templates/task_orders/new/review.html b/templates/task_orders/new/review.html index 587adcb5..0819bf39 100644 --- a/templates/task_orders/new/review.html +++ b/templates/task_orders/new/review.html @@ -1,5 +1,3 @@ -{% 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 %} diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py index 57ffcee5..9760f26f 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -33,12 +33,20 @@ def user(): return UserFactory.create() -def test_task_orders_new(): - pass +def test_task_orders_new(client, user_session, portfolio): + user_session(portfolio.owner) + response = client.get(url_for("task_orders.new", portfolio_id=portfolio.id)) + assert response.status_code == 200 -def test_task_orders_create(): - pass +def test_task_orders_create(client, user_session, portfolio): + user_session(portfolio.owner) + response = client.post( + url_for("task_orders.create", portfolio_id=portfolio.id), + data={"number": "0123456789"}, + ) + assert response.status_code == 200 + assert translate("task_orders.form.draft_alert_message") in response.data.decode() def test_task_orders_edit(): diff --git a/translations.yaml b/translations.yaml index 9e5af217..230d9557 100644 --- a/translations.yaml +++ b/translations.yaml @@ -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.' From d7d239d406ae1c19bcddf01532fcfbe90964b5ce Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 3 Jun 2019 11:49:16 -0400 Subject: [PATCH 3/6] Alphabetize flash messages --- atst/utils/flash.py | 184 ++++++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/atst/utils/flash.py b/atst/utils/flash.py index b16dee89..47efa58b 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -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}) }} + {{ "common.undo" | translate }} + """, + "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": "

Please see below.

", + "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": """ -

Task order has been signed successfully

+

{{ "flash.new_application_member" | translate({ "user_name": new_member.user_name }) }}

""", "category": "success", }, - "update_portfolio_members": { - "title_template": "Success!", + "new_portfolio": { + "title_template": "Portfolio created!", "message_template": """ -

You have successfully updated access permissions for members of {{ portfolio.name }}.

+

You are now ready to create applications and environments within the JEDI Cloud.

""", "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": """ -

Portfolio access successfully removed from {{ member_name }}.

+ 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": """ -

Successfully sent a new invitation to {{ user_name }}.

+ {{ "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": """ +

Successfully sent a new invitation to {{ user_name }}.

+ """, + "category": "success", + }, + "revoked_portfolio_access": { + "title_template": "Removed portfolio access", + "message_template": """ +

Portfolio access successfully removed from {{ member_name }}.

+ """, + "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": """ -

You are now ready to create applications and environments within the JEDI Cloud.

- """, - "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": "

Please see below.

", - "category": "error", - }, - "user_must_complete_profile": { - "title_template": "You must complete your profile", - "message_template": "

Before continuing, you must complete your profile

", - "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 }) }} +

Task order has been signed successfully

""", "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}) }} - {{ "common.undo" | translate }} + 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": """ -

{{ "flash.new_application_member" | translate({ "user_name": new_member.user_name }) }}

+

You have successfully updated access permissions for members of {{ portfolio.name }}.

""", "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": "

Before continuing, you must complete your profile

", "category": "info", }, + "user_updated": { + "title_template": "User information updated.", + "message_template": "", + "category": "success", + }, } From 0a2d241dc468304c83edda574440e68bdd2c3c66 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 3 Jun 2019 13:24:37 -0400 Subject: [PATCH 4/6] Add validator to TO number input and add cancel button to form --- atst/routes/task_orders/new.py | 33 ++++-- js/lib/input_validations.js | 144 +++++++++++---------------- templates/task_orders/edit.html | 30 ++++++ templates/task_orders/new.html | 8 +- tests/routes/task_orders/test_new.py | 12 ++- 5 files changed, 132 insertions(+), 95 deletions(-) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 864e6010..428eddd5 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -1,4 +1,4 @@ -from flask import g, render_template, request as http_request +from flask import g, redirect, render_template, request as http_request, url_for from . import task_orders_bp from atst.domain.authz.decorator import user_can_access_decorator as user_can @@ -11,7 +11,14 @@ from atst.utils.flash import formatted_flash as flash @task_orders_bp.route("/portfolios//task_orders/new") @user_can(Permissions.CREATE_TASK_ORDER, message="view new task order form") def new(portfolio_id): - return render_template("task_orders/new.html", form=TaskOrderForm()) + cancel_url = ( + http_request.referrer + if http_request.referrer + else url_for("task_orders.portfolio_funding", portfolio_id=portfolio_id) + ) + return render_template( + "task_orders/new.html", form=TaskOrderForm(), cancel_url=cancel_url + ) @task_orders_bp.route("/portfolios//task_orders/new", methods=["POST"]) @@ -19,22 +26,32 @@ def new(portfolio_id): def create(portfolio_id): form_data = http_request.form form = TaskOrderForm(form_data) - + # todo: add in better error handling for dupe TO numbers if form.validate(): - TaskOrders.create(g.current_user, portfolio_id, **form.data) + task_order = TaskOrders.create(g.current_user, portfolio_id, **form.data) flash("task_order_draft") - return render_template("task_orders/new.html", form=form) + return redirect( + url_for( + "task_orders.edit", + portfolio_id=portfolio_id, + task_order_id=task_order.id, + ) + ) else: flash("form_errors") return render_template("task_orders/new.html", form=form) +# Combine with new route? @task_orders_bp.route("/portfolios//task_orders//edit") @user_can(Permissions.CREATE_TASK_ORDER, message="update task order") -def edit(portfolio_id, taks_order_id): - return render_template("task_orders/edit", form=TaskOrderForm()) +def edit(portfolio_id, task_order_id): + task_order = TaskOrders.get(task_order_id) + form = TaskOrderForm(number=task_order.number) + return render_template("task_orders/edit.html", form=form) +# Combine with create route? @task_orders_bp.route( "/portfolios//task_orders/", methods=["POST"] ) @@ -49,4 +66,4 @@ def update(portfolio_id, task_order_id=None): return render_template("task_orders/new.html", form=form) else: flash("form_errors") - return render_template("task_orders/new.html", form=form) + return render_template("task_orders/edit.html", form=form) diff --git a/js/lib/input_validations.js b/js/lib/input_validations.js index 3a2fcd18..377af19f 100644 --- a/js/lib/input_validations.js +++ b/js/lib/input_validations.js @@ -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', - }, } diff --git a/templates/task_orders/edit.html b/templates/task_orders/edit.html index e69de29b..a856925b 100644 --- a/templates/task_orders/edit.html +++ b/templates/task_orders/edit.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% from 'components/save_button.html' import SaveButton %} +{% from 'components/text_input.html' import TextInput %} + +{% block content %} +
+ {% include "fragments/flash.html" %} +
+ {% block portfolio_header %} + {% include "portfolios/header.html" %} + {% endblock %} + +
+ {{ form.csrf_token }} +
+ + Add Funding + {{ SaveButton(text=('common.save' | translate), element='input', form='new-task-order') }} +
+
+ {{ "task_orders.new.form_help_text" | translate }} +
+ {{ TextInput(form.number, validation='taskOrderNumber') }} +
+
+
+
+
+{% endblock %} diff --git a/templates/task_orders/new.html b/templates/task_orders/new.html index 41c6e5bc..43b96c23 100644 --- a/templates/task_orders/new.html +++ b/templates/task_orders/new.html @@ -16,12 +16,18 @@
Add Funding + + + {{ "common.cancel" | translate }} + {{ SaveButton(text=('common.save' | translate), element='input', form='new-task-order') }}
{{ "task_orders.new.form_help_text" | translate }}
- {{ TextInput(form.number) }} + {{ TextInput(form.number, validation='taskOrderNumber') }}
diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py index 9760f26f..6ac1df4a 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -45,8 +45,18 @@ def test_task_orders_create(client, user_session, portfolio): url_for("task_orders.create", portfolio_id=portfolio.id), data={"number": "0123456789"}, ) + assert response.status_code == 302 + + +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( + url_for("task_orders.create", portfolio_id=portfolio.id), data={"number": ""} + ) assert response.status_code == 200 - assert translate("task_orders.form.draft_alert_message") in response.data.decode() + assert num_task_orders == len(portfolio.task_orders) + assert "There were some errors" in response.data.decode() def test_task_orders_edit(): From b97ae02a936bc40eabee6afca7070e3e99e576d4 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 3 Jun 2019 14:37:02 -0400 Subject: [PATCH 5/6] Combine similar routes and templates, delete unused TO form templates --- atst/routes/task_orders/new.py | 58 +++++------ templates/portfolios/task_orders/index.html | 4 +- templates/task_orders/_new-delete-later.html | 47 --------- templates/task_orders/edit.html | 8 +- templates/task_orders/new.html | 36 ------- templates/task_orders/new/_user_fields.html | 19 ---- templates/task_orders/new/app_info.html | 56 ---------- templates/task_orders/new/funding.html | 66 ------------ templates/task_orders/new/get_started.html | 78 -------------- templates/task_orders/new/menu.html | 22 ---- templates/task_orders/new/oversight.html | 57 ---------- templates/task_orders/new/review.html | 104 ------------------- tests/routes/task_orders/test_new.py | 10 +- tests/test_access.py | 14 +-- 14 files changed, 47 insertions(+), 532 deletions(-) delete mode 100644 templates/task_orders/_new-delete-later.html delete mode 100644 templates/task_orders/new.html delete mode 100644 templates/task_orders/new/_user_fields.html delete mode 100644 templates/task_orders/new/app_info.html delete mode 100644 templates/task_orders/new/funding.html delete mode 100644 templates/task_orders/new/get_started.html delete mode 100644 templates/task_orders/new/menu.html delete mode 100644 templates/task_orders/new/oversight.html delete mode 100644 templates/task_orders/new/review.html diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 428eddd5..1f1277fc 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -9,27 +9,44 @@ from atst.utils.flash import formatted_flash as flash @task_orders_bp.route("/portfolios//task_orders/new") +@task_orders_bp.route("/portfolios//task_orders//edit") @user_can(Permissions.CREATE_TASK_ORDER, message="view new task order form") -def new(portfolio_id): +def edit(portfolio_id, task_order_id=None): + form = None + + if task_order_id: + task_order = TaskOrders.get(task_order_id) + form = TaskOrderForm(number=task_order.number) + else: + form = TaskOrderForm() + cancel_url = ( http_request.referrer if http_request.referrer else url_for("task_orders.portfolio_funding", portfolio_id=portfolio_id) ) - return render_template( - "task_orders/new.html", form=TaskOrderForm(), cancel_url=cancel_url - ) + + return render_template("task_orders/edit.html", form=form, cancel_url=cancel_url) @task_orders_bp.route("/portfolios//task_orders/new", methods=["POST"]) +@task_orders_bp.route( + "/portfolios//task_orders/", methods=["POST"] +) @user_can(Permissions.CREATE_TASK_ORDER, message="create new task order") -def create(portfolio_id): +def update(portfolio_id, task_order_id=None): form_data = http_request.form form = TaskOrderForm(form_data) - # todo: add in better error handling for dupe TO numbers + if form.validate(): - task_order = TaskOrders.create(g.current_user, portfolio_id, **form.data) + task_order = None + if task_order_id: + task_order = TaskOrders.update(task_order_id, **form.data) + else: + task_order = TaskOrders.create(g.current_user, portfolio_id, **form.data) + flash("task_order_draft") + return redirect( url_for( "task_orders.edit", @@ -37,33 +54,6 @@ def create(portfolio_id): task_order_id=task_order.id, ) ) - else: - flash("form_errors") - return render_template("task_orders/new.html", form=form) - - -# Combine with new route? -@task_orders_bp.route("/portfolios//task_orders//edit") -@user_can(Permissions.CREATE_TASK_ORDER, message="update task order") -def edit(portfolio_id, task_order_id): - task_order = TaskOrders.get(task_order_id) - form = TaskOrderForm(number=task_order.number) - return render_template("task_orders/edit.html", form=form) - - -# Combine with create route? -@task_orders_bp.route( - "/portfolios//task_orders/", methods=["POST"] -) -@user_can(Permissions.CREATE_TASK_ORDER, message="update task order") -def update(portfolio_id, task_order_id=None): - form_data = http_request.form - form = TaskOrderForm(form_data) - - if form.validate(): - TaskOrders.update(task_order_id, **form.data) - flash("task_order_draft") - return render_template("task_orders/new.html", form=form) else: flash("form_errors") return render_template("task_orders/edit.html", form=form) diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index a10f35ee..59c0a3f8 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -93,7 +93,7 @@ {% call StickyCTA(text="Funding") %} {% 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', portfolio_id=portfolio.id), + action_href=url_for('task_orders.edit', portfolio_id=portfolio.id), icon='cloud', ) }} {% endif %} diff --git a/templates/task_orders/_new-delete-later.html b/templates/task_orders/_new-delete-later.html deleted file mode 100644 index d4b7c48a..00000000 --- a/templates/task_orders/_new-delete-later.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -
- - {% include 'task_orders/new/menu.html' %} - - {% include "fragments/flash.html" %} - - {% block form_action %} -
- {% endblock %} - -
- -
-

-
Task Order Builder
- {% block heading %}{% endblock %} -

-
- -
- - {{ form.csrf_token }} - {% block form %} - form goes here - {% endblock %} - -
- -
- - {% block next %} - -
- -
- - {% endblock %} - -
- -
- -{% endblock %} diff --git a/templates/task_orders/edit.html b/templates/task_orders/edit.html index a856925b..f67c42ea 100644 --- a/templates/task_orders/edit.html +++ b/templates/task_orders/edit.html @@ -11,11 +11,17 @@ {% include "portfolios/header.html" %} {% endblock %} -
+ {{ form.csrf_token }}
Add Funding + + + {{ "common.cancel" | translate }} + {{ SaveButton(text=('common.save' | translate), element='input', form='new-task-order') }}
diff --git a/templates/task_orders/new.html b/templates/task_orders/new.html deleted file mode 100644 index 43b96c23..00000000 --- a/templates/task_orders/new.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends "base.html" %} - -{% from 'components/save_button.html' import SaveButton %} -{% from 'components/text_input.html' import TextInput %} - -{% block content %} -
- {% include "fragments/flash.html" %} -
- {% block portfolio_header %} - {% include "portfolios/header.html" %} - {% endblock %} - - - {{ form.csrf_token }} -
- - Add Funding - - - {{ "common.cancel" | translate }} - - {{ SaveButton(text=('common.save' | translate), element='input', form='new-task-order') }} -
-
- {{ "task_orders.new.form_help_text" | translate }} -
- {{ TextInput(form.number, validation='taskOrderNumber') }} -
- -
-
-
-{% endblock %} diff --git a/templates/task_orders/new/_user_fields.html b/templates/task_orders/new/_user_fields.html deleted file mode 100644 index 51028005..00000000 --- a/templates/task_orders/new/_user_fields.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
- {{ TextInput(first_name) }} -
- -
- {{ TextInput(last_name) }} -
-
- -
-
- {{ TextInput(email, placeholder='name@mail.mil') }} -
- -
- {{ TextInput(dod_id, placeholder='1234567890') }} -
-
diff --git a/templates/task_orders/new/app_info.html b/templates/task_orders/new/app_info.html deleted file mode 100644 index 39f5fbb5..00000000 --- a/templates/task_orders/new/app_info.html +++ /dev/null @@ -1,56 +0,0 @@ -{% 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 %} - - - -

{{ "forms.task_order.basic_intro" | translate }}

-

{{ "task_orders.new.app_info.basic_info_title"| translate }}

- -{% 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) }} - -
- {% if portfolio %} - {{ ReviewField(heading="forms.task_order.defense_component_label" | translate, field=portfolio.defense_component) }} - {% else %} - {{ OptionsInput(form.defense_component) }} - {% endif %} -
- -
- -

{{ "task_orders.new.app_info.project_title" | translate }}

-

{{ "task_orders.new.app_info.details_description" | translate }}

-{{ OptionsInput(form.app_migration) }} -

{{ "forms.task_order.app_migration.not_sure_help" | translate }}

-{{ OptionsInput(form.native_apps) }} -

{{ "forms.task_order.native_apps.not_sure_help" | translate }}

-{{ MultiCheckboxInput(form.complexity, form.complexity_other) }} - -
- -

{{ "task_orders.new.app_info.team_title" | translate }}

-

{{ "task_orders.new.app_info.subtitle" | translate }}

-{{ MultiCheckboxInput(form.dev_team, form.dev_team_other) }} -{{ OptionsInput(form.team_experience) }} - -
- -

{{ "task_orders.new.app_info.market_research_title" | translate }}

-

{{ "task_orders.new.app_info.market_research_paragraph" | translate | safe }}

- -{% endblock %} diff --git a/templates/task_orders/new/funding.html b/templates/task_orders/new/funding.html deleted file mode 100644 index 6c6ab3fb..00000000 --- a/templates/task_orders/new/funding.html +++ /dev/null @@ -1,66 +0,0 @@ -{% 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 %} - - -
- -

{{ "task_orders.new.funding.subtitle" | translate }}

- -

{{ "task_orders.new.funding.performance_period_title" | translate }}

-

{{ "task_orders.new.funding.performance_period_description" | translate }}

-

{{ "task_orders.new.funding.performance_period_paragraph" | translate }}

- {{ OptionsInput(form.performance_length) }} - -
- -

{{ "task_orders.new.funding.estimate_usage_title" | translate }}

-

{{ "task_orders.new.funding.estimate_usage_description" | translate }}

-

- {{ Icon("link")}} Go to Cloud Service Provider’s estimate calculator -

-

{{ "task_orders.new.funding.estimate_usage_paragraph" | translate }}

- {{ UploadInput(form.csp_estimate, show_label=True) }} - -
- -

{{ "task_orders.new.funding.cloud_calculations_title" | translate }}

-

{{ "task_orders.new.funding.cloud_calculations_paragraph" | translate }}

- -

{{ "task_orders.new.funding.cloud_offerings_title" | translate }}

-

{{ "task_orders.new.funding.cloud_offerings_paragraph" | translate }}

- {{ TextInput(form.clin_01, validation='dollars', placeholder="$0.00") }} - {{ TextInput(form.clin_02, validation='dollars', placeholder="$0.00", disabled=(not config.CLASSIFIED)) }} - -

{{ "task_orders.new.funding.support_assistance_title" | translate }}

-

{{ "task_orders.new.funding.support_assistance_paragraph" | translate }}

- {{ 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)) }} -
-
- -{% endblock %} - -{% block next %} -
-
-

{{ "task_orders.new.funding.total" | translate }}

-
-
- {{ super() }} -
-
-{% endblock %} diff --git a/templates/task_orders/new/get_started.html b/templates/task_orders/new/get_started.html deleted file mode 100644 index 73b00074..00000000 --- a/templates/task_orders/new/get_started.html +++ /dev/null @@ -1,78 +0,0 @@ -{% from "components/icon.html" import Icon %} - - -{% extends "base.html" %} - -{% macro Help(icon_name="", name="", description="", link_text="") %} -
- {{ Icon(icon_name, classes="task-order-help__icon") }} -

{{ name }}

-

{{ description }}

- {{ link_text }} -
-{% endmacro %} - -{% block content %} - -
-
-

{{ "task_orders.new.get_started.title" | translate }}

-
-

- {{ "task_orders.new.get_started.intro" | translate }} -

-

- {{ "task_orders.new.get_started.intro2" | translate }} -

-
- -
    -
  • Statement of work
  • -
  • Market research
  • -
  • Security documentation
  • -
  • Various approvals
  • -
-
-
-

- {{ "task_orders.new.get_started.intro3" | translate }} -

-

- {{ "task_orders.new.get_started.intro4" | translate }} -

-
- -
- -
-

{{ "task_orders.new.get_started.team_header" | translate }}

-
- {{ 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") }} -
-
- - -
-{% endblock %} diff --git a/templates/task_orders/new/menu.html b/templates/task_orders/new/menu.html deleted file mode 100644 index 296c44fd..00000000 --- a/templates/task_orders/new/menu.html +++ /dev/null @@ -1,22 +0,0 @@ -
- -
diff --git a/templates/task_orders/new/oversight.html b/templates/task_orders/new/oversight.html deleted file mode 100644 index 4c7dc5ac..00000000 --- a/templates/task_orders/new/oversight.html +++ /dev/null @@ -1,57 +0,0 @@ -{% 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 %} - - -

{{ "task_orders.new.oversight.ko_info_title" | translate }}

-

{{ "task_orders.new.oversight.ko_info_paragraph" | translate }}

- -
- {{ UserInfo(form.ko_first_name, form.ko_last_name, form.ko_email, form.ko_phone_number) }} - {{ CheckboxInput(form.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")}} - - - -
- -

{{ "task_orders.new.oversight.cor_info_title" | translate }}

-

{{ "task_orders.new.oversight.cor_info_paragraph" | translate }}

- {{ CheckboxInput(form.am_cor, classes="normal") }} - - - -
- {{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_phone_number) }} - {{ CheckboxInput(form.cor_invite) }} - -
-
-
- -
- -

{{ "task_orders.new.oversight.so_info_title" | translate }}

-

{{ "task_orders.new.oversight.so_info_paragraph" | translate }}

- {{ UserInfo(form.so_first_name, form.so_last_name, form.so_email, form.so_phone_number) }} - {{ CheckboxInput(form.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")}} - - -
-
- - -{% endblock %} diff --git a/templates/task_orders/new/review.html b/templates/task_orders/new/review.html deleted file mode 100644 index 0819bf39..00000000 --- a/templates/task_orders/new/review.html +++ /dev/null @@ -1,104 +0,0 @@ -{% 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 %} - -

{{ "task_orders.new.review.app_info"| translate }} {{ EditLink(url_for("task_orders.new", screen=1, task_order_id=task_order.id)) }} -

-{% include "fragments/task_order_review/app_info.html" %} -
- -

{{ "task_orders.new.review.reporting"| translate }} {{ EditLink(url_for("task_orders.new", screen=1, task_order_id=task_order.id, _anchor="reporting")) }}

- -
- {{ - 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 - ) - ) - }} -
- -

{{ "task_orders.new.review.complexity"| translate }}

-{% if task_order.complexity %} -
    - {% for item in task_order.complexity %} -
  • - {{ Icon('ok', classes='icon--gray icon--medium') }}{{ "forms.task_order.complexity.{}".format(item) | translate }}{% if item == 'other' %}: {{ task_order.complexity_other }}{% endif %} -
  • - {% endfor %} -
-{% else %} -

{{ RequiredLabel() }}

-{% endif %} - -
-
-

{{ "task_orders.new.review.team"| translate }}

- {% if task_order.dev_team %} -
    - {% for item in task_order.dev_team %} -
  • - {% 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 %} -
  • - {% endfor %} -
- {% else %} -

{{ RequiredLabel() }}

- {% endif %} -
- - {{ - ReviewField( - ("forms.task_order.team_experience.label" |translate), - ( - ("forms.task_order.team_experience.{}".format(task_order.team_experience) | translate) if task_order.team_experience - ) - ) - }} -
- -
- -

{{ "task_orders.new.review.funding"| translate }} {{ EditLink(url_for("task_orders.new", screen=2, task_order_id=task_order.id)) }}

-{% include "fragments/task_order_review/funding.html" %} -
- -

{{ "task_orders.new.review.oversight"| translate }} {{ EditLink(url_for("task_orders.new", screen=3, task_order_id=task_order.id)) }}

-{% include "fragments/task_order_review/oversight.html" %} -{% endblock %} - -{% block next %} -
- -
-{% endblock %} - -{% block form_action %} - {% if task_order_id %} -
- {% endif %} -{% endblock %} diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py index 6ac1df4a..17904355 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -35,14 +35,14 @@ def user(): def test_task_orders_new(client, user_session, portfolio): user_session(portfolio.owner) - response = client.get(url_for("task_orders.new", portfolio_id=portfolio.id)) + response = client.get(url_for("task_orders.edit", portfolio_id=portfolio.id)) assert response.status_code == 200 def test_task_orders_create(client, user_session, portfolio): user_session(portfolio.owner) response = client.post( - url_for("task_orders.create", portfolio_id=portfolio.id), + url_for("task_orders.update", portfolio_id=portfolio.id), data={"number": "0123456789"}, ) assert response.status_code == 302 @@ -52,7 +52,7 @@ 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( - url_for("task_orders.create", portfolio_id=portfolio.id), data={"number": ""} + url_for("task_orders.update", portfolio_id=portfolio.id), data={"number": ""} ) assert response.status_code == 200 assert num_task_orders == len(portfolio.task_orders) @@ -78,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, ) diff --git a/tests/test_access.py b/tests/test_access.py index 7effd375..a9095a41 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -477,7 +477,7 @@ 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) @@ -485,7 +485,7 @@ def test_task_orders_new_access(get_url_assert_status): rando = user_with() portfolio = PortfolioFactory.create(owner=owner) - url = url_for("task_orders.new", 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) @@ -535,21 +535,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) From 580c6ce3abb45dbc7890ebdc688941a22a30f12f Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 4 Jun 2019 15:51:45 -0400 Subject: [PATCH 6/6] Fix rebase --- tests/test_access.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_access.py b/tests/test_access.py index a9095a41..6c2acc69 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -22,8 +22,9 @@ _NO_ACCESS_CHECK_REQUIRED = _NO_LOGIN_REQUIRED + [ "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.portfolios", # the portfolios list is scoped to the user separately + "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