Merge pull request #661 from dod-ccpo/dd-254-form

DD-254 form
This commit is contained in:
dandds
2019-02-20 16:04:23 -05:00
committed by GitHub
17 changed files with 428 additions and 17 deletions

View File

@@ -39,7 +39,13 @@ class Authorization(object):
@classmethod
def check_is_ko(cls, user, task_order):
if task_order.contracting_officer != user:
message = "review Task Order {}".format(task_order.id)
message = "review task order {}".format(task_order.id)
raise UnauthorizedError(user, message)
@classmethod
def check_is_so(cls, user, task_order):
if task_order.security_officer != user:
message = "review task order {}".format(task_order.id)
raise UnauthorizedError(user, message)
@classmethod

View File

@@ -4,6 +4,7 @@ from flask import current_app as app
from atst.database import db
from atst.models.task_order import TaskOrder
from atst.models.permissions import Permissions
from atst.models.dd_254 import DD254
from atst.domain.portfolios import Portfolios
from atst.domain.authz import Authorization
from .exceptions import NotFoundError
@@ -171,3 +172,26 @@ class TaskOrders(object):
raise TaskOrderError(
"{} is not an officer role on task orders".format(officer_type)
)
@classmethod
def add_dd_254(user, task_order, dd_254_data):
dd_254 = DD254(**dd_254_data)
task_order.dd_254 = dd_254
db.session.add(task_order)
db.session.commit()
class DD254s:
# TODO: standin implementation until we have a real download,
# sign, and verify process for the DD 254 PDF
@classmethod
def is_complete(cls, dd254):
if dd254 is None:
return False
for col in DD254.__table__.columns:
if getattr(dd254, col.name) is None:
return False
return True

View File

@@ -213,3 +213,12 @@ TEAM_EXPERIENCE = [
PERIOD_OF_PERFORMANCE_LENGTH = [
(str(x + 1), translate_duration(x + 1)) for x in range(24)
]
REQUIRED_DISTRIBUTIONS = [
("contractor", "Contractor"),
("subcontractor", "Subcontractor"),
("cognizant_so", "Cognizant Security Office for Prime and Subcontractor"),
("overseas", "U.S. Activity Responsible for Overseas Security Administration"),
("administrative_ko", "Administrative Contracting Officer"),
("other", "Other as necessary"),
]

39
atst/forms/dd_254.py Normal file
View File

@@ -0,0 +1,39 @@
from wtforms.fields import SelectMultipleField, StringField
from wtforms.fields.html5 import TelField
from wtforms.widgets import ListWidget, CheckboxInput
from wtforms.validators import Required
from atst.forms.validators import PhoneNumber
from .forms import CacheableForm
from .data import REQUIRED_DISTRIBUTIONS
from atst.utils.localization import translate
class DD254Form(CacheableForm):
certifying_official = StringField(
translate("forms.dd_254.certifying_official.label"),
description=translate("forms.dd_254.certifying_official.description"),
validators=[Required()],
)
certifying_official_title = StringField(
translate("forms.dd_254.certifying_official_title.label"),
validators=[Required()],
)
certifying_official_address = StringField(
translate("forms.dd_254.certifying_official_address.label"),
description=translate("forms.dd_254.certifying_official_address.description"),
validators=[Required()],
)
certifying_official_phone = TelField(
translate("forms.dd_254.certifying_official_phone.label"),
description=translate("forms.dd_254.certifying_official_phone.description"),
validators=[Required(), PhoneNumber()],
)
required_distribution = SelectMultipleField(
translate("forms.dd_254.required_distribution.label"),
choices=REQUIRED_DISTRIBUTIONS,
default="",
widget=ListWidget(prefix_label=False),
option_widget=CheckboxInput(),
)

View File

@@ -20,3 +20,4 @@ from .request_internal_comment import RequestInternalComment
from .audit_event import AuditEvent
from .invitation import Invitation
from .task_order import TaskOrder
from .dd_254 import DD254

31
atst/models/dd_254.py Normal file
View File

@@ -0,0 +1,31 @@
from sqlalchemy import Column, String
from sqlalchemy.types import ARRAY
from sqlalchemy.orm import relationship
from atst.models import Base, types, mixins
class DD254(Base, mixins.TimestampsMixin):
__tablename__ = "dd_254s"
id = types.Id()
certifying_official = Column(String)
certifying_official_title = Column(String)
certifying_official_address = Column(String)
certifying_official_phone = Column(String)
required_distribution = Column(ARRAY(String))
task_order = relationship("TaskOrder", uselist=False, backref="task_order")
def to_dictionary(self):
return {
c.name: getattr(self, c.name)
for c in self.__table__.columns
if c.name not in ["id"]
}
def __repr__(self):
return "<DD254(certifying_official='{}', task_order='{}', id='{}')>".format(
self.certifying_official, self.task_order.id, self.id
)

View File

@@ -49,6 +49,9 @@ class TaskOrder(Base, mixins.TimestampsMixin):
so_id = Column(ForeignKey("users.id"))
security_officer = relationship("User", foreign_keys="TaskOrder.so_id")
dd_254_id = Column(ForeignKey("dd_254s.id"))
dd_254 = relationship("DD254")
scope = Column(String) # Cloud Project Scope
defense_component = Column(String) # Department of Defense Component
app_migration = Column(String) # App Migration

View File

@@ -4,13 +4,14 @@ from flask import g, redirect, render_template, url_for, request as http_request
from . import portfolios_bp
from atst.database import db
from atst.domain.task_orders import TaskOrders
from atst.domain.task_orders import TaskOrders, DD254s
from atst.domain.exceptions import NotFoundError
from atst.domain.portfolios import Portfolios
from atst.domain.authz import Authorization
from atst.forms.officers import EditTaskOrderOfficersForm
from atst.models.task_order import Status as TaskOrderStatus
from atst.forms.ko_review import KOReviewForm
from atst.forms.dd_254 import DD254Form
@portfolios_bp.route("/portfolios/<portfolio_id>/task_orders")
@@ -60,12 +61,14 @@ def portfolio_funding(portfolio_id):
def view_task_order(portfolio_id, task_order_id):
portfolio = Portfolios.get(g.current_user, portfolio_id)
task_order = TaskOrders.get(g.current_user, task_order_id)
completed = TaskOrders.all_sections_complete(task_order)
to_form_complete = TaskOrders.all_sections_complete(task_order)
dd_254_complete = DD254s.is_complete(task_order.dd_254)
return render_template(
"portfolios/task_orders/show.html",
portfolio=portfolio,
task_order=task_order,
all_sections_complete=completed,
to_form_complete=to_form_complete,
dd_254_complete=dd_254_complete,
user=g.current_user,
)
@@ -154,3 +157,63 @@ def edit_task_order_invitations(portfolio_id, task_order_id):
task_order=task_order,
form=form,
)
def so_review_form(task_order):
if task_order.dd_254:
dd_254 = task_order.dd_254
form = DD254Form(obj=dd_254)
form.required_distribution.data = dd_254.required_distribution
return form
else:
so = task_order.officer_dictionary("security_officer")
form_data = {
"certifying_official": "{}, {}".format(
so.get("last_name", ""), so.get("first_name", "")
),
"co_phone": so.get("phone_number", ""),
}
return DD254Form(data=form_data)
@portfolios_bp.route("/portfolios/<portfolio_id>/task_order/<task_order_id>/dd254")
def so_review(portfolio_id, task_order_id):
task_order = TaskOrders.get(g.current_user, task_order_id)
Authorization.check_is_so(g.current_user, task_order)
form = so_review_form(task_order)
return render_template(
"portfolios/task_orders/so_review.html",
form=form,
portfolio=task_order.portfolio,
task_order=task_order,
)
@portfolios_bp.route(
"/portfolios/<portfolio_id>/task_order/<task_order_id>/dd254", methods=["POST"]
)
def submit_so_review(portfolio_id, task_order_id):
task_order = TaskOrders.get(g.current_user, task_order_id)
Authorization.check_is_so(g.current_user, task_order)
form = DD254Form(http_request.form)
if form.validate():
TaskOrders.add_dd_254(task_order, form.data)
# TODO: will redirect to download, sign, upload page
return redirect(
url_for(
"portfolios.view_task_order",
portfolio_id=task_order.portfolio.id,
task_order_id=task_order.id,
)
)
else:
return render_template(
"portfolios/task_orders/so_review.html",
form=form,
portfolio=task_order.portfolio,
task_order=task_order,
)