diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 0a2caeda..b3206b35 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -1,6 +1,7 @@ from flask import current_app as app from atst.database import db +from atst.models.clin import CLIN from atst.models.task_order import TaskOrder from . import BaseDomainClass @@ -18,27 +19,49 @@ class TaskOrders(BaseDomainClass): UNCLASSIFIED_FUNDING = [] @classmethod - 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) - + def create(cls, creator, portfolio_id, number, clins, pdf): + task_order = TaskOrder( + portfolio_id=portfolio_id, creator=creator, number=number, pdf=pdf + ) db.session.add(task_order) db.session.commit() + TaskOrders.create_clins(task_order.id, clins) + return task_order @classmethod - def update(cls, task_order_id, **kwargs): + def update(cls, task_order_id, number, clins, pdf): task_order = TaskOrders.get(task_order_id) - for key, value in kwargs.items(): - setattr(task_order, key, value) + task_order.pdf = pdf + + for clin in task_order.clins: + db.session.delete(clin) + + if number != task_order.number: + task_order.number = number + db.session.add(task_order) - db.session.add(task_order) db.session.commit() + TaskOrders.create_clins(task_order_id, clins) return task_order + @classmethod + def create_clins(cls, task_order_id, clin_list): + for clin_data in clin_list: + clin = CLIN( + task_order_id=task_order_id, + number=clin_data["number"], + loas=clin_data["loas"], + start_date=clin_data["start_date"], + end_date=clin_data["end_date"], + obligated_amount=clin_data["obligated_amount"], + jedi_clin_type=clin_data["jedi_clin_type"], + ) + db.session.add(clin) + db.session.commit() + @classmethod def section_completion_status(cls, task_order, section): if section in TaskOrders.mission_owner_sections(): diff --git a/atst/forms/data.py b/atst/forms/data.py index e0d4cf7f..b2b33e9a 100644 --- a/atst/forms/data.py +++ b/atst/forms/data.py @@ -221,3 +221,10 @@ ENV_ROLE_NO_ACCESS = "No Access" ENV_ROLES = [(role.value, role.value) for role in CSPRole] + [ (ENV_ROLE_NO_ACCESS, "No access") ] + +JEDI_CLIN_TYPES = [ + ("JEDI_CLIN_1", translate("forms.task_order.clin_01_label")), + ("JEDI_CLIN_2", translate("forms.task_order.clin_02_label")), + ("JEDI_CLIN_3", translate("forms.task_order.clin_03_label")), + ("JEDI_CLIN_4", translate("forms.task_order.clin_04_label")), +] diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index d5938bd1..c418899b 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -1,13 +1,45 @@ -from wtforms.fields import BooleanField, DecimalField, FileField, StringField +from wtforms.fields import ( + BooleanField, + DecimalField, + FieldList, + FileField, + FormField, + StringField, +) from wtforms.fields.html5 import DateField -from wtforms.validators import Required, Optional +from wtforms.validators import Required from flask_wtf.file import FileAllowed +from flask_wtf import FlaskForm +from .data import JEDI_CLIN_TYPES +from .fields import SelectField from .forms import BaseForm from atst.forms.validators import FileLength from atst.utils.localization import translate +class CLINForm(FlaskForm): + jedi_clin_type = SelectField("Jedi CLIN type", choices=JEDI_CLIN_TYPES) + number = StringField(validators=[Required()]) + start_date = DateField( + translate("forms.task_order.start_date_label"), + format="%m/%d/%Y", + validators=[Required()], + ) + end_date = DateField( + translate("forms.task_order.end_date_label"), + format="%m/%d/%Y", + validators=[Required()], + ) + obligated_amount = DecimalField() + loas = FieldList(StringField()) + + +class UnclassifiedCLINForm(CLINForm): + # TODO: overwrite jedi_clin_type to only include the unclassified options + pass + + class TaskOrderForm(BaseForm): number = StringField( translate("forms.task_order.number_label"), @@ -22,38 +54,7 @@ class TaskOrderForm(BaseForm): ], render_kw={"accept": ".pdf,application/pdf"}, ) - - -class FundingForm(BaseForm): - 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" - ) - clin_01 = DecimalField( - translate("forms.task_order.clin_01_label"), validators=[Optional()] - ) - clin_02 = DecimalField( - translate("forms.task_order.clin_02_label"), validators=[Optional()] - ) - clin_03 = DecimalField( - translate("forms.task_order.clin_03_label"), validators=[Optional()] - ) - clin_04 = DecimalField( - translate("forms.task_order.clin_04_label"), validators=[Optional()] - ) - - -class UnclassifiedFundingForm(FundingForm): - clin_02 = StringField( - translate("forms.task_order.unclassified_clin_02_label"), - filters=[BaseForm.remove_empty_string], - ) - clin_04 = StringField( - translate("forms.task_order.unclassified_clin_04_label"), - filters=[BaseForm.remove_empty_string], - ) + clins = FieldList(FormField(CLINForm)) class SignatureForm(BaseForm): diff --git a/atst/models/clin.py b/atst/models/clin.py index 0570ca02..dd4bdd76 100644 --- a/atst/models/clin.py +++ b/atst/models/clin.py @@ -7,10 +7,10 @@ from atst.models import Base, mixins, types class JEDICLINType(Enum): - JEDI_CLIN_1 = "jedi clin 0001" - JEDI_CLIN_2 = "jedi clin 0002" - JEDI_CLIN_3 = "jedi clin 0003" - JEDI_CLIN_4 = "jedi clin 0004" + JEDI_CLIN_1 = "JEDI_CLIN_1" + JEDI_CLIN_2 = "JEDI_CLIN_2" + JEDI_CLIN_3 = "JEDI_CLIN_3" + JEDI_CLIN_4 = "JEDI_CLIN_4" class CLIN(Base, mixins.TimestampsMixin): @@ -27,3 +27,10 @@ class CLIN(Base, mixins.TimestampsMixin): 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) + + def to_dictionary(self): + return { + c.name: getattr(self, c.name) + for c in self.__table__.columns + if c.name not in ["id"] + } diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 049b7d85..0e2d5ba2 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -34,7 +34,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): signer_dod_id = Column(String) signed_at = Column(DateTime) - clins = relationship("CLIN") + clins = relationship("CLIN", back_populates="task_order") @hybrid_property def pdf(self): @@ -129,6 +129,8 @@ class TaskOrder(Base, mixins.TimestampsMixin): def to_dictionary(self): return { "portfolio_name": self.portfolio_name, + "pdf": self.pdf, + "clins": [clin.to_dictionary() for clin in self.clins], **{ c.name: getattr(self, c.name) for c in self.__table__.columns diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 9778320f..1f6a271b 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -13,9 +13,7 @@ def render_task_orders_edit(portfolio_id, task_order_id=None, form=None): if task_order_id: task_order = TaskOrders.get(task_order_id) - render_args["form"] = form or TaskOrderForm( - number=task_order.number, pdf=task_order.pdf - ) + render_args["form"] = form or TaskOrderForm(**task_order.to_dictionary()) render_args["task_order_id"] = task_order_id else: render_args["form"] = form or TaskOrderForm() diff --git a/js/components/clin_fields.js b/js/components/clin_fields.js new file mode 100644 index 00000000..df52dd9d --- /dev/null +++ b/js/components/clin_fields.js @@ -0,0 +1,19 @@ +import DateSelector from './date_selector' +import textinput from './text_input' + +export default { + name: 'clin-fields', + + components: { + DateSelector, + textinput, + }, + + props: { + initialClinIndex: Number, + }, + + data: function() { + return { clinIndex: this.initialClinIndex } + }, +} diff --git a/js/components/date_selector.js b/js/components/date_selector.js index 0943bd0b..05809b0b 100644 --- a/js/components/date_selector.js +++ b/js/components/date_selector.js @@ -19,6 +19,7 @@ export default { initialyear: { type: String }, mindate: { type: String }, maxdate: { type: String }, + nameTag: { type: String }, }, data: function() { @@ -26,6 +27,7 @@ export default { day: this.initialday, month: this.initialmonth, year: this.initialyear, + name: this.nameTag, } }, diff --git a/js/components/forms/to_form.js b/js/components/forms/to_form.js new file mode 100644 index 00000000..5255fbb9 --- /dev/null +++ b/js/components/forms/to_form.js @@ -0,0 +1,41 @@ +import ClinFields from '../clin_fields' +import DateSelector from '../date_selector' +import FormMixin from '../../mixins/form' +import optionsinput from '../options_input' +import textinput from '../text_input' +import uploadinput from '../upload_input' + +export default { + name: 'to-form', + + mixins: [FormMixin], + + components: { + ClinFields, + DateSelector, + optionsinput, + textinput, + uploadinput, + }, + + props: { + initialClinCount: Number, + }, + + data: function() { + const clins = this.initialClinCount == 0 ? 1 : 0 + const clinIndex = this.initialClinCount == 0 ? 0 : this.initialClinCount - 1 + + return { + clins, + clinIndex, + } + }, + + methods: { + addClin: function(event) { + this.clins = this.clins + 1 + this.clinIndex = this.clinIndex + 1 + }, + }, +} diff --git a/js/index.js b/js/index.js index 9446814f..2f167883 100644 --- a/js/index.js +++ b/js/index.js @@ -41,6 +41,8 @@ import DeleteConfirmation from './components/delete_confirmation' import NewEnvironment from './components/forms/new_environment' import EnvironmentRole from './components/environment_role' import SemiCollapsibleText from './components/semi_collapsible_text' +import ToForm from './components/forms/to_form' +import ClinFields from './components/clin_fields' Vue.config.productionTip = false @@ -83,6 +85,8 @@ const app = new Vue({ NewEnvironment, EnvironmentRole, SemiCollapsibleText, + ToForm, + ClinFields, }, mounted: function() { diff --git a/templates/task_orders/edit.html b/templates/task_orders/edit.html index e8a5b6a6..09256b35 100644 --- a/templates/task_orders/edit.html +++ b/templates/task_orders/edit.html @@ -1,36 +1,256 @@ {% extends "portfolios/base.html" %} +{% from 'components/date_picker.html' import DatePicker %} +{% from 'components/icon.html' import Icon %} {% from 'components/save_button.html' import SaveButton %} +{% from 'components/options_input.html' import OptionsInput %} {% from 'components/text_input.html' import TextInput %} {% from 'components/upload_input.html' import UploadInput %} +{% macro CLINFields(fields) %} +