@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
39
atst/forms/dd_254.py
Normal 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(),
|
||||
)
|
@@ -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
31
atst/models/dd_254.py
Normal 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
|
||||
)
|
@@ -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
|
||||
|
@@ -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,
|
||||
)
|
||||
|
Reference in New Issue
Block a user