diff --git a/alembic/versions/3d3c71b03e98_nullable_fields_for_clins.py b/alembic/versions/3d3c71b03e98_nullable_fields_for_clins.py new file mode 100644 index 00000000..dfd0702f --- /dev/null +++ b/alembic/versions/3d3c71b03e98_nullable_fields_for_clins.py @@ -0,0 +1,43 @@ +"""Nullable fields for CLINs + + +Revision ID: 3d3c71b03e98 +Revises: b565531a1e82 +Create Date: 2019-06-17 11:04:03.294913 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '3d3c71b03e98' +down_revision = 'b565531a1e82' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('clins', 'end_date', + existing_type=sa.DATE(), + nullable=True) + op.alter_column('clins', 'jedi_clin_type', + existing_type=sa.VARCHAR(length=11), + nullable=True) + op.alter_column('clins', 'loas', + existing_type=postgresql.ARRAY(sa.VARCHAR()), + nullable=True, + existing_server_default=sa.text("'{}'::character varying[]")) + op.alter_column('clins', 'number', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('clins', 'obligated_amount', + existing_type=sa.NUMERIC(), + nullable=True) + op.alter_column('clins', 'start_date', + existing_type=sa.DATE(), + nullable=True) + # ### end Alembic commands ### + + diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 873551bf..fb902524 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -7,7 +7,7 @@ from wtforms.fields import ( StringField, ) from wtforms.fields.html5 import DateField -from wtforms.validators import Required +from wtforms.validators import Required, Optional from flask_wtf.file import FileAllowed from flask_wtf import FlaskForm @@ -30,18 +30,20 @@ class CLINForm(FlaskForm): "CLIN type", choices=JEDI_CLIN_TYPES, coerce=coerce_enum ) - number = StringField(label="CLIN", validators=[Required()]) + number = StringField(label="CLIN", validators=[Optional()]) start_date = DateField( translate("forms.task_order.start_date_label"), format="%m/%d/%Y", - validators=[Required()], + validators=[Optional()], ) end_date = DateField( translate("forms.task_order.end_date_label"), format="%m/%d/%Y", - validators=[Required()], + validators=[Optional()], + ) + obligated_amount = DecimalField( + label="Funds obligated for cloud", validators=[Optional()] ) - obligated_amount = DecimalField(label="Funds obligated for cloud") loas = FieldList(StringField()) diff --git a/atst/models/clin.py b/atst/models/clin.py index 2bba6a3c..6c884811 100644 --- a/atst/models/clin.py +++ b/atst/models/clin.py @@ -21,12 +21,12 @@ class CLIN(Base, mixins.TimestampsMixin): task_order_id = Column(ForeignKey("task_orders.id"), nullable=False) task_order = relationship("TaskOrder") - number = Column(String, nullable=False) - loas = Column(ARRAY(String), server_default="{}", nullable=False) - start_date = Column(Date, nullable=False) - end_date = Column(Date, nullable=False) - obligated_amount = Column(Numeric(scale=2), nullable=False) - jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False) + number = Column(String, nullable=True) + loas = Column(ARRAY(String), server_default="{}", nullable=True) + start_date = Column(Date, nullable=True) + end_date = Column(Date, nullable=True) + obligated_amount = Column(Numeric(scale=2), nullable=True) + jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=True) # # NOTE: For now obligated CLINS are CLIN 1 + CLIN 3 diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 9c8a5569..3b0ab850 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -86,11 +86,11 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def has_begun(self): - return Clock.today() >= self.start_date + return self.start_date is not None and Clock.today() >= self.start_date @property def has_ended(self): - return Clock.today() >= self.end_date + return self.start_date is not None and Clock.today() >= self.end_date @property def is_completed(self): @@ -133,7 +133,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): def total_obligated_funds(self): total = 0 for clin in self.clins: - if clin.jedi_clin_type in [ + if clin.obligated_amount is not None and clin.jedi_clin_type in [ JEDICLINType.JEDI_CLIN_1, JEDICLINType.JEDI_CLIN_3, ]: @@ -144,7 +144,8 @@ class TaskOrder(Base, mixins.TimestampsMixin): def total_contract_amount(self): total = 0 for clin in self.clins: - total += clin.obligated_amount + if clin.obligated_amount is not None: + total += clin.obligated_amount return total @property diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index ffaf1b8c..4f084748 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -14,12 +14,15 @@ from atst.utils.flash import formatted_flash as flash @user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="review task order details") def review_task_order(task_order_id): task_order = TaskOrders.get(task_order_id) - signature_form = SignatureForm() - return render_template( - "portfolios/task_orders/review.html", - task_order=task_order, - signature_form=signature_form, - ) + if task_order.is_draft: + return redirect(url_for("task_orders.edit", task_order_id=task_order.id)) + else: + signature_form = SignatureForm() + return render_template( + "portfolios/task_orders/review.html", + task_order=task_order, + signature_form=signature_form, + ) @task_orders_bp.route("/task_orders//submit", methods=["POST"]) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 1c1bec1d..fbc9afca 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -57,13 +57,14 @@ def update(portfolio_id=None, task_order_id=None): else: task_order = TaskOrders.create(g.current_user, portfolio_id, **form.data) - if http_request.args.get("review"): + if task_order.is_completed and http_request.args.get("review"): return redirect( url_for("task_orders.review_task_order", task_order_id=task_order.id) ) flash("task_order_draft") - return redirect(url_for("task_orders.edit", task_order_id=task_order.id)) - else: - return render_task_orders_edit(portfolio_id, task_order_id, form), 400 + if task_order.is_completed: + return redirect(url_for("task_orders.edit", task_order_id=task_order.id)) + + return render_task_orders_edit(portfolio_id, task_order_id, form), 400 diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 4829bfd2..c737a9e8 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -13,7 +13,7 @@ {% endmacro %} {% macro TaskOrderEditButton(task_order, text="Edit", secondary=False) %} - + {{ text }} {% endmacro %} diff --git a/templates/task_orders/edit.html b/templates/task_orders/edit.html index b2c9137a..6a7186bc 100644 --- a/templates/task_orders/edit.html +++ b/templates/task_orders/edit.html @@ -121,6 +121,9 @@ type="submit" formaction="{{ review_action }}" tabindex="0" + {% if task_order and task_order.is_draft %} + disabled="disabled" + {% endif %} :disabled="invalid" value="Review task order" form="new-task-order" diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index 3d6d5706..51225892 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -33,7 +33,8 @@ def task_order(): return TaskOrderFactory.create(creator=user, portfolio=portfolio) -def test_review_task_order(client, user_session, task_order): +def test_review_task_order_not_draft(client, user_session, task_order): + TaskOrders.sign(task_order=task_order, signer_dod_id=random_dod_id()) user_session(task_order.portfolio.owner) response = client.get( url_for("task_orders.review_task_order", task_order_id=task_order.id) @@ -41,6 +42,18 @@ def test_review_task_order(client, user_session, task_order): assert response.status_code == 200 +def test_review_task_order_draft(client, user_session, task_order): + TaskOrders.update( + task_order_id=task_order.id, number="1234567890", clins=[], pdf=None + ) + user_session(task_order.portfolio.owner) + response = client.get( + url_for("task_orders.review_task_order", task_order_id=task_order.id) + ) + assert response.status_code == 302 + assert url_for("task_orders.edit", task_order_id=task_order.id) in response.location + + def test_submit_task_order(client, user_session, task_order): user_session(task_order.portfolio.owner) response = client.post( diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py index 7dbad2c4..abe9ca6d 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -108,8 +108,8 @@ def test_task_orders_update(client, user_session, portfolio, pdf_upload): response = client.post( url_for("task_orders.update", task_order_id=task_order.id), data=data ) - assert response.status_code == 302 assert task_order.number == data["number"] + assert response.status_code == 400 def test_task_orders_update_pdf( @@ -121,8 +121,8 @@ def test_task_orders_update_pdf( response = client.post( url_for("task_orders.update", task_order_id=task_order.id), data=data ) - assert response.status_code == 302 assert task_order.pdf.filename == pdf_upload2.filename + assert response.status_code == 400 def test_task_orders_update_delete_pdf(client, user_session, portfolio, pdf_upload): @@ -132,8 +132,19 @@ def test_task_orders_update_delete_pdf(client, user_session, portfolio, pdf_uplo response = client.post( url_for("task_orders.update", task_order_id=task_order.id), data=data ) - assert response.status_code == 302 assert task_order.pdf is None + assert response.status_code == 400 + + +def test_cannot_get_to_review_screen_with_incomplete_data( + client, user_session, portfolio +): + user_session(portfolio.owner) + data = {"number": "0123456789"} + response = client.post( + url_for("task_orders.update", portfolio_id=portfolio.id, review=True), data=data + ) + assert response.status_code == 400 @pytest.mark.skip(reason="Update after implementing new TO form")