Merge branch 'master' into title-error-message
12
README.md
@ -19,14 +19,15 @@ Before running the setup scripts, a couple of dependencies need to be installed
|
|||||||
locally:
|
locally:
|
||||||
|
|
||||||
* `python` == 3.6
|
* `python` == 3.6
|
||||||
Python version 3.6 must be installed on your machine before installing `pipenv`.
|
Python version 3.6 **must** be installed on your machine before installing `pipenv`.
|
||||||
You can download Python 3.6 [from python.org](https://www.python.org/downloads/)
|
You can download Python 3.6 [from python.org](https://www.python.org/downloads/)
|
||||||
or use your preferred system package manager.
|
or use your preferred system package manager. Multiple versions of Python can exist on one
|
||||||
|
computer, but 3.6 is required for ATAT.
|
||||||
|
|
||||||
* `pipenv`
|
* `pipenv`
|
||||||
ATST requires `pipenv` to be installed for python dependency management. `pipenv`
|
ATST requires `pipenv` to be installed for python dependency management. `pipenv`
|
||||||
will create the virtual environment that the app requires. [See
|
will create the virtual environment that the app requires. [See
|
||||||
`pipenv`'s documentation for instructions on installing `pipenv](
|
`pipenv`'s documentation for instructions on installing `pipenv`](
|
||||||
https://pipenv.readthedocs.io/en/latest/install/#installing-pipenv).
|
https://pipenv.readthedocs.io/en/latest/install/#installing-pipenv).
|
||||||
|
|
||||||
* `yarn`
|
* `yarn`
|
||||||
@ -35,7 +36,10 @@ locally:
|
|||||||
|
|
||||||
* `postgres` >= 9.6
|
* `postgres` >= 9.6
|
||||||
ATST requires a PostgreSQL instance (>= 9.6) for persistence. Have PostgresSQL installed
|
ATST requires a PostgreSQL instance (>= 9.6) for persistence. Have PostgresSQL installed
|
||||||
and running on the default port of 5432. You can verify that PostgresSQL is running
|
and running on the default port of 5432. (A good resource for installing and running
|
||||||
|
PostgreSQL for Macs is [Postgres.app](https://postgresapp.com/). Follow the instructions,
|
||||||
|
including the optional Step 3, and add `/Applications/Postgres.app/Contents/Versions/latest/bin`
|
||||||
|
to your `PATH` environment variable.) You can verify that PostgresSQL is running
|
||||||
by executing `psql` and ensuring that a connection is successfully made.
|
by executing `psql` and ensuring that a connection is successfully made.
|
||||||
|
|
||||||
* `redis`
|
* `redis`
|
||||||
|
36
alembic/versions/1f690989e38e_add_pdf_to_task_order.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""Add PDF to Task Order
|
||||||
|
|
||||||
|
Revision ID: 1f690989e38e
|
||||||
|
Revises: 0ff4c31c4d28
|
||||||
|
Create Date: 2019-02-04 15:56:57.642156
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1f690989e38e'
|
||||||
|
down_revision = '0ff4c31c4d28'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('task_orders', sa.Column('pdf_attachment_id', postgresql.UUID(as_uuid=True), nullable=True))
|
||||||
|
op.drop_constraint('task_orders_attachments_attachment_id', 'task_orders', type_='foreignkey')
|
||||||
|
op.alter_column('task_orders', 'attachment_id', new_column_name='csp_attachment_id')
|
||||||
|
op.create_foreign_key('task_orders_attachments_pdf_attachment_id', 'task_orders', 'attachments', ['pdf_attachment_id'], ['id'])
|
||||||
|
op.create_foreign_key('task_orders_attachments_csp_attachment_id', 'task_orders', 'attachments', ['csp_attachment_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('task_orders_attachments_csp_attachment_id', 'task_orders', type_='foreignkey')
|
||||||
|
op.drop_constraint('task_orders_attachments_pdf_attachment_id', 'task_orders', type_='foreignkey')
|
||||||
|
op.alter_column('task_orders', 'csp_attachment_id', new_column_name='attachment_id')
|
||||||
|
op.create_foreign_key('task_orders_attachments_attachment_id', 'task_orders', 'attachments', ['attachment_id'], ['id'])
|
||||||
|
op.drop_column('task_orders', 'pdf_attachment_id')
|
||||||
|
# ### end Alembic commands ###
|
34
alembic/versions/b3a1a07cf30b_record_signer_dod_id.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""Record signer DOD ID
|
||||||
|
|
||||||
|
Revision ID: b3a1a07cf30b
|
||||||
|
Revises: c98adf9bb431
|
||||||
|
Create Date: 2019-02-12 10:16:19.349083
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b3a1a07cf30b'
|
||||||
|
down_revision = 'c98adf9bb431'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('task_orders', sa.Column('level_of_warrant', sa.Numeric(scale=2), nullable=True))
|
||||||
|
op.add_column('task_orders', sa.Column('signed_at', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('task_orders', sa.Column('signer_dod_id', sa.String(), nullable=True))
|
||||||
|
op.add_column('task_orders', sa.Column('unlimited_level_of_warrant', sa.Boolean(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('task_orders', 'unlimited_level_of_warrant')
|
||||||
|
op.drop_column('task_orders', 'signer_dod_id')
|
||||||
|
op.drop_column('task_orders', 'signed_at')
|
||||||
|
op.drop_column('task_orders', 'level_of_warrant')
|
||||||
|
# ### end Alembic commands ###
|
32
alembic/versions/c98adf9bb431_record_invitation_status.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""record invitation status
|
||||||
|
|
||||||
|
Revision ID: c98adf9bb431
|
||||||
|
Revises: 1f690989e38e
|
||||||
|
Create Date: 2019-02-06 09:02:28.617202
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c98adf9bb431'
|
||||||
|
down_revision = '1f690989e38e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('task_orders', sa.Column('cor_invite', sa.Boolean(), nullable=True))
|
||||||
|
op.add_column('task_orders', sa.Column('ko_invite', sa.Boolean(), nullable=True))
|
||||||
|
op.add_column('task_orders', sa.Column('so_invite', sa.Boolean(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('task_orders', 'so_invite')
|
||||||
|
op.drop_column('task_orders', 'ko_invite')
|
||||||
|
op.drop_column('task_orders', 'cor_invite')
|
||||||
|
# ### end Alembic commands ###
|
@ -1,3 +1,4 @@
|
|||||||
|
import datetime
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import pendulum
|
import pendulum
|
||||||
@ -31,131 +32,152 @@ class MockApplication:
|
|||||||
self.environments = [make_env(env_name) for env_name in envs]
|
self.environments = [make_env(env_name) for env_name in envs]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_sample_dates(_max=8):
|
||||||
|
current = datetime.datetime.today()
|
||||||
|
sample_dates = []
|
||||||
|
for _i in range(_max):
|
||||||
|
current = current - datetime.timedelta(days=29)
|
||||||
|
sample_dates.append(current.strftime("%m/%Y"))
|
||||||
|
|
||||||
|
reversed(sample_dates)
|
||||||
|
return sample_dates
|
||||||
|
|
||||||
|
|
||||||
class MockReportingProvider(ReportingInterface):
|
class MockReportingProvider(ReportingInterface):
|
||||||
|
FIXTURE_MONTHS = generate_sample_dates()
|
||||||
|
|
||||||
MONTHLY_SPEND_BY_ENVIRONMENT = {
|
MONTHLY_SPEND_BY_ENVIRONMENT = {
|
||||||
"LC04_Integ": {
|
"LC04_Integ": {
|
||||||
"02/2018": 284,
|
FIXTURE_MONTHS[7]: 284,
|
||||||
"03/2018": 1210,
|
FIXTURE_MONTHS[6]: 1210,
|
||||||
"04/2018": 1430,
|
FIXTURE_MONTHS[5]: 1430,
|
||||||
"05/2018": 1366,
|
FIXTURE_MONTHS[4]: 1366,
|
||||||
"06/2018": 1169,
|
FIXTURE_MONTHS[3]: 1169,
|
||||||
"07/2018": 991,
|
FIXTURE_MONTHS[2]: 991,
|
||||||
"08/2018": 978,
|
FIXTURE_MONTHS[1]: 978,
|
||||||
"09/2018": 737,
|
FIXTURE_MONTHS[0]: 737,
|
||||||
},
|
},
|
||||||
"LC04_PreProd": {
|
"LC04_PreProd": {
|
||||||
"02/2018": 812,
|
FIXTURE_MONTHS[7]: 812,
|
||||||
"03/2018": 1389,
|
FIXTURE_MONTHS[6]: 1389,
|
||||||
"04/2018": 1425,
|
FIXTURE_MONTHS[5]: 1425,
|
||||||
"05/2018": 1306,
|
FIXTURE_MONTHS[4]: 1306,
|
||||||
"06/2018": 1112,
|
FIXTURE_MONTHS[3]: 1112,
|
||||||
"07/2018": 936,
|
FIXTURE_MONTHS[2]: 936,
|
||||||
"08/2018": 921,
|
FIXTURE_MONTHS[1]: 921,
|
||||||
"09/2018": 694,
|
FIXTURE_MONTHS[0]: 694,
|
||||||
},
|
},
|
||||||
"LC04_Prod": {
|
"LC04_Prod": {
|
||||||
"02/2018": 1742,
|
FIXTURE_MONTHS[7]: 1742,
|
||||||
"03/2018": 1716,
|
FIXTURE_MONTHS[6]: 1716,
|
||||||
"04/2018": 1866,
|
FIXTURE_MONTHS[5]: 1866,
|
||||||
"05/2018": 1809,
|
FIXTURE_MONTHS[4]: 1809,
|
||||||
"06/2018": 1839,
|
FIXTURE_MONTHS[3]: 1839,
|
||||||
"07/2018": 1633,
|
FIXTURE_MONTHS[2]: 1633,
|
||||||
"08/2018": 1654,
|
FIXTURE_MONTHS[1]: 1654,
|
||||||
"09/2018": 1103,
|
FIXTURE_MONTHS[0]: 1103,
|
||||||
},
|
},
|
||||||
"SF18_Integ": {
|
"SF18_Integ": {
|
||||||
"04/2018": 1498,
|
FIXTURE_MONTHS[5]: 1498,
|
||||||
"05/2018": 1400,
|
FIXTURE_MONTHS[4]: 1400,
|
||||||
"06/2018": 1394,
|
FIXTURE_MONTHS[3]: 1394,
|
||||||
"07/2018": 1171,
|
FIXTURE_MONTHS[2]: 1171,
|
||||||
"08/2018": 1200,
|
FIXTURE_MONTHS[1]: 1200,
|
||||||
"09/2018": 963,
|
FIXTURE_MONTHS[0]: 963,
|
||||||
},
|
},
|
||||||
"SF18_PreProd": {
|
"SF18_PreProd": {
|
||||||
"04/2018": 1780,
|
FIXTURE_MONTHS[5]: 1780,
|
||||||
"05/2018": 1667,
|
FIXTURE_MONTHS[4]: 1667,
|
||||||
"06/2018": 1703,
|
FIXTURE_MONTHS[3]: 1703,
|
||||||
"07/2018": 1474,
|
FIXTURE_MONTHS[2]: 1474,
|
||||||
"08/2018": 1441,
|
FIXTURE_MONTHS[1]: 1441,
|
||||||
"09/2018": 933,
|
FIXTURE_MONTHS[0]: 933,
|
||||||
},
|
},
|
||||||
"SF18_Prod": {
|
"SF18_Prod": {
|
||||||
"04/2018": 1686,
|
FIXTURE_MONTHS[5]: 1686,
|
||||||
"05/2018": 1779,
|
FIXTURE_MONTHS[4]: 1779,
|
||||||
"06/2018": 1792,
|
FIXTURE_MONTHS[3]: 1792,
|
||||||
"07/2018": 1570,
|
FIXTURE_MONTHS[2]: 1570,
|
||||||
"08/2018": 1539,
|
FIXTURE_MONTHS[1]: 1539,
|
||||||
"09/2018": 986,
|
FIXTURE_MONTHS[0]: 986,
|
||||||
},
|
},
|
||||||
"Canton_Prod": {
|
"Canton_Prod": {
|
||||||
"05/2018": 28699,
|
FIXTURE_MONTHS[4]: 28699,
|
||||||
"06/2018": 26766,
|
FIXTURE_MONTHS[3]: 26766,
|
||||||
"07/2018": 22619,
|
FIXTURE_MONTHS[2]: 22619,
|
||||||
"08/2018": 24090,
|
FIXTURE_MONTHS[1]: 24090,
|
||||||
"09/2018": 16719,
|
FIXTURE_MONTHS[0]: 16719,
|
||||||
},
|
},
|
||||||
"BD04_Integ": {},
|
"BD04_Integ": {},
|
||||||
"BD04_PreProd": {
|
"BD04_PreProd": {
|
||||||
"02/2018": 7019,
|
FIXTURE_MONTHS[7]: 7019,
|
||||||
"03/2018": 3004,
|
FIXTURE_MONTHS[6]: 3004,
|
||||||
"04/2018": 2691,
|
FIXTURE_MONTHS[5]: 2691,
|
||||||
"05/2018": 2901,
|
FIXTURE_MONTHS[4]: 2901,
|
||||||
"06/2018": 3463,
|
FIXTURE_MONTHS[3]: 3463,
|
||||||
"07/2018": 3314,
|
FIXTURE_MONTHS[2]: 3314,
|
||||||
"08/2018": 3432,
|
FIXTURE_MONTHS[1]: 3432,
|
||||||
"09/2018": 723,
|
FIXTURE_MONTHS[0]: 723,
|
||||||
},
|
},
|
||||||
"SCV18_Dev": {"05/2019": 9797},
|
"SCV18_Dev": {FIXTURE_MONTHS[1]: 9797},
|
||||||
"Crown_CR Portal Dev": {
|
"Crown_CR Portal Dev": {
|
||||||
"03/2018": 208,
|
FIXTURE_MONTHS[6]: 208,
|
||||||
"04/2018": 457,
|
FIXTURE_MONTHS[5]: 457,
|
||||||
"05/2018": 671,
|
FIXTURE_MONTHS[4]: 671,
|
||||||
"06/2018": 136,
|
FIXTURE_MONTHS[3]: 136,
|
||||||
"07/2018": 1524,
|
FIXTURE_MONTHS[2]: 1524,
|
||||||
"08/2018": 2077,
|
FIXTURE_MONTHS[1]: 2077,
|
||||||
"09/2018": 1858,
|
FIXTURE_MONTHS[0]: 1858,
|
||||||
},
|
},
|
||||||
"Crown_CR Staging": {
|
"Crown_CR Staging": {
|
||||||
"03/2018": 208,
|
FIXTURE_MONTHS[6]: 208,
|
||||||
"04/2018": 457,
|
FIXTURE_MONTHS[5]: 457,
|
||||||
"05/2018": 671,
|
FIXTURE_MONTHS[4]: 671,
|
||||||
"06/2018": 136,
|
FIXTURE_MONTHS[3]: 136,
|
||||||
"07/2018": 1524,
|
FIXTURE_MONTHS[2]: 1524,
|
||||||
"08/2018": 2077,
|
FIXTURE_MONTHS[1]: 2077,
|
||||||
"09/2018": 1858,
|
FIXTURE_MONTHS[0]: 1858,
|
||||||
|
},
|
||||||
|
"Crown_CR Portal Test 1": {
|
||||||
|
FIXTURE_MONTHS[2]: 806,
|
||||||
|
FIXTURE_MONTHS[1]: 1966,
|
||||||
|
FIXTURE_MONTHS[0]: 2597,
|
||||||
|
},
|
||||||
|
"Crown_Jewels Prod": {
|
||||||
|
FIXTURE_MONTHS[2]: 806,
|
||||||
|
FIXTURE_MONTHS[1]: 1966,
|
||||||
|
FIXTURE_MONTHS[0]: 2597,
|
||||||
},
|
},
|
||||||
"Crown_CR Portal Test 1": {"07/2018": 806, "08/2018": 1966, "09/2018": 2597},
|
|
||||||
"Crown_Jewels Prod": {"07/2018": 806, "08/2018": 1966, "09/2018": 2597},
|
|
||||||
"Crown_Jewels Dev": {
|
"Crown_Jewels Dev": {
|
||||||
"03/2018": 145,
|
FIXTURE_MONTHS[6]: 145,
|
||||||
"04/2018": 719,
|
FIXTURE_MONTHS[5]: 719,
|
||||||
"05/2018": 1243,
|
FIXTURE_MONTHS[4]: 1243,
|
||||||
"06/2018": 2214,
|
FIXTURE_MONTHS[3]: 2214,
|
||||||
"07/2018": 2959,
|
FIXTURE_MONTHS[2]: 2959,
|
||||||
"08/2018": 4151,
|
FIXTURE_MONTHS[1]: 4151,
|
||||||
"09/2018": 4260,
|
FIXTURE_MONTHS[0]: 4260,
|
||||||
},
|
},
|
||||||
"NP02_Integ": {"08/2018": 284, "09/2018": 1210},
|
"NP02_Integ": {FIXTURE_MONTHS[1]: 284, FIXTURE_MONTHS[0]: 1210},
|
||||||
"NP02_PreProd": {"08/2018": 812, "09/2018": 1389},
|
"NP02_PreProd": {FIXTURE_MONTHS[1]: 812, FIXTURE_MONTHS[0]: 1389},
|
||||||
"NP02_Prod": {"08/2018": 3742, "09/2018": 4716},
|
"NP02_Prod": {FIXTURE_MONTHS[1]: 3742, FIXTURE_MONTHS[0]: 4716},
|
||||||
"FM_Integ": {"08/2018": 1498},
|
"FM_Integ": {FIXTURE_MONTHS[1]: 1498},
|
||||||
"FM_Prod": {"09/2018": 5686},
|
"FM_Prod": {FIXTURE_MONTHS[0]: 5686},
|
||||||
}
|
}
|
||||||
|
|
||||||
CUMULATIVE_BUDGET_AARDVARK = {
|
CUMULATIVE_BUDGET_AARDVARK = {
|
||||||
"02/2018": {"spend": 9857, "cumulative": 9857},
|
FIXTURE_MONTHS[7]: {"spend": 9857, "cumulative": 9857},
|
||||||
"03/2018": {"spend": 7881, "cumulative": 17738},
|
FIXTURE_MONTHS[6]: {"spend": 7881, "cumulative": 17738},
|
||||||
"04/2018": {"spend": 14010, "cumulative": 31748},
|
FIXTURE_MONTHS[5]: {"spend": 14010, "cumulative": 31748},
|
||||||
"05/2018": {"spend": 43510, "cumulative": 75259},
|
FIXTURE_MONTHS[4]: {"spend": 43510, "cumulative": 75259},
|
||||||
"06/2018": {"spend": 41725, "cumulative": 116_984},
|
FIXTURE_MONTHS[3]: {"spend": 41725, "cumulative": 116_984},
|
||||||
"07/2018": {"spend": 41328, "cumulative": 158_312},
|
FIXTURE_MONTHS[2]: {"spend": 41328, "cumulative": 158_312},
|
||||||
"08/2018": {"spend": 47491, "cumulative": 205_803},
|
FIXTURE_MONTHS[1]: {"spend": 47491, "cumulative": 205_803},
|
||||||
"09/2018": {"spend": 36028, "cumulative": 241_831},
|
FIXTURE_MONTHS[0]: {"spend": 36028, "cumulative": 241_831},
|
||||||
}
|
}
|
||||||
|
|
||||||
CUMULATIVE_BUDGET_BELUGA = {
|
CUMULATIVE_BUDGET_BELUGA = {
|
||||||
"08/2018": {"spend": 4838, "cumulative": 4838},
|
FIXTURE_MONTHS[1]: {"spend": 4838, "cumulative": 4838},
|
||||||
"09/2018": {"spend": 14500, "cumulative": 19338},
|
FIXTURE_MONTHS[0]: {"spend": 14500, "cumulative": 19338},
|
||||||
}
|
}
|
||||||
|
|
||||||
REPORT_FIXTURE_MAP = {
|
REPORT_FIXTURE_MAP = {
|
||||||
|
@ -19,7 +19,19 @@ def dollars(value):
|
|||||||
return "${:,.2f}".format(numberValue)
|
return "${:,.2f}".format(numberValue)
|
||||||
|
|
||||||
|
|
||||||
|
def justDollars(value):
|
||||||
|
raw = dollars(value)
|
||||||
|
return raw.split(".")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def justCents(value):
|
||||||
|
raw = dollars(value)
|
||||||
|
return raw.split(".")[1]
|
||||||
|
|
||||||
|
|
||||||
def usPhone(number):
|
def usPhone(number):
|
||||||
|
if not number:
|
||||||
|
return ""
|
||||||
phone = re.sub(r"\D", "", number)
|
phone = re.sub(r"\D", "", number)
|
||||||
return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:])
|
return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:])
|
||||||
|
|
||||||
@ -99,6 +111,8 @@ def normalizeOrder(title):
|
|||||||
def register_filters(app):
|
def register_filters(app):
|
||||||
app.jinja_env.filters["iconSvg"] = iconSvg
|
app.jinja_env.filters["iconSvg"] = iconSvg
|
||||||
app.jinja_env.filters["dollars"] = dollars
|
app.jinja_env.filters["dollars"] = dollars
|
||||||
|
app.jinja_env.filters["justDollars"] = justDollars
|
||||||
|
app.jinja_env.filters["justCents"] = justCents
|
||||||
app.jinja_env.filters["usPhone"] = usPhone
|
app.jinja_env.filters["usPhone"] = usPhone
|
||||||
app.jinja_env.filters["readableInteger"] = readableInteger
|
app.jinja_env.filters["readableInteger"] = readableInteger
|
||||||
app.jinja_env.filters["getOptionLabel"] = getOptionLabel
|
app.jinja_env.filters["getOptionLabel"] = getOptionLabel
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms.fields import StringField
|
from wtforms.fields import StringField, BooleanField
|
||||||
from wtforms.fields.html5 import TelField
|
from wtforms.fields.html5 import TelField
|
||||||
from wtforms.validators import Email, Length, Optional
|
from wtforms.validators import Email, Length, Optional
|
||||||
|
|
||||||
@ -15,6 +15,7 @@ class OfficerForm(FlaskForm):
|
|||||||
email = StringField("Email", validators=[Optional(), Email()])
|
email = StringField("Email", validators=[Optional(), Email()])
|
||||||
phone_number = TelField("Phone Number", validators=[PhoneNumber()])
|
phone_number = TelField("Phone Number", validators=[PhoneNumber()])
|
||||||
dod_id = StringField("DoD ID", validators=[Optional(), Length(min=10), IsNumber()])
|
dod_id = StringField("DoD ID", validators=[Optional(), Length(min=10), IsNumber()])
|
||||||
|
invite = BooleanField("Invite to Task Order Builder")
|
||||||
|
|
||||||
|
|
||||||
class EditTaskOrderOfficersForm(CacheableForm):
|
class EditTaskOrderOfficersForm(CacheableForm):
|
||||||
|
@ -222,3 +222,28 @@ class OversightForm(CacheableForm):
|
|||||||
|
|
||||||
class ReviewForm(CacheableForm):
|
class ReviewForm(CacheableForm):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureForm(CacheableForm):
|
||||||
|
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"),
|
||||||
|
validators=[Required()],
|
||||||
|
)
|
||||||
|
@ -17,6 +17,14 @@ class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
|||||||
portfolio = relationship("Portfolio")
|
portfolio = relationship("Portfolio")
|
||||||
environments = relationship("Environment", back_populates="application")
|
environments = relationship("Environment", back_populates="application")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def users(self):
|
||||||
|
return set([user for env in self.environments for user in env.users])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_users(self):
|
||||||
|
return len(self.users)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def displayname(self):
|
def displayname(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -2,7 +2,16 @@ from enum import Enum
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
from sqlalchemy import Column, Numeric, String, ForeignKey, Date, Integer
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
ForeignKey,
|
||||||
|
Date,
|
||||||
|
Integer,
|
||||||
|
DateTime,
|
||||||
|
Boolean,
|
||||||
|
)
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from sqlalchemy.types import ARRAY
|
from sqlalchemy.types import ARRAY
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
@ -12,6 +21,7 @@ from atst.models import Attachment, Base, types, mixins
|
|||||||
|
|
||||||
|
|
||||||
class Status(Enum):
|
class Status(Enum):
|
||||||
|
STARTED = "Started"
|
||||||
PENDING = "Pending"
|
PENDING = "Pending"
|
||||||
ACTIVE = "Active"
|
ACTIVE = "Active"
|
||||||
EXPIRED = "Expired"
|
EXPIRED = "Expired"
|
||||||
@ -51,8 +61,8 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
start_date = Column(Date) # Period of Performance
|
start_date = Column(Date) # Period of Performance
|
||||||
end_date = Column(Date)
|
end_date = Column(Date)
|
||||||
performance_length = Column(Integer)
|
performance_length = Column(Integer)
|
||||||
attachment_id = Column(ForeignKey("attachments.id"))
|
csp_attachment_id = Column(ForeignKey("attachments.id"))
|
||||||
_csp_estimate = relationship("Attachment")
|
_csp_estimate = relationship("Attachment", foreign_keys=[csp_attachment_id])
|
||||||
clin_01 = Column(Numeric(scale=2))
|
clin_01 = Column(Numeric(scale=2))
|
||||||
clin_02 = Column(Numeric(scale=2))
|
clin_02 = Column(Numeric(scale=2))
|
||||||
clin_03 = Column(Numeric(scale=2))
|
clin_03 = Column(Numeric(scale=2))
|
||||||
@ -62,19 +72,28 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
ko_email = Column(String) # Email
|
ko_email = Column(String) # Email
|
||||||
ko_phone_number = Column(String) # Phone Number
|
ko_phone_number = Column(String) # Phone Number
|
||||||
ko_dod_id = Column(String) # DOD ID
|
ko_dod_id = Column(String) # DOD ID
|
||||||
|
ko_invite = Column(Boolean)
|
||||||
cor_first_name = Column(String) # First Name
|
cor_first_name = Column(String) # First Name
|
||||||
cor_last_name = Column(String) # Last Name
|
cor_last_name = Column(String) # Last Name
|
||||||
cor_email = Column(String) # Email
|
cor_email = Column(String) # Email
|
||||||
cor_phone_number = Column(String) # Phone Number
|
cor_phone_number = Column(String) # Phone Number
|
||||||
cor_dod_id = Column(String) # DOD ID
|
cor_dod_id = Column(String) # DOD ID
|
||||||
|
cor_invite = Column(Boolean)
|
||||||
so_first_name = Column(String) # First Name
|
so_first_name = Column(String) # First Name
|
||||||
so_last_name = Column(String) # Last Name
|
so_last_name = Column(String) # Last Name
|
||||||
so_email = Column(String) # Email
|
so_email = Column(String) # Email
|
||||||
so_phone_number = Column(String) # Phone Number
|
so_phone_number = Column(String) # Phone Number
|
||||||
so_dod_id = Column(String) # DOD ID
|
so_dod_id = Column(String) # DOD ID
|
||||||
|
so_invite = Column(Boolean)
|
||||||
|
pdf_attachment_id = Column(ForeignKey("attachments.id"))
|
||||||
|
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
|
||||||
number = Column(String, unique=True) # Task Order Number
|
number = Column(String, unique=True) # Task Order Number
|
||||||
loa = Column(String) # Line of Accounting (LOA)
|
loa = Column(String) # Line of Accounting (LOA)
|
||||||
custom_clauses = Column(String) # Custom Clauses
|
custom_clauses = Column(String) # Custom Clauses
|
||||||
|
signer_dod_id = Column(String)
|
||||||
|
signed_at = Column(DateTime)
|
||||||
|
level_of_warrant = Column(Numeric(scale=2))
|
||||||
|
unlimited_level_of_warrant = Column(Boolean)
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def csp_estimate(self):
|
def csp_estimate(self):
|
||||||
@ -82,26 +101,38 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
|
|
||||||
@csp_estimate.setter
|
@csp_estimate.setter
|
||||||
def csp_estimate(self, new_csp_estimate):
|
def csp_estimate(self, new_csp_estimate):
|
||||||
if isinstance(new_csp_estimate, Attachment):
|
self._csp_estimate = self._set_attachment(new_csp_estimate, "_csp_estimate")
|
||||||
self._csp_estimate = new_csp_estimate
|
|
||||||
elif isinstance(new_csp_estimate, FileStorage):
|
@hybrid_property
|
||||||
self._csp_estimate = Attachment.attach(
|
def pdf(self):
|
||||||
new_csp_estimate, "task_order", self.id
|
return self._pdf
|
||||||
)
|
|
||||||
elif not new_csp_estimate and self._csp_estimate:
|
@pdf.setter
|
||||||
self._csp_estimate = None
|
def pdf(self, new_pdf):
|
||||||
elif new_csp_estimate:
|
self._pdf = self._set_attachment(new_pdf, "_pdf")
|
||||||
raise TypeError("Could not set csp_estimate with invalid type")
|
|
||||||
|
def _set_attachment(self, new_attachment, attribute):
|
||||||
|
if isinstance(new_attachment, Attachment):
|
||||||
|
return new_attachment
|
||||||
|
elif isinstance(new_attachment, FileStorage):
|
||||||
|
return Attachment.attach(new_attachment, "task_order", self.id)
|
||||||
|
elif not new_attachment and hasattr(self, attribute):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise TypeError("Could not set attachment with invalid type")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_submitted(self):
|
def is_submitted(self):
|
||||||
|
|
||||||
return (
|
return (
|
||||||
self.number is not None
|
self.number is not None
|
||||||
and self.start_date is not None
|
and self.start_date is not None
|
||||||
and self.end_date is not None
|
and self.end_date is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return self.status == Status.ACTIVE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
if self.is_submitted:
|
if self.is_submitted:
|
||||||
@ -112,7 +143,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
return Status.EXPIRED
|
return Status.EXPIRED
|
||||||
return Status.ACTIVE
|
return Status.ACTIVE
|
||||||
else:
|
else:
|
||||||
return Status.PENDING
|
return Status.STARTED
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_status(self):
|
def display_status(self):
|
||||||
@ -142,6 +173,44 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
def is_pending(self):
|
def is_pending(self):
|
||||||
return self.status == Status.PENDING
|
return self.status == Status.PENDING
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ko_invitable(self):
|
||||||
|
"""
|
||||||
|
The MO has indicated that the KO should be invited but we have not sent
|
||||||
|
an invite and attached the KO user
|
||||||
|
"""
|
||||||
|
return self.ko_invite and not self.contracting_officer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cor_invitable(self):
|
||||||
|
"""
|
||||||
|
The MO has indicated that the COR should be invited but we have not sent
|
||||||
|
an invite and attached the COR user
|
||||||
|
"""
|
||||||
|
return self.cor_invite and not self.contracting_officer_representative
|
||||||
|
|
||||||
|
@property
|
||||||
|
def so_invitable(self):
|
||||||
|
"""
|
||||||
|
The MO has indicated that the SO should be invited but we have not sent
|
||||||
|
an invite and attached the SO user
|
||||||
|
"""
|
||||||
|
return self.so_invite and not self.security_officer
|
||||||
|
|
||||||
|
_OFFICER_PREFIXES = {
|
||||||
|
"contracting_officer": "ko",
|
||||||
|
"contracting_officer_representative": "cor",
|
||||||
|
"security_officer": "so",
|
||||||
|
}
|
||||||
|
_OFFICER_PROPERTIES = ["first_name", "last_name", "phone_number", "email", "dod_id"]
|
||||||
|
|
||||||
|
def officer_dictionary(self, officer_type):
|
||||||
|
prefix = self._OFFICER_PREFIXES[officer_type]
|
||||||
|
return {
|
||||||
|
field: getattr(self, "{}_{}".format(prefix, field))
|
||||||
|
for field in self._OFFICER_PROPERTIES
|
||||||
|
}
|
||||||
|
|
||||||
def to_dictionary(self):
|
def to_dictionary(self):
|
||||||
return {
|
return {
|
||||||
"portfolio_name": self.portfolio_name,
|
"portfolio_name": self.portfolio_name,
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
import urllib.parse as url
|
import urllib.parse as url
|
||||||
from flask import Blueprint, render_template, g, redirect, session, url_for, request
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
render_template,
|
||||||
|
g,
|
||||||
|
redirect,
|
||||||
|
session,
|
||||||
|
url_for,
|
||||||
|
request,
|
||||||
|
make_response,
|
||||||
|
)
|
||||||
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from jinja2.exceptions import TemplateNotFound
|
from jinja2.exceptions import TemplateNotFound
|
||||||
@ -56,7 +65,7 @@ def home():
|
|||||||
num_portfolios = len([role for role in user.portfolio_roles if role.is_active])
|
num_portfolios = len([role for role in user.portfolio_roles if role.is_active])
|
||||||
|
|
||||||
if num_portfolios == 0:
|
if num_portfolios == 0:
|
||||||
return redirect(url_for("requests.requests_index"))
|
return redirect(url_for("portfolios.portfolios"))
|
||||||
elif num_portfolios == 1:
|
elif num_portfolios == 1:
|
||||||
portfolio_role = user.portfolio_roles[0]
|
portfolio_role = user.portfolio_roles[0]
|
||||||
portfolio_id = portfolio_role.portfolio.id
|
portfolio_id = portfolio_role.portfolio.id
|
||||||
@ -131,7 +140,9 @@ def login_redirect():
|
|||||||
@bp.route("/logout")
|
@bp.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
_logout()
|
_logout()
|
||||||
return redirect(url_for(".root"))
|
response = make_response(redirect(url_for(".root")))
|
||||||
|
response.set_cookie("expandSidenav", "", expires=0)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/activity-history")
|
@bp.route("/activity-history")
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from flask import Blueprint, request as http_request, g, render_template
|
from flask import Blueprint, request as http_request, g, render_template
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
portfolios_bp = Blueprint("portfolios", __name__)
|
portfolios_bp = Blueprint("portfolios", __name__)
|
||||||
|
|
||||||
@ -31,4 +32,24 @@ def portfolio():
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return {"portfolio": portfolio, "permissions": Permissions, "user_can": user_can}
|
if not portfolio is None:
|
||||||
|
active_task_orders = [
|
||||||
|
task_order for task_order in portfolio.task_orders if task_order.is_active
|
||||||
|
]
|
||||||
|
funding_end_date = (
|
||||||
|
sorted(active_task_orders, key=attrgetter("end_date"))[-1].end_date
|
||||||
|
if active_task_orders
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
funded = len(active_task_orders) > 1
|
||||||
|
else:
|
||||||
|
funding_end_date = None
|
||||||
|
funded = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"portfolio": portfolio,
|
||||||
|
"permissions": Permissions,
|
||||||
|
"user_can": user_can,
|
||||||
|
"funding_end_date": funding_end_date,
|
||||||
|
"funded": funded,
|
||||||
|
}
|
||||||
|
@ -15,14 +15,27 @@ from atst.models.permissions import Permissions
|
|||||||
@portfolios_bp.route("/portfolios")
|
@portfolios_bp.route("/portfolios")
|
||||||
def portfolios():
|
def portfolios():
|
||||||
portfolios = Portfolios.for_user(g.current_user)
|
portfolios = Portfolios.for_user(g.current_user)
|
||||||
|
|
||||||
|
if portfolios:
|
||||||
return render_template("portfolios/index.html", page=5, portfolios=portfolios)
|
return render_template("portfolios/index.html", page=5, portfolios=portfolios)
|
||||||
|
else:
|
||||||
|
return render_template("portfolios/blank_slate.html")
|
||||||
|
|
||||||
|
|
||||||
@portfolios_bp.route("/portfolios/<portfolio_id>/edit")
|
@portfolios_bp.route("/portfolios/<portfolio_id>/admin")
|
||||||
def portfolio(portfolio_id):
|
def portfolio_admin(portfolio_id):
|
||||||
portfolio = Portfolios.get_for_update_information(g.current_user, portfolio_id)
|
portfolio = Portfolios.get_for_update_information(g.current_user, portfolio_id)
|
||||||
form = PortfolioForm(data={"name": portfolio.name})
|
form = PortfolioForm(data={"name": portfolio.name})
|
||||||
return render_template("portfolios/edit.html", form=form, portfolio=portfolio)
|
pagination_opts = Paginator.get_pagination_opts(http_request)
|
||||||
|
audit_events = AuditLog.get_portfolio_events(
|
||||||
|
g.current_user, portfolio, pagination_opts
|
||||||
|
)
|
||||||
|
return render_template(
|
||||||
|
"portfolios/admin.html",
|
||||||
|
form=form,
|
||||||
|
portfolio=portfolio,
|
||||||
|
audit_events=audit_events,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@portfolios_bp.route("/portfolios/<portfolio_id>/edit", methods=["POST"])
|
@portfolios_bp.route("/portfolios/<portfolio_id>/edit", methods=["POST"])
|
||||||
@ -62,9 +75,11 @@ def portfolio_reports(portfolio_id):
|
|||||||
prev_month = current_month - timedelta(days=28)
|
prev_month = current_month - timedelta(days=28)
|
||||||
two_months_ago = prev_month - timedelta(days=28)
|
two_months_ago = prev_month - timedelta(days=28)
|
||||||
|
|
||||||
expiration_date = (
|
task_order = next(
|
||||||
portfolio.legacy_task_order and portfolio.legacy_task_order.expiration_date
|
(task_order for task_order in portfolio.task_orders if task_order.is_active),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
|
expiration_date = task_order and task_order.end_date
|
||||||
if expiration_date:
|
if expiration_date:
|
||||||
remaining_difference = expiration_date - today
|
remaining_difference = expiration_date - today
|
||||||
remaining_days = remaining_difference.days
|
remaining_days = remaining_difference.days
|
||||||
@ -76,8 +91,7 @@ def portfolio_reports(portfolio_id):
|
|||||||
cumulative_budget=Reports.cumulative_budget(portfolio),
|
cumulative_budget=Reports.cumulative_budget(portfolio),
|
||||||
portfolio_totals=Reports.portfolio_totals(portfolio),
|
portfolio_totals=Reports.portfolio_totals(portfolio),
|
||||||
monthly_totals=Reports.monthly_totals(portfolio),
|
monthly_totals=Reports.monthly_totals(portfolio),
|
||||||
jedi_request=portfolio.request,
|
task_order=task_order,
|
||||||
legacy_task_order=portfolio.legacy_task_order,
|
|
||||||
current_month=current_month,
|
current_month=current_month,
|
||||||
prev_month=prev_month,
|
prev_month=prev_month,
|
||||||
two_months_ago=two_months_ago,
|
two_months_ago=two_months_ago,
|
||||||
|
@ -23,26 +23,25 @@ from atst.models.permissions import Permissions
|
|||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_portfolio_role(portfolio_role):
|
||||||
|
return {
|
||||||
|
"name": portfolio_role.user_name,
|
||||||
|
"status": portfolio_role.display_status,
|
||||||
|
"id": portfolio_role.user_id,
|
||||||
|
"role": portfolio_role.role_displayname,
|
||||||
|
"num_env": portfolio_role.num_environment_roles,
|
||||||
|
"edit_link": url_for(
|
||||||
|
"portfolios.view_member",
|
||||||
|
portfolio_id=portfolio_role.portfolio_id,
|
||||||
|
member_id=portfolio_role.user_id,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@portfolios_bp.route("/portfolios/<portfolio_id>/members")
|
@portfolios_bp.route("/portfolios/<portfolio_id>/members")
|
||||||
def portfolio_members(portfolio_id):
|
def portfolio_members(portfolio_id):
|
||||||
portfolio = Portfolios.get_with_members(g.current_user, portfolio_id)
|
portfolio = Portfolios.get_with_members(g.current_user, portfolio_id)
|
||||||
new_member_name = http_request.args.get("newMemberName")
|
members_list = [serialize_portfolio_role(k) for k in portfolio.members]
|
||||||
new_member = next(
|
|
||||||
filter(lambda m: m.user_name == new_member_name, portfolio.members), None
|
|
||||||
)
|
|
||||||
members_list = [
|
|
||||||
{
|
|
||||||
"name": k.user_name,
|
|
||||||
"status": k.display_status,
|
|
||||||
"id": k.user_id,
|
|
||||||
"role": k.role_displayname,
|
|
||||||
"num_env": k.num_environment_roles,
|
|
||||||
"edit_link": url_for(
|
|
||||||
"portfolios.view_member", portfolio_id=portfolio.id, member_id=k.user_id
|
|
||||||
),
|
|
||||||
}
|
|
||||||
for k in portfolio.members
|
|
||||||
]
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"portfolios/members/index.html",
|
"portfolios/members/index.html",
|
||||||
@ -50,7 +49,21 @@ def portfolio_members(portfolio_id):
|
|||||||
role_choices=PORTFOLIO_ROLE_DEFINITIONS,
|
role_choices=PORTFOLIO_ROLE_DEFINITIONS,
|
||||||
status_choices=MEMBER_STATUS_CHOICES,
|
status_choices=MEMBER_STATUS_CHOICES,
|
||||||
members=members_list,
|
members=members_list,
|
||||||
new_member=new_member,
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@portfolios_bp.route("/portfolios/<portfolio_id>/applications/<application_id>/members")
|
||||||
|
def application_members(portfolio_id, application_id):
|
||||||
|
portfolio = Portfolios.get_with_members(g.current_user, portfolio_id)
|
||||||
|
application = Applications.get(g.current_user, portfolio, application_id)
|
||||||
|
# TODO: this should show only members that have env roles in this application
|
||||||
|
members_list = [serialize_portfolio_role(k) for k in portfolio.members]
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"portfolios/applications/members.html",
|
||||||
|
portfolio=portfolio,
|
||||||
|
application=application,
|
||||||
|
members=members_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -76,7 +89,7 @@ def create_member(portfolio_id):
|
|||||||
)
|
)
|
||||||
invite_service.invite()
|
invite_service.invite()
|
||||||
|
|
||||||
flash("new_portfolio_member", new_member=new_member, portfolio=portfolio)
|
flash("new_portfolio_member", new_member=member, portfolio=portfolio)
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("portfolios.portfolio_members", portfolio_id=portfolio.id)
|
url_for("portfolios.portfolio_members", portfolio_id=portfolio.id)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from operator import itemgetter
|
|
||||||
|
|
||||||
from flask import g, redirect, render_template, url_for, request as http_request
|
from flask import g, redirect, render_template, url_for, request as http_request
|
||||||
|
|
||||||
@ -41,22 +40,17 @@ def portfolio_funding(portfolio_id):
|
|||||||
task_orders_by_status[task_order.status].append(serialized_task_order)
|
task_orders_by_status[task_order.status].append(serialized_task_order)
|
||||||
|
|
||||||
active_task_orders = task_orders_by_status.get(TaskOrderStatus.ACTIVE, [])
|
active_task_orders = task_orders_by_status.get(TaskOrderStatus.ACTIVE, [])
|
||||||
funding_end_date = (
|
|
||||||
sorted(active_task_orders, key=itemgetter("end_date"))[-1]["end_date"]
|
|
||||||
if active_task_orders
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
funded = len(active_task_orders) > 1
|
|
||||||
total_balance = sum([task_order["balance"] for task_order in active_task_orders])
|
total_balance = sum([task_order["balance"] for task_order in active_task_orders])
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"portfolios/task_orders/index.html",
|
"portfolios/task_orders/index.html",
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
pending_task_orders=task_orders_by_status.get(TaskOrderStatus.PENDING, []),
|
pending_task_orders=(
|
||||||
|
task_orders_by_status.get(TaskOrderStatus.STARTED, [])
|
||||||
|
+ task_orders_by_status.get(TaskOrderStatus.PENDING, [])
|
||||||
|
),
|
||||||
active_task_orders=active_task_orders,
|
active_task_orders=active_task_orders,
|
||||||
expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []),
|
expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []),
|
||||||
funding_end_date=funding_end_date,
|
|
||||||
funded=funded,
|
|
||||||
total_balance=total_balance,
|
total_balance=total_balance,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -101,11 +95,7 @@ def submit_ko_review(portfolio_id, task_order_id, form=None):
|
|||||||
if form.validate():
|
if form.validate():
|
||||||
TaskOrders.update(user=g.current_user, task_order=task_order, **form.data)
|
TaskOrders.update(user=g.current_user, task_order=task_order, **form.data)
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for("task_orders.signature_requested", task_order_id=task_order_id)
|
||||||
"portfolios.view_task_order",
|
|
||||||
portfolio_id=portfolio_id,
|
|
||||||
task_order_id=task_order_id,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return render_template(
|
return render_template(
|
||||||
|
@ -5,3 +5,4 @@ task_orders_bp = Blueprint("task_orders", __name__)
|
|||||||
from . import new
|
from . import new
|
||||||
from . import index
|
from . import index
|
||||||
from . import invite
|
from . import invite
|
||||||
|
from . import signing
|
||||||
|
@ -20,20 +20,29 @@ def download_summary(task_order_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_file(attachment):
|
||||||
|
generator = app.csp.files.download(attachment.object_name)
|
||||||
|
return Response(
|
||||||
|
generator,
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": "attachment; filename={}".format(attachment.filename)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@task_orders_bp.route("/task_orders/csp_estimate/<task_order_id>")
|
@task_orders_bp.route("/task_orders/csp_estimate/<task_order_id>")
|
||||||
def download_csp_estimate(task_order_id):
|
def download_csp_estimate(task_order_id):
|
||||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||||
if task_order.csp_estimate:
|
if task_order.csp_estimate:
|
||||||
estimate = task_order.csp_estimate
|
return send_file(task_order.csp_estimate)
|
||||||
generator = app.csp.files.download(estimate.object_name)
|
|
||||||
return Response(
|
|
||||||
generator,
|
|
||||||
headers={
|
|
||||||
"Content-Disposition": "attachment; filename={}".format(
|
|
||||||
estimate.filename
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise NotFoundError("task_order CSP estimate")
|
raise NotFoundError("task_order CSP estimate")
|
||||||
|
|
||||||
|
|
||||||
|
@task_orders_bp.route("/task_orders/pdf/<task_order_id>")
|
||||||
|
def download_task_order_pdf(task_order_id):
|
||||||
|
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||||
|
if task_order.pdf:
|
||||||
|
return send_file(task_order.pdf)
|
||||||
|
else:
|
||||||
|
raise NotFoundError("task_order pdf")
|
||||||
|
@ -3,11 +3,57 @@ from flask import redirect, url_for, g
|
|||||||
from . import task_orders_bp
|
from . import task_orders_bp
|
||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
|
from atst.domain.portfolio_roles import PortfolioRoles
|
||||||
|
from atst.services.invitation import Invitation as InvitationService
|
||||||
|
|
||||||
|
|
||||||
|
OFFICER_INVITATIONS = [
|
||||||
|
{
|
||||||
|
"field": "ko_invite",
|
||||||
|
"role": "contracting_officer",
|
||||||
|
"subject": "Review a task order",
|
||||||
|
"template": "emails/invitation.txt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "cor_invite",
|
||||||
|
"role": "contracting_officer_representative",
|
||||||
|
"subject": "Help with a task order",
|
||||||
|
"template": "emails/invitation.txt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "so_invite",
|
||||||
|
"role": "security_officer",
|
||||||
|
"subject": "Review security for a task order",
|
||||||
|
"template": "emails/invitation.txt",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def update_officer_invitations(user, task_order):
|
||||||
|
for officer_type in OFFICER_INVITATIONS:
|
||||||
|
field = officer_type["field"]
|
||||||
|
if getattr(task_order, field) and not getattr(task_order, officer_type["role"]):
|
||||||
|
officer_data = task_order.officer_dictionary(officer_type["role"])
|
||||||
|
officer = TaskOrders.add_officer(
|
||||||
|
user, task_order, officer_type["role"], officer_data
|
||||||
|
)
|
||||||
|
pf_officer_member = PortfolioRoles.get(task_order.portfolio.id, officer.id)
|
||||||
|
invite_service = InvitationService(
|
||||||
|
user,
|
||||||
|
pf_officer_member,
|
||||||
|
officer_data["email"],
|
||||||
|
subject=officer_type["subject"],
|
||||||
|
email_template=officer_type["template"],
|
||||||
|
)
|
||||||
|
invite_service.invite()
|
||||||
|
|
||||||
|
|
||||||
@task_orders_bp.route("/task_orders/invite/<task_order_id>", methods=["POST"])
|
@task_orders_bp.route("/task_orders/invite/<task_order_id>", methods=["POST"])
|
||||||
def invite(task_order_id):
|
def invite(task_order_id):
|
||||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||||
|
if TaskOrders.all_sections_complete(task_order):
|
||||||
|
update_officer_invitations(g.current_user, task_order)
|
||||||
|
|
||||||
portfolio = task_order.portfolio
|
portfolio = task_order.portfolio
|
||||||
flash("task_order_congrats", portfolio=portfolio)
|
flash("task_order_congrats", portfolio=portfolio)
|
||||||
return redirect(
|
return redirect(
|
||||||
@ -17,3 +63,8 @@ def invite(task_order_id):
|
|||||||
task_order_id=task_order.id,
|
task_order_id=task_order.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
flash("task_order_incomplete")
|
||||||
|
return redirect(
|
||||||
|
url_for("task_orders.new", screen=4, task_order_id=task_order.id)
|
||||||
|
)
|
||||||
|
@ -12,9 +12,7 @@ from flask import (
|
|||||||
from . import task_orders_bp
|
from . import task_orders_bp
|
||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.domain.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
from atst.domain.portfolio_roles import PortfolioRoles
|
|
||||||
import atst.forms.task_order as task_order_form
|
import atst.forms.task_order as task_order_form
|
||||||
from atst.services.invitation import Invitation as InvitationService
|
|
||||||
|
|
||||||
|
|
||||||
TASK_ORDER_SECTIONS = [
|
TASK_ORDER_SECTIONS = [
|
||||||
@ -173,7 +171,7 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
return self.form.validate()
|
return self.form.validate()
|
||||||
|
|
||||||
def _update_task_order(self):
|
def update(self):
|
||||||
if self.task_order:
|
if self.task_order:
|
||||||
if "portfolio_name" in self.form.data:
|
if "portfolio_name" in self.form.data:
|
||||||
new_name = self.form.data["portfolio_name"]
|
new_name = self.form.data["portfolio_name"]
|
||||||
@ -189,65 +187,6 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
|||||||
self._task_order = TaskOrders.create(portfolio=pf, creator=self.user)
|
self._task_order = TaskOrders.create(portfolio=pf, creator=self.user)
|
||||||
TaskOrders.update(self.user, self.task_order, **self.task_order_form_data)
|
TaskOrders.update(self.user, self.task_order, **self.task_order_form_data)
|
||||||
|
|
||||||
OFFICER_INVITATIONS = [
|
|
||||||
{
|
|
||||||
"field": "ko_invite",
|
|
||||||
"prefix": "ko",
|
|
||||||
"role": "contracting_officer",
|
|
||||||
"subject": "Review a task order",
|
|
||||||
"template": "emails/invitation.txt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"field": "cor_invite",
|
|
||||||
"prefix": "cor",
|
|
||||||
"role": "contracting_officer_representative",
|
|
||||||
"subject": "Help with a task order",
|
|
||||||
"template": "emails/invitation.txt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"field": "so_invite",
|
|
||||||
"prefix": "so",
|
|
||||||
"role": "security_officer",
|
|
||||||
"subject": "Review security for a task order",
|
|
||||||
"template": "emails/invitation.txt",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
def _update_officer_invitations(self):
|
|
||||||
for officer_type in self.OFFICER_INVITATIONS:
|
|
||||||
field = officer_type["field"]
|
|
||||||
if (
|
|
||||||
hasattr(self.form, field)
|
|
||||||
and self.form[field].data
|
|
||||||
and not getattr(self.task_order, officer_type["role"])
|
|
||||||
):
|
|
||||||
prefix = officer_type["prefix"]
|
|
||||||
officer_data = {
|
|
||||||
field: getattr(self.task_order, prefix + "_" + field)
|
|
||||||
for field in [
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"email",
|
|
||||||
"phone_number",
|
|
||||||
"dod_id",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
officer = TaskOrders.add_officer(
|
|
||||||
self.user, self.task_order, officer_type["role"], officer_data
|
|
||||||
)
|
|
||||||
pf_officer_member = PortfolioRoles.get(self.portfolio.id, officer.id)
|
|
||||||
invite_service = InvitationService(
|
|
||||||
self.user,
|
|
||||||
pf_officer_member,
|
|
||||||
officer_data["email"],
|
|
||||||
subject=officer_type["subject"],
|
|
||||||
email_template=officer_type["template"],
|
|
||||||
)
|
|
||||||
invite_service.invite()
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
self._update_task_order()
|
|
||||||
self._update_officer_invitations()
|
|
||||||
return self.task_order
|
return self.task_order
|
||||||
|
|
||||||
|
|
||||||
|
74
atst/routes/task_orders/signing.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from flask import url_for, redirect, render_template, g, request as http_request
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from . import task_orders_bp
|
||||||
|
from atst.domain.authz import Authorization
|
||||||
|
from atst.domain.exceptions import NotFoundError
|
||||||
|
from atst.domain.task_orders import TaskOrders
|
||||||
|
from atst.forms.task_order import SignatureForm
|
||||||
|
from atst.utils.flash import formatted_flash as flash
|
||||||
|
|
||||||
|
|
||||||
|
def find_unsigned_ko_to(task_order_id):
|
||||||
|
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||||
|
Authorization.check_is_ko(g.current_user, task_order)
|
||||||
|
|
||||||
|
if task_order.signer_dod_id is not None:
|
||||||
|
raise NotFoundError("task_order")
|
||||||
|
|
||||||
|
return task_order
|
||||||
|
|
||||||
|
|
||||||
|
@task_orders_bp.route("/task_orders/<task_order_id>/digital_signature", methods=["GET"])
|
||||||
|
def signature_requested(task_order_id):
|
||||||
|
task_order = find_unsigned_ko_to(task_order_id)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"task_orders/signing/signature_requested.html",
|
||||||
|
task_order_id=task_order.id,
|
||||||
|
form=SignatureForm(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@task_orders_bp.route(
|
||||||
|
"/task_orders/<task_order_id>/digital_signature", methods=["POST"]
|
||||||
|
)
|
||||||
|
def record_signature(task_order_id):
|
||||||
|
task_order = find_unsigned_ko_to(task_order_id)
|
||||||
|
|
||||||
|
form_data = {**http_request.form}
|
||||||
|
|
||||||
|
if "unlimited_level_of_warrant" in form_data and form_data[
|
||||||
|
"unlimited_level_of_warrant"
|
||||||
|
] == ["y"]:
|
||||||
|
del form_data["level_of_warrant"]
|
||||||
|
|
||||||
|
form = SignatureForm(form_data)
|
||||||
|
|
||||||
|
if form.validate():
|
||||||
|
TaskOrders.update(
|
||||||
|
user=g.current_user,
|
||||||
|
task_order=task_order,
|
||||||
|
signer_dod_id=g.current_user.dod_id,
|
||||||
|
signed_at=datetime.datetime.now(),
|
||||||
|
**form.data,
|
||||||
|
)
|
||||||
|
|
||||||
|
flash("task_order_signed")
|
||||||
|
return redirect(
|
||||||
|
url_for(
|
||||||
|
"portfolios.view_task_order",
|
||||||
|
portfolio_id=task_order.portfolio_id,
|
||||||
|
task_order_id=task_order.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
render_template(
|
||||||
|
"task_orders/signing/signature_requested.html",
|
||||||
|
task_order_id=task_order_id,
|
||||||
|
form=form,
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
@ -1,6 +1,13 @@
|
|||||||
from flask import flash, render_template_string
|
from flask import flash, render_template_string
|
||||||
|
|
||||||
MESSAGES = {
|
MESSAGES = {
|
||||||
|
"task_order_signed": {
|
||||||
|
"title_template": "Task Order Signed",
|
||||||
|
"message_template": """
|
||||||
|
<p>Task order has been signed successfully</p>
|
||||||
|
""",
|
||||||
|
"category": "success",
|
||||||
|
},
|
||||||
"new_portfolio_member": {
|
"new_portfolio_member": {
|
||||||
"title_template": "Member added successfully",
|
"title_template": "Member added successfully",
|
||||||
"message_template": """
|
"message_template": """
|
||||||
@ -128,6 +135,13 @@ MESSAGES = {
|
|||||||
""",
|
""",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
|
"task_order_incomplete": {
|
||||||
|
"title_template": "Task Order Incomplete",
|
||||||
|
"message_template": """
|
||||||
|
You must complete your Task Order form before submitting.
|
||||||
|
""",
|
||||||
|
"category": "error",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from flask.json import JSONEncoder
|
from flask.json import JSONEncoder
|
||||||
|
from werkzeug.datastructures import FileStorage
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from atst.models.attachment import Attachment
|
from atst.models.attachment import Attachment
|
||||||
|
|
||||||
@ -7,6 +8,8 @@ class CustomJSONEncoder(JSONEncoder):
|
|||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
if isinstance(obj, Attachment):
|
if isinstance(obj, Attachment):
|
||||||
return obj.filename
|
return obj.filename
|
||||||
if isinstance(obj, date):
|
elif isinstance(obj, date):
|
||||||
return obj.strftime("%Y-%m-%d")
|
return obj.strftime("%Y-%m-%d")
|
||||||
|
elif isinstance(obj, FileStorage):
|
||||||
|
return obj.filename
|
||||||
return JSONEncoder.default(self, obj)
|
return JSONEncoder.default(self, obj)
|
||||||
|
@ -4,6 +4,7 @@ import { conformToMask } from 'vue-text-mask'
|
|||||||
import FormMixin from '../../mixins/form'
|
import FormMixin from '../../mixins/form'
|
||||||
import textinput from '../text_input'
|
import textinput from '../text_input'
|
||||||
import optionsinput from '../options_input'
|
import optionsinput from '../options_input'
|
||||||
|
import uploadinput from '../upload_input'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'funding',
|
name: 'funding',
|
||||||
@ -13,6 +14,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
textinput,
|
textinput,
|
||||||
optionsinput,
|
optionsinput,
|
||||||
|
uploadinput,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -32,7 +34,6 @@ export default {
|
|||||||
clin_02 = 0,
|
clin_02 = 0,
|
||||||
clin_03 = 0,
|
clin_03 = 0,
|
||||||
clin_04 = 0,
|
clin_04 = 0,
|
||||||
csp_estimate,
|
|
||||||
} = this.initialData
|
} = this.initialData
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -40,7 +41,6 @@ export default {
|
|||||||
clin_02,
|
clin_02,
|
||||||
clin_03,
|
clin_03,
|
||||||
clin_04,
|
clin_04,
|
||||||
showUpload: !csp_estimate || this.uploadErrors.length > 0,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -63,9 +63,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
showUploadInput: function() {
|
|
||||||
this.showUpload = true
|
|
||||||
},
|
|
||||||
updateBudget: function() {
|
updateBudget: function() {
|
||||||
document.querySelector('#to-target').innerText = this.totalBudgetStr
|
document.querySelector('#to-target').innerText = this.totalBudgetStr
|
||||||
},
|
},
|
||||||
|
27
js/components/levelofwarrant.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import textinput from './text_input'
|
||||||
|
import checkboxinput from './checkbox_input'
|
||||||
|
import FormMixin from '../mixins/form'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [FormMixin],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
textinput,
|
||||||
|
checkboxinput,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
initialData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const { unlimited_level_of_warrant = false } = this.initialData
|
||||||
|
|
||||||
|
return {
|
||||||
|
unlimited_level_of_warrant,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
@ -61,8 +61,14 @@ export default {
|
|||||||
|
|
||||||
props: {
|
props: {
|
||||||
members: Array,
|
members: Array,
|
||||||
role_choices: Array,
|
role_choices: {
|
||||||
status_choices: Array,
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
status_choices: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
@ -87,7 +93,7 @@ export default {
|
|||||||
displayName: 'Environments',
|
displayName: 'Environments',
|
||||||
attr: 'num_env',
|
attr: 'num_env',
|
||||||
sortFunc: numericSort,
|
sortFunc: numericSort,
|
||||||
class: 'table-cell--align-right',
|
class: 'table-cell--align-center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Status',
|
displayName: 'Status',
|
||||||
|
30
js/components/sidenav_toggler.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import ToggleMixin from '../mixins/toggle'
|
||||||
|
|
||||||
|
const cookieName = 'expandSidenav'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'sidenav-toggler',
|
||||||
|
|
||||||
|
mixins: [ToggleMixin],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
defaultVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: function() {
|
||||||
|
if (document.cookie.match(cookieName)) {
|
||||||
|
return !!document.cookie.match(cookieName + ' *= *true')
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggle: function(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.isVisible = !this.isVisible
|
||||||
|
document.cookie = cookieName + '=' + this.isVisible + '; path=/'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -84,6 +84,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onBlur: function(e) {
|
||||||
|
this._checkIfValid({ value: e.target.value, invalidate: true })
|
||||||
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
_checkIfValid: function({ value, invalidate = false }) {
|
_checkIfValid: function({ value, invalidate = false }) {
|
||||||
// Validate the value
|
// Validate the value
|
||||||
|
@ -1,32 +1,14 @@
|
|||||||
|
import ToggleMixin from '../mixins/toggle'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'toggler',
|
name: 'toggler',
|
||||||
|
|
||||||
|
mixins: [ToggleMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
defaultVisible: {
|
defaultVisible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: () => false,
|
default: () => false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
isVisible: this.defaultVisible,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function(createElement) {
|
|
||||||
return createElement(this.$vnode.data.tag, [
|
|
||||||
this.$scopedSlots.default({
|
|
||||||
isVisible: this.isVisible,
|
|
||||||
toggle: this.toggle,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
toggle: function(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.isVisible = !this.isVisible
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
41
js/components/upload_input.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import createNumberMask from 'text-mask-addons/dist/createNumberMask'
|
||||||
|
import { conformToMask } from 'vue-text-mask'
|
||||||
|
|
||||||
|
import FormMixin from '../mixins/form'
|
||||||
|
import textinput from './text_input'
|
||||||
|
import optionsinput from './options_input'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'uploadinput',
|
||||||
|
|
||||||
|
mixins: [FormMixin],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
textinput,
|
||||||
|
optionsinput,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
initialData: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
uploadErrors: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
const pdf = this.initialData
|
||||||
|
|
||||||
|
return {
|
||||||
|
showUpload: !pdf || this.uploadErrors.length > 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
showUploadInput: function() {
|
||||||
|
this.showUpload = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -6,6 +6,7 @@ import classes from '../styles/atat.scss'
|
|||||||
import Vue from 'vue/dist/vue'
|
import Vue from 'vue/dist/vue'
|
||||||
import VTooltip from 'v-tooltip'
|
import VTooltip from 'v-tooltip'
|
||||||
|
|
||||||
|
import levelofwarrant from './components/levelofwarrant'
|
||||||
import optionsinput from './components/options_input'
|
import optionsinput from './components/options_input'
|
||||||
import multicheckboxinput from './components/multi_checkbox_input'
|
import multicheckboxinput from './components/multi_checkbox_input'
|
||||||
import textinput from './components/text_input'
|
import textinput from './components/text_input'
|
||||||
@ -20,6 +21,7 @@ import NewApplication from './components/forms/new_application'
|
|||||||
import EditEnvironmentRole from './components/forms/edit_environment_role'
|
import EditEnvironmentRole from './components/forms/edit_environment_role'
|
||||||
import EditApplicationRoles from './components/forms/edit_application_roles'
|
import EditApplicationRoles from './components/forms/edit_application_roles'
|
||||||
import funding from './components/forms/funding'
|
import funding from './components/forms/funding'
|
||||||
|
import uploadinput from './components/upload_input'
|
||||||
import Modal from './mixins/modal'
|
import Modal from './mixins/modal'
|
||||||
import selector from './components/selector'
|
import selector from './components/selector'
|
||||||
import BudgetChart from './components/charts/budget_chart'
|
import BudgetChart from './components/charts/budget_chart'
|
||||||
@ -32,6 +34,7 @@ import RequestsList from './components/requests_list'
|
|||||||
import ConfirmationPopover from './components/confirmation_popover'
|
import ConfirmationPopover from './components/confirmation_popover'
|
||||||
import { isNotInVerticalViewport } from './lib/viewport'
|
import { isNotInVerticalViewport } from './lib/viewport'
|
||||||
import DateSelector from './components/date_selector'
|
import DateSelector from './components/date_selector'
|
||||||
|
import SidenavToggler from './components/sidenav_toggler'
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
@ -43,6 +46,7 @@ const app = new Vue({
|
|||||||
el: '#app-root',
|
el: '#app-root',
|
||||||
components: {
|
components: {
|
||||||
toggler,
|
toggler,
|
||||||
|
levelofwarrant,
|
||||||
optionsinput,
|
optionsinput,
|
||||||
multicheckboxinput,
|
multicheckboxinput,
|
||||||
textinput,
|
textinput,
|
||||||
@ -64,8 +68,10 @@ const app = new Vue({
|
|||||||
RequestsList,
|
RequestsList,
|
||||||
ConfirmationPopover,
|
ConfirmationPopover,
|
||||||
funding,
|
funding,
|
||||||
|
uploadinput,
|
||||||
DateSelector,
|
DateSelector,
|
||||||
EditOfficerForm,
|
EditOfficerForm,
|
||||||
|
SidenavToggler,
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function() {
|
mounted: function() {
|
||||||
|
@ -5,6 +5,10 @@ export const formatDollars = (value, cents = true) => {
|
|||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
})
|
})
|
||||||
} else if (typeof value === 'string') {
|
} else if (typeof value === 'string') {
|
||||||
|
if (value === '') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
return parseFloat(value).toLocaleString('us-US', {
|
return parseFloat(value).toLocaleString('us-US', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
|
23
js/mixins/toggle.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export default {
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
isVisible: this.defaultVisible,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(createElement) {
|
||||||
|
return createElement(this.$vnode.data.tag, [
|
||||||
|
this.$scopedSlots.default({
|
||||||
|
isVisible: this.isVisible,
|
||||||
|
toggle: this.toggle,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggle: function(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.isVisible = !this.isVisible
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
1
static/icons/angle-double-left-solid.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="angle-double-left" class="svg-inline--fa fa-angle-double-left fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M223.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L319.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L393.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34zm-192 34l136 136c9.4 9.4 24.6 9.4 33.9 0l22.6-22.6c9.4-9.4 9.4-24.6 0-33.9L127.9 256l96.4-96.4c9.4-9.4 9.4-24.6 0-33.9L201.7 103c-9.4-9.4-24.6-9.4-33.9 0l-136 136c-9.5 9.4-9.5 24.6-.1 34z"></path></svg>
|
After Width: | Height: | Size: 630 B |
1
static/icons/angle-double-right-solid.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="angle-double-right" class="svg-inline--fa fa-angle-double-right fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34zm192-34l-136-136c-9.4-9.4-24.6-9.4-33.9 0l-22.6 22.6c-9.4 9.4-9.4 24.6 0 33.9l96.4 96.4-96.4 96.4c-9.4 9.4-9.4 24.6 0 33.9l22.6 22.6c9.4 9.4 24.6 9.4 33.9 0l136-136c9.4-9.2 9.4-24.4 0-33.8z"></path></svg>
|
After Width: | Height: | Size: 634 B |
1
static/icons/chart-pie.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 544 512"><path d="M527.79 288H290.5l158.03 158.03c6.04 6.04 15.98 6.53 22.19.68 38.7-36.46 65.32-85.61 73.13-140.86 1.34-9.46-6.51-17.85-16.06-17.85zm-15.83-64.8C503.72 103.74 408.26 8.28 288.8.04 279.68-.59 272 7.1 272 16.24V240h223.77c9.14 0 16.82-7.68 16.19-16.8zM224 288V50.71c0-9.55-8.39-17.4-17.84-16.06C86.99 51.49-4.1 155.6.14 280.37 4.5 408.51 114.83 513.59 243.03 511.98c50.4-.63 96.97-16.87 135.26-44.03 7.9-5.6 8.42-17.23 1.57-24.08L224 288z"/></svg>
|
After Width: | Height: | Size: 515 B |
1
static/icons/cog.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z"/></svg>
|
After Width: | Height: | Size: 890 B |
1
static/icons/envelope.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M464 64H48C21.49 64 0 85.49 0 112v288c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V112c0-26.51-21.49-48-48-48zm0 48v40.805c-22.422 18.259-58.168 46.651-134.587 106.49-16.841 13.247-50.201 45.072-73.413 44.701-23.208.375-56.579-31.459-73.413-44.701C106.18 199.465 70.425 171.067 48 152.805V112h416zM48 400V214.398c22.914 18.251 55.409 43.862 104.938 82.646 21.857 17.205 60.134 55.186 103.062 54.955 42.717.231 80.509-37.199 103.053-54.947 49.528-38.783 82.032-64.401 104.947-82.653V400H48z"/></svg>
|
After Width: | Height: | Size: 574 B |
1
static/icons/home.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M280.37 148.26L96 300.11V464a16 16 0 0 0 16 16l112.06-.29a16 16 0 0 0 15.92-16V368a16 16 0 0 1 16-16h64a16 16 0 0 1 16 16v95.64a16 16 0 0 0 16 16.05L464 480a16 16 0 0 0 16-16V300L295.67 148.26a12.19 12.19 0 0 0-15.3 0zM571.6 251.47L488 182.56V44.05a12 12 0 0 0-12-12h-56a12 12 0 0 0-12 12v72.61L318.47 43a48 48 0 0 0-61 0L4.34 251.47a12 12 0 0 0-1.6 16.9l25.5 31A12 12 0 0 0 45.15 301l235.22-193.74a12.19 12.19 0 0 1 15.3 0L530.9 301a12 12 0 0 0 16.9-1.6l25.5-31a12 12 0 0 0-1.7-16.93z"/></svg>
|
After Width: | Height: | Size: 565 B |
1
static/icons/minus.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"/></svg>
|
After Width: | Height: | Size: 196 B |
@ -12,6 +12,7 @@
|
|||||||
@import 'elements/buttons';
|
@import 'elements/buttons';
|
||||||
@import 'elements/panels';
|
@import 'elements/panels';
|
||||||
@import 'elements/block_lists';
|
@import 'elements/block_lists';
|
||||||
|
@import 'elements/accordians';
|
||||||
@import 'elements/tables';
|
@import 'elements/tables';
|
||||||
@import 'elements/sidenav';
|
@import 'elements/sidenav';
|
||||||
@import 'elements/action_group';
|
@import 'elements/action_group';
|
||||||
@ -23,6 +24,7 @@
|
|||||||
@import 'elements/menu';
|
@import 'elements/menu';
|
||||||
|
|
||||||
@import 'components/topbar';
|
@import 'components/topbar';
|
||||||
|
@import 'components/top_message';
|
||||||
@import 'components/global_layout';
|
@import 'components/global_layout';
|
||||||
@import 'components/global_navigation';
|
@import 'components/global_navigation';
|
||||||
@import 'components/portfolio_layout';
|
@import 'components/portfolio_layout';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.app-footer {
|
.app-footer {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
border-top: 1px solid $color-black;
|
border-top: 1px solid $color-gray-lightest;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
.app-footer__info__link {
|
.app-footer__info__link {
|
||||||
margin: (-$gap * 2) (-$gap);
|
margin: (-$gap * 2) (-$gap);
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
.icon--footer {
|
.icon--footer {
|
||||||
@include icon-size(16);
|
@include icon-size(16);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#app-root {
|
#app-root {
|
||||||
background-color: $color-gray-lightest;
|
background-color: $color-offwhite;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
@ -2,29 +2,16 @@
|
|||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
|
|
||||||
.sidenav__link {
|
.sidenav__link {
|
||||||
padding-right: $gap;
|
padding-right: $gap * 2;
|
||||||
|
|
||||||
@include media($large-screen) {
|
@include media($large-screen) {
|
||||||
padding-right: $gap * 2;
|
padding-right: $gap * 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenav__link-label {
|
|
||||||
@include hide;
|
|
||||||
|
|
||||||
@include media($large-screen) {
|
|
||||||
@include unhide;
|
|
||||||
padding-left: $gap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.global-navigation__context--portfolio {
|
&.global-navigation__context--portfolio {
|
||||||
.sidenav__link {
|
.sidenav__link {
|
||||||
padding-right: $gap;
|
padding-right: $gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenav__link-label {
|
|
||||||
@include hide;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,53 +2,137 @@
|
|||||||
@include media($large-screen) {
|
@include media($large-screen) {
|
||||||
@include grid-row;
|
@include grid-row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
margin-left: 2 * $gap;
|
||||||
|
|
||||||
|
.line {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid $color-gray-lightest;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.portfolio-navigation {
|
.portfolio-breadcrumbs {
|
||||||
@include panel-margin;
|
margin-bottom: $gap * 2;
|
||||||
margin-bottom: $gap * 4;
|
color: $color-gray-medium;
|
||||||
|
font-size: $h5-font-size;
|
||||||
|
|
||||||
ul {
|
.icon-link {
|
||||||
display: flex;
|
color: $color-blue;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon--tiny {
|
||||||
|
padding: $gap 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
@include icon-color($color-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-breadcrumbs__home {
|
||||||
|
&.icon-link--disabled {
|
||||||
|
color: $color-gray-medium;
|
||||||
|
opacity: 1;
|
||||||
|
.icon {
|
||||||
|
@include icon-color($color-gray-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-breadcrumbs__crumb {
|
||||||
|
.icon {
|
||||||
|
@include icon-color($color-gray-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-link {
|
||||||
|
color: $color-gray-medium;
|
||||||
|
pointer-events: none;
|
||||||
|
&.icon-link--disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
li {
|
@include media($small-screen) {
|
||||||
flex-grow: 1;
|
flex-direction: row;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media($medium-screen) {
|
margin: 2 * $gap;
|
||||||
margin-bottom: $gap * 5;
|
|
||||||
|
.portfolio-header__name {
|
||||||
|
@include h1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media($large-screen) {
|
.portfolio-header__budget {
|
||||||
width: 20rem;
|
font-size: $small-font-size;
|
||||||
margin-right: $gap * 2;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-funding {
|
|
||||||
.portfolio-funding__header {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 $gap;
|
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.portfolio-funding__header--funded-through {
|
.icon-tooltip {
|
||||||
padding: 2 * $gap;
|
margin-left: -$gap / 2;
|
||||||
flex-grow: 1;
|
}
|
||||||
text-align: left;
|
|
||||||
|
button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-header__budget--dollars {
|
||||||
|
font-size: $h2-font-size;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
justify-content: center;
|
||||||
|
font-size: $small-font-size;
|
||||||
|
|
||||||
|
.icon-link {
|
||||||
|
&.active {
|
||||||
|
color: $color-gray;
|
||||||
|
.icon {
|
||||||
|
@include icon-color($color-gray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-link--icon {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
@include icon-size(30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-left {
|
||||||
|
width: 12.5rem;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-right {
|
||||||
|
margin-left: -.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-header__budget--amount {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-header__budget--cents {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-top: .75rem;
|
||||||
|
margin-left: -.7rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.funded {
|
.portfolio-funding__header--funded-through {
|
||||||
color: $color-green;
|
flex-grow: 1;
|
||||||
.icon {
|
text-align: left;
|
||||||
@include icon-color($color-green);
|
font-weight: bold;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.unfunded {
|
.unfunded {
|
||||||
@ -59,11 +143,156 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin subheading {
|
||||||
|
color: $color-gray-dark;
|
||||||
|
padding: $gap 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.54;
|
||||||
|
font-size: $small-font-size;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 3 * $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-content {
|
||||||
|
margin: 6 * $gap $gap 0 $gap;
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
.panel {
|
||||||
|
@include shadow-panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
box-shadow: 0 6px 18px 0 rgba(144,164,183,0.3);
|
||||||
|
thead {
|
||||||
|
th:first-child {
|
||||||
|
padding-left: 3 * $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: $color-gray-lightest;
|
||||||
|
padding: $gap 2 * $gap;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
color: $color-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-bottom: 1px solid $color-gray-lightest;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-member-link {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-content {
|
||||||
|
.subheading {
|
||||||
|
@include subheading;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
@include shadow-panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-list-item {
|
||||||
|
ul {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.block-list__footer {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.application-edit__env-list-item {
|
||||||
|
label {
|
||||||
|
color: $color-black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-log {
|
||||||
|
border-top: 3px solid $color-blue;
|
||||||
|
|
||||||
|
.subheading {
|
||||||
|
border-top: 0;
|
||||||
|
border-bottom: 1px solid $color-gray-lightest;
|
||||||
|
padding: 1.6rem 1.6rem;
|
||||||
|
font-weight: $font-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-applications {
|
||||||
|
.portfolio-applications__header {
|
||||||
|
|
||||||
|
.portfolio-applications__header--title {
|
||||||
|
@include subheading;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-applications__header--actions {
|
||||||
|
color: $color-blue;
|
||||||
|
font-size: $small-font-size;
|
||||||
|
.icon {
|
||||||
|
@include icon-color($color-blue);
|
||||||
|
@include icon-size(14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-list {
|
||||||
|
.toggle-link {
|
||||||
|
background-color: $color-blue-light;
|
||||||
|
.icon {
|
||||||
|
margin: $gap / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-list-item {
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 4px 8px 1px rgba(230,230,230,0.5), -4px 4px 8px 1px rgba(230,230,230,0.5);
|
||||||
|
margin-bottom: 6 * $gap;
|
||||||
|
|
||||||
|
.col {
|
||||||
|
max-width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-list-item__environment__name {
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-list-item__environment__csp_link {
|
||||||
|
font-size: $small-font-size;
|
||||||
|
font-weight: normal;
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-aqua-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-funding {
|
||||||
|
.panel {
|
||||||
|
@include shadow-panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subheading {
|
||||||
|
@include subheading;
|
||||||
|
margin-top: 6 * $gap;
|
||||||
|
margin-bottom: 2 * $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-funding__header {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
.pending-task-order {
|
.pending-task-order {
|
||||||
background-color: $color-gold-lightest;
|
background-color: $color-gold-lightest;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin-bottom: 2 * $gap;
|
||||||
padding: 2 * $gap;
|
padding: 2 * $gap;
|
||||||
|
|
||||||
dt {
|
dt {
|
||||||
@ -96,34 +325,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.portfolio-total-balance {
|
.total-balance {
|
||||||
margin-top: -$gap;
|
margin-right: 2 * $gap;
|
||||||
margin-bottom: 3rem;
|
text-align: right;
|
||||||
|
|
||||||
.row {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
margin: 2 * $gap 0;
|
|
||||||
padding-right: 14rem;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
margin: 0 2 * $gap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.responsive-table-wrapper {
|
||||||
|
margin: 0 (-2 * $gap);
|
||||||
|
padding: 2 * $gap;
|
||||||
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
box-shadow: 0 6px 18px 0 rgba(144,164,183,0.3);
|
||||||
|
thead {
|
||||||
|
th:first-child {
|
||||||
|
padding-left: 3 * $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
|
background-color: $color-gray-lightest;
|
||||||
|
padding: $gap 2 * $gap;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
color: $color-gray;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.period-of-performance {
|
|
||||||
color: $color-blue;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
@include icon-color($color-primary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-bottom: 1px solid $color-gray-lightest;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.unused-balance {
|
td.unused-balance {
|
||||||
@ -146,7 +380,6 @@
|
|||||||
&.funded .to-expiration-alert {
|
&.funded .to-expiration-alert {
|
||||||
color: $color-blue;
|
color: $color-blue;
|
||||||
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@include icon-color($color-blue);
|
@include icon-color($color-blue);
|
||||||
}
|
}
|
||||||
@ -169,3 +402,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.portfolio-reports {
|
||||||
|
.portfolio-reports__header {
|
||||||
|
margin-bottom: 4 * $gap;
|
||||||
|
|
||||||
|
.portfolio-reports__header--title {
|
||||||
|
@include subheading;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
@include shadow-panel;
|
||||||
|
margin-bottom: 4 * $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-admin {
|
||||||
|
.edit-portfolio-name.action-group {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.form-col {
|
||||||
|
.usa-input--validation--portfolioName {
|
||||||
|
input {
|
||||||
|
max-width: 30em;
|
||||||
|
}
|
||||||
|
.icon-validation {
|
||||||
|
left: 30em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,6 +6,9 @@
|
|||||||
padding: $gap;
|
padding: $gap;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
@media (min-width:1000px) {
|
@media (min-width:1000px) {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
35
styles/components/_top_message.scss
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
.top-message {
|
||||||
|
padding: $gap * 2;
|
||||||
|
|
||||||
|
@include panel-margin;
|
||||||
|
@include media($medium-screen) {
|
||||||
|
padding: $gap * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-top-style: solid;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: $color-gray-light;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding-top: $gap * 2;
|
||||||
|
padding-bottom: $gap * 2;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-title {
|
||||||
|
padding-top: $gap * 4;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
padding-left: $gap * 2.5;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
@ -43,6 +43,7 @@ $font-bold: 700;
|
|||||||
$color-blue: #0071bc;
|
$color-blue: #0071bc;
|
||||||
$color-blue-darker: #205493;
|
$color-blue-darker: #205493;
|
||||||
$color-blue-darkest: #112e51;
|
$color-blue-darkest: #112e51;
|
||||||
|
$color-blue-light: #e5f1ff;
|
||||||
|
|
||||||
$color-aqua: #02bfe7;
|
$color-aqua: #02bfe7;
|
||||||
$color-aqua-dark: #00a6d2;
|
$color-aqua-dark: #00a6d2;
|
||||||
@ -57,12 +58,13 @@ $color-red-light: #e59393;
|
|||||||
$color-red-lightest: #f9dede;
|
$color-red-lightest: #f9dede;
|
||||||
|
|
||||||
$color-white: #ffffff;
|
$color-white: #ffffff;
|
||||||
|
$color-offwhite: #fbfbfd;
|
||||||
$color-black: #000000;
|
$color-black: #000000;
|
||||||
$color-black-light: #212121;
|
$color-black-light: #212121;
|
||||||
|
|
||||||
$color-gray-dark: #323a45;
|
$color-gray-dark: #323a45;
|
||||||
$color-gray: #5b616b;
|
$color-gray: #5b616b;
|
||||||
$color-gray-medium: #757575;
|
$color-gray-medium: #9b9b9b;
|
||||||
$color-gray-light: #aeb0b5;
|
$color-gray-light: #aeb0b5;
|
||||||
$color-gray-lighter: #d6d7d9;
|
$color-gray-lighter: #d6d7d9;
|
||||||
$color-gray-lightest: #f1f1f1;
|
$color-gray-lightest: #f1f1f1;
|
||||||
@ -83,7 +85,7 @@ $color-green-lighter: #94bfa2;
|
|||||||
$color-green-lightest: #e7f4e4;
|
$color-green-lightest: #e7f4e4;
|
||||||
|
|
||||||
$color-cool-blue: #205493;
|
$color-cool-blue: #205493;
|
||||||
$color-cool-blue-light: #4773aa;
|
$color-cool-blue-light: #4190e2;
|
||||||
$color-cool-blue-lighter: #8ba6ca;
|
$color-cool-blue-lighter: #8ba6ca;
|
||||||
$color-cool-blue-lightest: #dce4ef;
|
$color-cool-blue-lightest: #dce4ef;
|
||||||
|
|
||||||
|
123
styles/elements/_accordians.scss
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
.accordian {
|
||||||
|
@include block-list;
|
||||||
|
|
||||||
|
box-shadow: 0 4px 10px 0 rgba(193,193,193,0.5);
|
||||||
|
margin-bottom: 6 * $gap;
|
||||||
|
|
||||||
|
.icon-link {
|
||||||
|
margin: -$gap 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-link,
|
||||||
|
.label {
|
||||||
|
&:first-child {
|
||||||
|
margin-left: -$gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: -$gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordian__header {
|
||||||
|
@include block-list-header;
|
||||||
|
border-top: 3px solid $color-blue;
|
||||||
|
border-bottom: none;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(216,218,222,0.58);
|
||||||
|
|
||||||
|
&.row {
|
||||||
|
background: $color-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordian__title {
|
||||||
|
@include block-list__title;
|
||||||
|
color: $color-blue;
|
||||||
|
@include h3;
|
||||||
|
|
||||||
|
&.icon-link {
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
padding: 0 $gap;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordian__description {
|
||||||
|
@include block-list__description;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: $small-font-size;
|
||||||
|
color: $color-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordian__actions {
|
||||||
|
margin-top: $gap;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.icon-link {
|
||||||
|
font-size: $small-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
background-color: $color-cool-blue-light;
|
||||||
|
color: $color-white;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: $gap / 2 $gap;
|
||||||
|
margin-left: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
border: 1px solid $color-gray-medium;
|
||||||
|
opacity: 0.75;
|
||||||
|
margin: 0 .5 * $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordian__item {
|
||||||
|
@include block-list-item;
|
||||||
|
|
||||||
|
opacity: 0.75;
|
||||||
|
background-color: $color-blue-light;
|
||||||
|
border-bottom: 1px solid rgba($color-gray-light, 0.5);
|
||||||
|
|
||||||
|
&.accordian__item--selectable {
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
@include ie-only {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> label {
|
||||||
|
@include block-list-selectable-label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> label {
|
||||||
|
@include block-list-selectable-label;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked {
|
||||||
|
+ label {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include ie-only {
|
||||||
|
dl {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: $gap*4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordian__footer {
|
||||||
|
@include block-list__footer;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
@mixin block-list {
|
@mixin block-list {
|
||||||
@include panel-margin;
|
@include panel-margin;
|
||||||
|
@include shadow-panel
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
ul, dl {
|
ul, dl {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@ -15,6 +17,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
background-color: $color-gray-lightest;
|
||||||
|
padding: $gap 2 * $gap;
|
||||||
|
color: $color-gray;
|
||||||
|
|
||||||
.icon-tooltip {
|
.icon-tooltip {
|
||||||
margin: -$gap;
|
margin: -$gap;
|
||||||
@ -59,7 +64,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: $gap * 2;
|
padding: $gap * 2;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
border-bottom: 1px dashed $color-gray-light;
|
border-bottom: 1px solid $color-gray-lightest;
|
||||||
|
|
||||||
@at-root li#{&} {
|
@at-root li#{&} {
|
||||||
&:last-child {
|
&:last-child {
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
.icon-link {
|
.icon-link {
|
||||||
@include icon-link;
|
@include icon-link;
|
||||||
@include icon-link-color($color-primary);
|
@include icon-link-color($color-primary);
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
&.icon-link--vertical {
|
&.icon-link--vertical {
|
||||||
@include icon-link-vertical;
|
@include icon-link-vertical;
|
||||||
@ -67,6 +68,7 @@
|
|||||||
&.icon-link--disabled {
|
&.icon-link--disabled {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.icon-link--left {
|
&.icon-link--left {
|
||||||
|
@ -67,7 +67,24 @@
|
|||||||
@include icon-color($color-gray);
|
@include icon-color($color-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.icon--blue {
|
||||||
|
@include icon-color($color-blue-darker);
|
||||||
|
}
|
||||||
|
|
||||||
&.icon--medium {
|
&.icon--medium {
|
||||||
@include icon-size(12);
|
@include icon-size(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.icon--gold {
|
||||||
|
@include icon-color($color-gold-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon--circle {
|
||||||
|
svg {
|
||||||
|
border-radius: 100%;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
@mixin input-state($state) {
|
@mixin input-state($state) {
|
||||||
$border-width: 1px;
|
$border-width: 1px;
|
||||||
$state-color: $color-gray;
|
$state-color: $color-blue;
|
||||||
|
|
||||||
@if $state == 'error' {
|
@if $state == 'error' {
|
||||||
$border-width: 2px;
|
$border-width: 2px;
|
||||||
@ -283,6 +283,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include input-state('default');
|
||||||
|
|
||||||
&.usa-input--error {
|
&.usa-input--error {
|
||||||
@include input-state('error');
|
@include input-state('error');
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,13 @@
|
|||||||
padding: $gap;
|
padding: $gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin shadow-panel {
|
||||||
|
padding: $gap / 2 0;
|
||||||
|
box-shadow: 0 6px 18px 0 rgba(144,164,183,0.3);
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
@include panel-base;
|
@include panel-base;
|
||||||
@include panel-theme-default;
|
@include panel-theme-default;
|
||||||
|
@ -1,21 +1,40 @@
|
|||||||
.sidenav {
|
@mixin sidenav__header {
|
||||||
@include hide;
|
padding: $gap ($gap * 2);
|
||||||
|
font-size: $small-font-size;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.global-navigation.sidenav {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav {
|
||||||
@include media($large-screen) {
|
@include media($large-screen) {
|
||||||
@include unhide;
|
|
||||||
width: 25rem;
|
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
width: 25rem;
|
||||||
|
|
||||||
box-shadow: 0 6px 18px 0 rgba(48,58,65,0.15);
|
box-shadow: 0 6px 18px 0 rgba(48,58,65,0.15);
|
||||||
|
|
||||||
.sidenav__title {
|
.sidenav__title {
|
||||||
color: $color-gray-dark;
|
@include sidenav__header;
|
||||||
padding: $gap ($gap * 2);
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
width: 50%;
|
||||||
|
color: $color-gray-dark;
|
||||||
opacity: 0.54;
|
opacity: 0.54;
|
||||||
font-size: $small-font-size;
|
}
|
||||||
font-weight: bold;
|
|
||||||
|
.sidenav__toggle {
|
||||||
|
@include sidenav__header;
|
||||||
|
float: right;
|
||||||
|
color: $color-blue-darker;
|
||||||
|
|
||||||
|
.toggle-arrows {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
@ -41,11 +60,17 @@
|
|||||||
margin-bottom: $gap;
|
margin-bottom: $gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidenav__text {
|
||||||
|
margin: 2 * $gap;
|
||||||
|
color: $color-gray;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.sidenav__link {
|
.sidenav__link {
|
||||||
display: block;
|
display: block;
|
||||||
padding: $gap ($gap * 2);
|
padding: $gap ($gap * 2);
|
||||||
color: $color-black;
|
color: $color-black;
|
||||||
text-decoration: none;
|
text-decoration: underline;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -112,7 +137,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
+ ul {
|
+ ul {
|
||||||
// padding-bottom: $gap / 2;
|
|
||||||
|
|
||||||
li {
|
li {
|
||||||
.sidenav__link {
|
.sidenav__link {
|
||||||
@ -151,10 +175,7 @@
|
|||||||
.sidenav--minimized {
|
.sidenav--minimized {
|
||||||
@extend .sidenav;
|
@extend .sidenav;
|
||||||
|
|
||||||
@include unhide;
|
width: auto;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
@include media($large-screen) {
|
|
||||||
@include hide;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,10 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.table-cell--align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
&.table-cell--shrink {
|
&.table-cell--shrink {
|
||||||
width: 1%;
|
width: 1%;
|
||||||
}
|
}
|
||||||
@ -32,9 +36,20 @@
|
|||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
tr th {
|
||||||
.sorting-direction {
|
.sorting-direction {
|
||||||
position: absolute;
|
position: inherit;
|
||||||
|
margin-right: -16px;
|
||||||
|
width: 16px;
|
||||||
|
.icon {
|
||||||
|
height: 14px;
|
||||||
|
width: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,4 +23,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header.accordian__header {
|
||||||
|
padding: 1.6rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,37 @@
|
|||||||
|
.member-edit {
|
||||||
|
.panel {
|
||||||
|
@include shadow-panel;
|
||||||
|
margin: $gap;
|
||||||
|
padding: 2 * $gap $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subheading {
|
||||||
|
@include subheading;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-access {
|
||||||
|
padding: 2 * $gap;
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: $small-font-size;
|
||||||
|
color: $color-gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-list-item {
|
||||||
|
margin: 2 * $gap 3 * $gap;
|
||||||
|
|
||||||
|
.block-list__header {
|
||||||
|
border-top-color: $color-gray-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
margin: 2 * $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.member-card {
|
.member-card {
|
||||||
@include grid-row;
|
@include grid-row;
|
||||||
padding: $gap*2;
|
padding: $gap*2;
|
||||||
|
@ -7,6 +7,11 @@
|
|||||||
|
|
||||||
.funding-summary-row__col {
|
.funding-summary-row__col {
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 2 * $gap 0;
|
||||||
|
border-bottom: 1px solid $color-gray-lightest;
|
||||||
|
}
|
||||||
|
|
||||||
@include media($medium-screen) {
|
@include media($medium-screen) {
|
||||||
@include grid-pad;
|
@include grid-pad;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@ -36,6 +41,11 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subheading {
|
||||||
|
@include h4;
|
||||||
|
margin: 0 $gap 2 * $gap 0;
|
||||||
|
-ms-flex-negative: 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Spending Summary
|
// Spending Summary
|
||||||
// ===============================
|
// ===============================
|
||||||
@ -53,40 +63,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spend-summary__heading {
|
|
||||||
@include h3;
|
|
||||||
margin: 0 $gap 0 0;
|
|
||||||
-ms-flex-negative: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spend-summary__budget {
|
.spend-summary__budget {
|
||||||
margin: 0 0 0 $gap;
|
|
||||||
|
|
||||||
@include ie-only {
|
@include ie-only {
|
||||||
margin: $gap 0 0 0;
|
margin: $gap 0 0 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> div {
|
dl {
|
||||||
text-align: right;
|
text-align: left;
|
||||||
margin: 0 0 ($gap / 2) 0;
|
margin: 0 0 ($gap / 2) 0;
|
||||||
|
|
||||||
@include ie-only {
|
@include ie-only {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
dd, dt {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt {
|
dt {
|
||||||
color: $color-gray;
|
text-transform: uppercase;
|
||||||
|
color: $color-gray-light;
|
||||||
margin-right: $gap;
|
margin-right: $gap;
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd {
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
font-size: $small-font-size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,32 +94,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spend-summary__spent {
|
.spend-summary__spent {
|
||||||
margin: $gap 0 0 0;
|
margin: 2 * $gap 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
dd, dt {
|
|
||||||
@include h5;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt {
|
dt {
|
||||||
font-weight: normal;
|
letter-spacing: 0.47px;
|
||||||
margin-left: $gap;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Task Order Summary
|
// Task Order Summary
|
||||||
// ===============================
|
// ===============================
|
||||||
&.to-summary {
|
&.to-summary {
|
||||||
|
|
||||||
.to-summary__row {
|
.icon-link {
|
||||||
|
font-weight: $font-normal
|
||||||
|
}
|
||||||
|
|
||||||
|
.subheading {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.to-summary__heading {
|
.to-summary__heading {
|
||||||
@include h3;
|
@include h4;
|
||||||
margin: 0;
|
margin: 0 $gap 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.to-summary__to-number {
|
.to-summary__to-number {
|
||||||
@ -134,7 +132,6 @@
|
|||||||
margin-right: $gap;
|
margin-right: $gap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@include media($xlarge-screen) {
|
@include media($xlarge-screen) {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -163,23 +160,26 @@
|
|||||||
.to-summary__expiration {
|
.to-summary__expiration {
|
||||||
|
|
||||||
dl {
|
dl {
|
||||||
margin: ($gap * 2) 0 0 0;
|
text-align: right;
|
||||||
|
margin-top: -2 * $gap;
|
||||||
> div {
|
|
||||||
margin: 0 0 ($gap / 2) 0;
|
|
||||||
|
|
||||||
dd, dt {
|
dd, dt {
|
||||||
display: block;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
dt {
|
dt {
|
||||||
color: $color-gray;
|
font-size: $small-font-size;
|
||||||
margin-right: $gap;
|
text-transform: uppercase;
|
||||||
font-weight: normal;
|
font-weight: $font-bold;
|
||||||
|
color: $color-gray-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
dd {
|
dd.ending-soon {
|
||||||
font-weight: bold;
|
font-size: $h2-font-size;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
@include icon-size(28);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,9 +203,12 @@
|
|||||||
|
|
||||||
|
|
||||||
.spend-table {
|
.spend-table {
|
||||||
|
box-shadow: 0 6px 18px 0 rgba(144,164,183,0.3);
|
||||||
|
|
||||||
.spend-table__header {
|
.spend-table__header {
|
||||||
@include panel-base;
|
@include panel-base;
|
||||||
@include panel-theme-default;
|
@include panel-theme-default;
|
||||||
|
border-top: none;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -215,8 +218,8 @@
|
|||||||
padding: $gap * 2;
|
padding: $gap * 2;
|
||||||
|
|
||||||
.spend-table__title {
|
.spend-table__title {
|
||||||
@include h3;
|
@include h4;
|
||||||
margin: 0;
|
font-size: $lead-font-size;
|
||||||
flex: 2;
|
flex: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,6 +230,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
thead th {
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 1px solid $color-gray-lightest;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
@ -234,10 +243,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.current-month {
|
|
||||||
background-color: $color-aqua-lightest;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.previous-month {
|
&.previous-month {
|
||||||
color: $color-gray;
|
color: $color-gray;
|
||||||
}
|
}
|
||||||
@ -286,28 +291,53 @@
|
|||||||
.spend-table__portfolio {
|
.spend-table__portfolio {
|
||||||
th, td {
|
th, td {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
border-bottom: 1px solid $color-gray-lightest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spend-table__application {
|
.spend-table__application {
|
||||||
.spend-table__application__toggler {
|
.spend-table__application__toggler {
|
||||||
@include icon-link-color($color-black-light, $color-gray-lightest);
|
@include icon-link-color($color-blue, $color-gray-lightest);
|
||||||
margin-left: -$gap;
|
margin-left: -$gap;
|
||||||
|
color: $color-blue;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@include icon-size(12);
|
@include icon-size(12);
|
||||||
margin-right: $gap;
|
margin-right: $gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.open-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 5 * $gap;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 10px solid transparent;
|
||||||
|
border-right: 10px solid transparent;
|
||||||
|
|
||||||
|
border-bottom: 10px solid $color-blue-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
th[scope=rowgroup] {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spend-table__application__env {
|
.spend-table__application__env {
|
||||||
margin-left: $gap;
|
margin-left: 2 * $gap;
|
||||||
|
|
||||||
&:last-child {
|
th, td {
|
||||||
td, th {
|
.icon-link {
|
||||||
padding-bottom: $gap * 5;
|
font-weight: $font-normal;
|
||||||
box-shadow: inset 0 (-$gap * 2.5) 0 $color-gray-lightest;
|
font-size: $base-font-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
border-bottom: 1px dashed $color-white;
|
||||||
|
background-color: $color-blue-light;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,11 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4rem 6rem;
|
padding: 4rem 6rem;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
@include shadow-panel;
|
||||||
|
margin-bottom: 6 * $gap;
|
||||||
|
}
|
||||||
|
|
||||||
.task-order-get-started__list {
|
.task-order-get-started__list {
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@ -49,6 +54,10 @@
|
|||||||
top: 2.5rem;
|
top: 2.5rem;
|
||||||
margin-left: -23rem;
|
margin-left: -23rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-button {
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@ -58,6 +67,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-order-summary {
|
.task-order-summary {
|
||||||
|
.panel {
|
||||||
|
@include shadow-panel;
|
||||||
|
}
|
||||||
|
|
||||||
.alert .alert__actions {
|
.alert .alert__actions {
|
||||||
margin-top: 2 * $gap;
|
margin-top: 2 * $gap;
|
||||||
@ -67,7 +79,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label--pending {
|
.label--pending, .label--started {
|
||||||
background-color: $color-gold;
|
background-color: $color-gold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +124,11 @@
|
|||||||
|
|
||||||
.task-order-next-steps {
|
.task-order-next-steps {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@include media($xlarge-screen) {
|
@include media($xlarge-screen) {
|
||||||
padding-right: $gap;
|
padding-right: $gap;
|
||||||
}
|
}
|
||||||
@ -135,8 +152,17 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-top: 3 * $gap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 2 * $gap;
|
||||||
|
|
||||||
|
.alert__message {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.task-order-next-steps__icon {
|
.task-order-next-steps__icon {
|
||||||
width: 8%;
|
|
||||||
padding: $gap $gap 0 0;
|
padding: $gap $gap 0 0;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
.complete {
|
.complete {
|
||||||
@ -147,34 +173,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-order-next-steps__text {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-order-next-steps__action {
|
.task-order-next-steps__action {
|
||||||
|
min-width: 10 * $gap;
|
||||||
padding: $gap 0 0 $gap;
|
padding: $gap 0 0 $gap;
|
||||||
width: 32%;
|
|
||||||
a.usa-button {
|
a.usa-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-order-next-steps__text {
|
||||||
|
display: flex;
|
||||||
.task-order-next-steps__heading {
|
.task-order-next-steps__heading {
|
||||||
|
display: block;
|
||||||
h4 {
|
max-width: 100%;
|
||||||
@include ie-only {
|
flex-shrink: 1;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
margin: $gap $gap 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task-order-next-steps__description {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-order-sidebar {
|
.task-order-sidebar {
|
||||||
|
@include media($xlarge-screen) {
|
||||||
|
padding-left: 3 * $gap;
|
||||||
|
}
|
||||||
min-width: 35rem;
|
min-width: 35rem;
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@ -193,7 +214,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-order-invitations {
|
||||||
|
.task-order-invitations__heading {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.task-order-invitation-status {
|
.task-order-invitation-status {
|
||||||
|
margin-bottom: 3 * $gap;
|
||||||
|
.task-order-invitation-status__title {
|
||||||
|
font-weight: $font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
.invited {
|
.invited {
|
||||||
color: $color-green;
|
color: $color-green;
|
||||||
@include icon-color($color-green);
|
@include icon-color($color-green);
|
||||||
@ -207,6 +238,11 @@
|
|||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-order-invitation-details {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-order-form {
|
.task-order-form {
|
||||||
@ -230,14 +266,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-order-invite-message {
|
.task-order-invite-message {
|
||||||
|
font-weight: $font-bold;
|
||||||
|
|
||||||
&.not-sent {
|
&.not-sent {
|
||||||
color: $color-red;
|
color: $color-red;
|
||||||
font-weight: $font-bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sent {
|
&.sent {
|
||||||
color: $color-green;
|
color: $color-green;
|
||||||
font-weight: $font-bold;
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
color: $color-gold-dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,6 +397,48 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.officer__form {
|
.officer__form {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background-color: $color-aqua-lightest;
|
||||||
|
border-left-color: $color-blue;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-width: $gap / 2;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
|
.edit-officer {
|
||||||
|
margin-bottom: $gap * 2;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
color: $color-gray;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-input__title {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
margin-bottom: 0rem;
|
||||||
|
margin-top: 0rem;
|
||||||
|
margin-right: 2rem;
|
||||||
|
|
||||||
|
.usa-input {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.officer__form--dodId {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
|
.usa-input {
|
||||||
|
margin-bottom: 0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.officer__form--actions {
|
.officer__form--actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
{% macro CheckboxInput(field, inline=False, classes="") -%}
|
{% macro CheckboxInput(
|
||||||
|
field,
|
||||||
|
label=field.label | striptags,
|
||||||
|
inline=False,
|
||||||
|
classes="") -%}
|
||||||
<checkboxinput name='{{ field.name }}' inline-template key='{{ field.name }}'>
|
<checkboxinput name='{{ field.name }}' inline-template key='{{ field.name }}'>
|
||||||
<div class='usa-input {{ classes }} {% if field.errors %}usa-input--error{% endif %}'>
|
<div class='usa-input {{ classes }} {% if field.errors %}usa-input--error{% endif %}'>
|
||||||
|
|
||||||
<fieldset v-on:change="onInput" class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
<fieldset v-on:change="onInput" class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
||||||
<legend>
|
<legend>
|
||||||
{{ field() }}
|
{{ field() }}
|
||||||
{{ field.label }}
|
<label for={{field.name}}>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
|
||||||
{% if field.description %}
|
{% if field.description %}
|
||||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||||
|
@ -64,6 +64,7 @@
|
|||||||
|
|
||||||
<masked-input
|
<masked-input
|
||||||
v-on:input='onInput'
|
v-on:input='onInput'
|
||||||
|
v-on:blur='onBlur'
|
||||||
v-on:change='onChange'
|
v-on:change='onChange'
|
||||||
v-bind:value='value'
|
v-bind:value='value'
|
||||||
v-bind:mask='mask'
|
v-bind:mask='mask'
|
||||||
@ -87,6 +88,9 @@
|
|||||||
<template v-if='showError'>
|
<template v-if='showError'>
|
||||||
<span class='usa-input__message' v-html='validationError'></span>
|
<span class='usa-input__message' v-html='validationError'></span>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class='usa-input__message'></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</textinput>
|
</textinput>
|
||||||
|
24
templates/components/upload_input.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% macro UploadInput(field, show_label=False) -%}
|
||||||
|
<uploadinput inline-template v-bind:initial-data='{{ field.data | tojson }}' v-bind:upload-errors='{{ field.errors | list }}'>
|
||||||
|
<div>
|
||||||
|
<template v-if="showUpload">
|
||||||
|
<div class="usa-input {% if field.errors %} usa-input--error {% endif %}">
|
||||||
|
{% if show_label %}
|
||||||
|
{{ field.label }}
|
||||||
|
{% endif %}
|
||||||
|
{{ field.description }}
|
||||||
|
{{ field }}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<span class="usa-input__message">{{error}}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p>Uploaded {{ field.data.filename }}</p>
|
||||||
|
<div>
|
||||||
|
<button type="button" v-on:click="showUploadInput">Change</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</uploadinput>
|
||||||
|
{%- endmacro %}
|
@ -1,10 +1,7 @@
|
|||||||
{% from "components/pagination.html" import Pagination %}
|
{% from "components/pagination.html" import Pagination %}
|
||||||
|
|
||||||
<section class="block-list">
|
<section class="block-list activity-log">
|
||||||
<header class="block-list__header">
|
<div class='subheading'>{{ "portfolios.admin.activity_log_title" | translate }}</div>
|
||||||
<h1 class="block-list__title">{{ "audit_log.header_title" | translate }}</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for event in audit_events %}
|
{% for event in audit_events %}
|
||||||
<li class="block-list__item">
|
<li class="block-list__item">
|
||||||
|
@ -1,18 +1,8 @@
|
|||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
|
||||||
{% set title_text = ('fragments.edit_application_form.existing_application_title' | translate({ "application_name": application.name })) if application else ('fragments.edit_application_form.new_application_title' | translate) %}
|
|
||||||
|
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<div class="panel">
|
|
||||||
<div class="panel__heading panel__heading--grow">
|
|
||||||
<h1>{{ title_text }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel__content">
|
|
||||||
<p>
|
<p>
|
||||||
{{ "fragments.edit_application_form.explain" | translate }}
|
{{ "fragments.edit_application_form.explain" | translate }}
|
||||||
</p>
|
</p>
|
||||||
{{ TextInput(form.name) }}
|
{{ TextInput(form.name) }}
|
||||||
{{ TextInput(form.description, paragraph=True) }}
|
{{ TextInput(form.description, paragraph=True) }}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
<br>
|
|
||||||
<p>{{ "fragments.ko_review_alert.make_sure" | translate }}:</p>
|
|
||||||
<ul>
|
|
||||||
<li>{{ "fragments.ko_review_alert.bullet_1" | translate }}</li>
|
|
||||||
<li>{{ "fragments.ko_review_alert.bullet_2" | translate }}</li>
|
|
||||||
<li>{{ "fragments.ko_review_alert.bullet_3" | translate }}</li>
|
|
||||||
</ul>
|
|
7
templates/fragments/ko_review_message.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{{ "task_orders.ko_review.message" | translate }}
|
||||||
|
<div class="list-title">{{ "fragments.ko_review_message.title" | translate }}:</div>
|
||||||
|
<ul class="list">
|
||||||
|
<li class="list-item">{{ "fragments.ko_review_message.bullet_1" | translate }}</li>
|
||||||
|
<li class="list-item">{{ "fragments.ko_review_message.bullet_2" | translate }}</li>
|
||||||
|
<li class="list-item">{{ "fragments.ko_review_message.bullet_3" | translate }}</li>
|
||||||
|
</ul>
|
@ -8,7 +8,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<br/>
|
<br/>
|
||||||
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left icon-link--disabled' aria-disabled="true">{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a>
|
<a class='icon-link icon-link--left icon-link--disabled' aria-disabled="true">{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a>
|
||||||
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_uploaded"| translate }}</span>
|
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_uploaded"| translate }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
{% macro ReviewOfficerInfo(heading, first_name, last_name, email, phone_number, dod_id, officer) %}
|
{% macro ReviewOfficerInfo(heading, officer_data, has_officer, invite_pending) %}
|
||||||
<div class="col col--grow">
|
<div class="col col--grow">
|
||||||
<h4 class='task-order-form__heading'>{{ heading | translate }}</h4>
|
<h4 class='task-order-form__heading'>{{ heading | translate }}</h4>
|
||||||
{{ first_name }} {{ last_name }}<br>
|
{{ officer_data.first_name }} {{ officer_data.last_name }}<br>
|
||||||
{{ email }}<br>
|
{{ officer_data.email }}<br>
|
||||||
{% if phone_number %}
|
{% if officer_data.phone_number %}
|
||||||
{{ phone_number | usPhone }}
|
{{ officer_data.phone_number | usPhone }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<br>
|
||||||
{{ "task_orders.new.review.dod_id" | translate }} {{ dod_id}}<br>
|
{{ "task_orders.new.review.dod_id" | translate }} {{ officer_data.dod_id}}<br>
|
||||||
{% if officer %}
|
{% if has_officer %}
|
||||||
{{ Icon('ok', classes='icon--green') }} <span class="task-order-invite-message sent">{{ "task_orders.new.review.invited"| translate }}</<span>
|
{{ Icon('ok', classes='icon--green') }} <span class="task-order-invite-message sent">{{ "task_orders.new.review.invited"| translate }}</<span>
|
||||||
|
{% elif invite_pending %}
|
||||||
|
{{ Icon('alert', classes='icon--gold') }} <span class="task-order-invite-message pending">{{ "task_orders.new.review.pending_to"| translate }}</<span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_invited"| translate }}</span>
|
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_invited"| translate }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -17,9 +19,24 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ ReviewOfficerInfo("task_orders.new.review.ko", task_order.ko_first_name, task_order.ko_last_name, task_order.ko_email, task_order.ko_phone_number, task_order.ko_dod_id, task_order.contracting_officer) }}
|
{{ ReviewOfficerInfo(
|
||||||
{{ ReviewOfficerInfo("task_orders.new.review.cor", task_order.cor_first_name, task_order.cor_last_name, task_order.cor_email, task_order.cor_phone_number, task_order.cor_dod_id, task_order.contracting_officer_representative) }}
|
"task_orders.new.review.ko",
|
||||||
|
task_order.officer_dictionary("contracting_officer"),
|
||||||
|
task_order.contracting_officer,
|
||||||
|
task_order.ko_invitable
|
||||||
|
) }}
|
||||||
|
{{ ReviewOfficerInfo(
|
||||||
|
"task_orders.new.review.cor",
|
||||||
|
task_order.officer_dictionary("contracting_officer_representative"),
|
||||||
|
task_order.contracting_officer_representative,
|
||||||
|
task_order.cor_invitable
|
||||||
|
) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ ReviewOfficerInfo("task_orders.new.review.so", task_order.so_first_name, task_order.so_last_name, task_order.so_email, task_order.so_phone_number, task_order.so_dod_id, task_order.security_officer) }}
|
{{ ReviewOfficerInfo(
|
||||||
|
"task_orders.new.review.so",
|
||||||
|
task_order.officer_dictionary("security_officer"),
|
||||||
|
task_order.security_officer,
|
||||||
|
task_order.so_invitable
|
||||||
|
) }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,23 +2,39 @@
|
|||||||
{% from "components/sidenav_item.html" import SidenavItem %}
|
{% from "components/sidenav_item.html" import SidenavItem %}
|
||||||
|
|
||||||
|
|
||||||
<div class="global-navigation sidenav">
|
<div v-cloak is="SidenavToggler" class="sidenav-container">
|
||||||
|
<template slot-scope='props'>
|
||||||
|
<div v-bind:class="{'global-navigation': true, 'sidenav': props.isVisible, 'sidenav--minimized': !props.isVisible}">
|
||||||
|
<a href="#" v-on:click="props.toggle" class="sidenav__toggle">
|
||||||
|
<template v-if="props.isVisible">
|
||||||
|
{{ Icon('angle-double-left-solid', classes="toggle-arrows icon--blue") }}
|
||||||
|
Hide
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Show
|
||||||
|
{{ Icon('angle-double-right-solid', classes="toggle-arrows icon--blue") }}
|
||||||
|
</template>
|
||||||
|
</a>
|
||||||
|
<div v-if="props.isVisible">
|
||||||
<div class="sidenav__title">Portfolios</div>
|
<div class="sidenav__title">Portfolios</div>
|
||||||
<ul class="sidenav__list--padded">
|
<ul class="sidenav__list--padded">
|
||||||
|
{% if portfolios %}
|
||||||
{% for other_portfolio in portfolios|sort(attribute='name') %}
|
{% for other_portfolio in portfolios|sort(attribute='name') %}
|
||||||
{{ SidenavItem(other_portfolio.name,
|
{{ SidenavItem(other_portfolio.name,
|
||||||
href=url_for("portfolios.show_portfolio", portfolio_id=other_portfolio.id),
|
href=url_for("portfolios.show_portfolio", portfolio_id=other_portfolio.id),
|
||||||
active=(other_portfolio.id | string) == request.view_args.get('portfolio_id')
|
active=(other_portfolio.id | string) == request.view_args.get('portfolio_id')
|
||||||
) }}
|
) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<li><span class="sidenav__text">You have no portfolios yet</span></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="sidenav__divider--small"></div>
|
<div class="sidenav__divider--small"></div>
|
||||||
<a class="sidenav__link sidenav__link--add" href="{{ url_for("task_orders.get_started") }}" title="Fund a New Portfolio">
|
<a class="sidenav__link sidenav__link--add" href="{{ url_for("task_orders.get_started") }}" title="Fund a New Portfolio">
|
||||||
<span class="sidenav__link-label">Fund a New Portfolio</span>
|
<span class="sidenav__link-label">Fund a New Portfolio</span>
|
||||||
{{ Icon("plus", classes="sidenav__link-icon") }}
|
{{ Icon("plus", classes="sidenav__link-icon icon--circle") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="global-navigation sidenav--minimized">
|
</template>
|
||||||
<div class="sidenav__title">Show >>></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
{% from "components/sidenav_item.html" import SidenavItem %}
|
|
||||||
|
|
||||||
<nav class='sidenav portfolio-navigation'>
|
|
||||||
<ul>
|
|
||||||
{{ SidenavItem(
|
|
||||||
("navigation.portfolio_navigation.applications" | translate),
|
|
||||||
href=url_for("portfolios.portfolio_applications", portfolio_id=portfolio.id),
|
|
||||||
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/applications'),
|
|
||||||
subnav=None if not user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) else [
|
|
||||||
{
|
|
||||||
"label": ("navigation.portfolio_navigation.add_new_application_label" | translate),
|
|
||||||
"href": url_for('portfolios.new_application', portfolio_id=portfolio.id),
|
|
||||||
"active": g.matchesPath('\/portfolios\/[A-Za-z0-9-]*\/applications'),
|
|
||||||
"icon": "plus"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% if user_can(permissions.VIEW_PORTFOLIO_MEMBERS) %}
|
|
||||||
{{ SidenavItem(
|
|
||||||
("navigation.portfolio_navigation.members" | translate),
|
|
||||||
href=url_for("portfolios.portfolio_members", portfolio_id=portfolio.id),
|
|
||||||
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/members'),
|
|
||||||
subnav=None if not user_can(permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE) else [
|
|
||||||
{
|
|
||||||
"label": ("navigation.portfolio_navigation.add_new_member_label" | translate),
|
|
||||||
"href": url_for("portfolios.new_member", portfolio_id=portfolio.id),
|
|
||||||
"active": request.url_rule.rule.startswith('/portfolios/<portfolio_id>/members/new'),
|
|
||||||
"icon": "plus"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user_can(permissions.VIEW_USAGE_DOLLARS) %}
|
|
||||||
{{ SidenavItem(
|
|
||||||
("navigation.portfolio_navigation.budget_report" | translate),
|
|
||||||
href=url_for("portfolios.portfolio_reports", portfolio_id=portfolio.id),
|
|
||||||
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/reports')
|
|
||||||
) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{{ SidenavItem(
|
|
||||||
("navigation.portfolio_navigation.portfolio_funding" | translate),
|
|
||||||
href=url_for("portfolios.portfolio_funding", portfolio_id=portfolio.id),
|
|
||||||
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/task_order'),
|
|
||||||
subnav=None
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{% if user_can(permissions.EDIT_PORTFOLIO_INFORMATION) %}
|
|
||||||
{{ SidenavItem(
|
|
||||||
("navigation.portfolio_navigation.portfolio_settings" | translate),
|
|
||||||
href=url_for("portfolios.portfolio", portfolio_id=portfolio.id),
|
|
||||||
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/edit'),
|
|
||||||
subnav=None
|
|
||||||
) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user_can(permissions.VIEW_PORTFOLIO_AUDIT_LOG) %}
|
|
||||||
{{ SidenavItem(
|
|
||||||
("navigation.portfolio_navigation.activity_log" | translate),
|
|
||||||
href=url_for("portfolios.portfolio_activity", portfolio_id=portfolio.id),
|
|
||||||
active=request.url_rule.rule.startswith('/portfolios/<portfolio_id>/activity')
|
|
||||||
) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
@ -1,6 +1,8 @@
|
|||||||
{% extends "portfolios/base.html" %}
|
{% extends "portfolios/base.html" %}
|
||||||
{% from "components/pagination.html" import Pagination %}
|
{% from "components/pagination.html" import Pagination %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.admin" | translate %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
<div v-cloak>
|
<div v-cloak>
|
||||||
{% include "fragments/audit_events_log.html" %}
|
{% include "fragments/audit_events_log.html" %}
|
||||||
|
33
templates/portfolios/admin.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% extends "portfolios/base.html" %}
|
||||||
|
|
||||||
|
{% from "components/pagination.html" import Pagination %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = "navigation.portfolio_navigation.portfolio_admin" | translate %}
|
||||||
|
|
||||||
|
{% block portfolio_content %}
|
||||||
|
|
||||||
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
|
<div v-cloak class="portfolio-admin portfolio-content">
|
||||||
|
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('portfolios.edit_portfolio', portfolio_id=portfolio.id) }}" autocomplete="false">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
<div class='form-row'>
|
||||||
|
<div class='form-col form-col--half'>
|
||||||
|
{{ TextInput(form.name, validation="portfolioName") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='edit-portfolio-name action-group'>
|
||||||
|
<button type="submit" class="usa-button usa-button-big usa-button-primary" tabindex="0">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% include "fragments/audit_events_log.html" %}
|
||||||
|
|
||||||
|
{{ Pagination(audit_events, 'portfolios.portfolio_admin', portfolio_id=portfolio.id) }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
15
templates/portfolios/applications/base.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "portfolios/base.html" %}
|
||||||
|
|
||||||
|
{% block portfolio_header %}
|
||||||
|
<div class='portfolio-header'>
|
||||||
|
<div class='portfolio-header__name'>
|
||||||
|
{{ secondary_breadcrumb }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block portfolio_content %}
|
||||||
|
<div class='application-content'>
|
||||||
|
{% block application_content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1,35 +1,43 @@
|
|||||||
{% extends "portfolios/base.html" %}
|
{% extends "portfolios/applications/base.html" %}
|
||||||
|
|
||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% set secondary_breadcrumb = 'portfolios.applications.existing_application_title' | translate({ "application_name": application.name }) %}
|
||||||
|
|
||||||
|
{% block application_content %}
|
||||||
|
|
||||||
|
<div class='subheading'>{{ 'portfolios.applications.settings_heading' | translate }}</div>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('portfolios.edit_application', portfolio_id=portfolio.id, application_id=application.id) }}">
|
<form method="POST" action="{{ url_for('portfolios.edit_application', portfolio_id=portfolio.id, application_id=application.id) }}">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel__content">
|
||||||
|
|
||||||
{% include "fragments/edit_application_form.html" %}
|
{% include "fragments/edit_application_form.html" %}
|
||||||
|
|
||||||
<div class="block-list application-list-item">
|
<div class="application-list-item">
|
||||||
<header class="block-list__header block-list__header--grow">
|
<header>
|
||||||
<h2 class="block-list__title">Application Environments</h2>
|
<h2 class="block-list__title">{{ 'portfolios.applications.environments_heading' | translate }}</h2>
|
||||||
<p>
|
<p>
|
||||||
Each environment created within an application is an enclave of cloud resources that is logically separated from each other for increased security.
|
{{ 'portfolios.applications.environments_description' | translate }}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for environment in application.environments %}
|
{% for environment in application.environments %}
|
||||||
<li class="block-list__item application-edit__env-list-item">
|
<li class="application-edit__env-list-item">
|
||||||
<div class="usa-input">
|
<div class="usa-input input--disabled">
|
||||||
<label>Environment Name</label>
|
<label>Environment Name</label>
|
||||||
<input type="text" value="{{ environment.name }}" readonly />
|
<input type="text" disabled value="{{ environment.name }}" readonly />
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<button class="usa-button usa-button-primary" tabindex="0" type="submit">Update Application</button>
|
<button class="usa-button usa-button-primary" tabindex="0" type="submit">{{ 'portfolios.applications.update_button_text' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -3,12 +3,24 @@
|
|||||||
|
|
||||||
{% extends "portfolios/base.html" %}
|
{% extends "portfolios/base.html" %}
|
||||||
|
|
||||||
|
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
|
||||||
{% if not portfolio.applications %}
|
<div class='portfolio-applications'>
|
||||||
|
<div class='portfolio-applications__header row'>
|
||||||
|
<div class='portfolio-applications__header--title col col--grow'>Applications</div>
|
||||||
|
<div class='portfolio-applications__header--actions col'>
|
||||||
|
{% if can_create_applications %}
|
||||||
|
<a class='icon-link' href='{{ url_for('portfolios.new_application', portfolio_id=portfolio.id) }}'>
|
||||||
|
{{ 'portfolios.applications.add_application_text' | translate }}
|
||||||
|
{{ Icon("plus", classes="sidenav__link-icon icon--circle") }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %}
|
{% if not portfolio.applications %}
|
||||||
|
|
||||||
{{ EmptyState(
|
{{ EmptyState(
|
||||||
'This portfolio doesn’t have any applications yet.',
|
'This portfolio doesn’t have any applications yet.',
|
||||||
@ -20,36 +32,61 @@
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{% for application in portfolio.applications %}
|
<div class='application-list'>
|
||||||
<div v-cloak class='block-list application-list-item'>
|
{% for application in portfolio.applications|sort(attribute='name') %}
|
||||||
<header class='block-list__header'>
|
<div is='toggler' v-cloak class='accordian application-list-item'>
|
||||||
<h2 class='block-list__title'>{{ application.name }} ({{ application.environments|length }} environments)</h2>
|
<template slot-scope='props'>
|
||||||
|
<header class='accordian__header row'>
|
||||||
|
<div class='col col-grow'>
|
||||||
|
<h3 class='icon-link accordian__title' v-on:click="props.toggle">{{ application.name }}</h3>
|
||||||
|
<span class='accordian__description'>{{ application.description }}</span>
|
||||||
|
<div class='accordian__actions'>
|
||||||
{% if user_can(permissions.RENAME_APPLICATION_IN_PORTFOLIO) %}
|
{% if user_can(permissions.RENAME_APPLICATION_IN_PORTFOLIO) %}
|
||||||
<a class='icon-link' href='{{ url_for("portfolios.edit_application", portfolio_id=portfolio.id, application_id=application.id) }}'>
|
<a class='icon-link' href='{{ url_for("portfolios.edit_application", portfolio_id=portfolio.id, application_id=application.id) }}'>
|
||||||
{{ Icon('edit') }}
|
<span>{{ "portfolios.applications.app_settings_text" | translate }}</span>
|
||||||
<span>edit</span>
|
</a>
|
||||||
|
<div class='separator'></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if user_can(permissions.VIEW_PORTFOLIO_MEMBERS) %}
|
||||||
|
<a class='icon-link' href='{{ url_for("portfolios.application_members", portfolio_id=portfolio.id, application_id=application.id) }}'>
|
||||||
|
<span>{{ "portfolios.applications.team_text" | translate }}</span>
|
||||||
|
<span class='counter'>{{ application.num_users }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='col'>
|
||||||
|
<span v-on:click="props.toggle" class='icon-link toggle-link'>
|
||||||
|
<template v-if="props.isVisible">
|
||||||
|
{{ Icon('minus') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ Icon('plus') }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<ul>
|
<ul v-if="props.isVisible">
|
||||||
{% for environment in application.environments %}
|
{% for environment in application.environments %}
|
||||||
<li class='block-list__item application-list-item__environment'>
|
<li class='accordian__item application-list-item__environment'>
|
||||||
<a href='{{ url_for("portfolios.access_environment", portfolio_id=portfolio.id, environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer' class='application-list-item__environment__link'>
|
<div class='application-list-item__environment__name'>
|
||||||
{{ Icon('link') }}
|
|
||||||
<span>{{ environment.name }}</span>
|
<span>{{ environment.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href='{{ url_for("portfolios.access_environment", portfolio_id=portfolio.id, environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer' class='application-list-item__environment__csp_link icon-link'>
|
||||||
|
<span>{{ "portfolios.applications.csp_console_text" | translate }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class='application-list-item__environment__members'>
|
|
||||||
<div class='label'>{{ environment.num_users }}</div>
|
|
||||||
<span>members</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
90
templates/portfolios/applications/members.html
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
{% extends "portfolios/applications/base.html" %}
|
||||||
|
|
||||||
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = 'portfolios.applications.team_management.title' | translate({ "application_name": application.name }) %}
|
||||||
|
|
||||||
|
{% block application_content %}
|
||||||
|
|
||||||
|
<div class='subheading'>{{ 'portfolios.applications.team_management.subheading' | translate }}</div>
|
||||||
|
|
||||||
|
{% if not portfolio.members %}
|
||||||
|
|
||||||
|
{% set user_can_invite = user_can(permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE) %}
|
||||||
|
|
||||||
|
{{ EmptyState(
|
||||||
|
'There are currently no members in this Portfolio.',
|
||||||
|
action_label='Invite a new Member' if user_can_invite else None,
|
||||||
|
action_href='/members/new' if user_can_invite else None,
|
||||||
|
sub_message=None if user_can_invite else 'Please contact your JEDI Cloud portfolio administrator to invite new members.',
|
||||||
|
icon='avatar'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
|
<members-list
|
||||||
|
inline-template
|
||||||
|
id="search-template"
|
||||||
|
class='member-list'
|
||||||
|
v-bind:members='{{ members | tojson}}'>
|
||||||
|
<div class='responsive-table-wrapper panel'>
|
||||||
|
<table v-cloak v-if='searchedList && searchedList.length'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="col in getColumns()" @click="updateSort(col.displayName)" :width="col.width" :class="col.class" scope="col">
|
||||||
|
!{ col.displayName }
|
||||||
|
<span class="sorting-direction" v-if="col.displayName === sortInfo.columnName && sortInfo.isAscending">
|
||||||
|
{{ Icon("caret_down") }}
|
||||||
|
</span>
|
||||||
|
<span class="sorting-direction" v-if="col.displayName === sortInfo.columnName && !sortInfo.isAscending">
|
||||||
|
{{ Icon("caret_up") }}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr v-for='member in searchedList'>
|
||||||
|
<td>
|
||||||
|
<a :href="member.edit_link" class="icon-link icon-link--large" v-html="member.name"></a>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell--align-center" v-if='member.num_env'>
|
||||||
|
<span v-html="member.num_env"></span>
|
||||||
|
</td>
|
||||||
|
<td class='table-cell--shrink' v-else>
|
||||||
|
<span class="label label--info">No Environment Access</span>
|
||||||
|
</td>
|
||||||
|
<td v-html="member.status"></td>
|
||||||
|
<td v-html="member.role"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="add-member-link" colspan=4>
|
||||||
|
<a class="icon-link" href="{{ url_for('portfolios.new_member', portfolio_id=portfolio.id) }}">
|
||||||
|
Add A New Member
|
||||||
|
{{ Icon('plus', classes='icon--circle') }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-else>
|
||||||
|
{{ EmptyState(
|
||||||
|
'No members found.',
|
||||||
|
action_label=None,
|
||||||
|
action_href=None,
|
||||||
|
sub_message='Please try a different search.',
|
||||||
|
icon=None
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</members-list>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -1,23 +1,29 @@
|
|||||||
{% extends "portfolios/base.html" %}
|
{% extends "portfolios/applications/base.html" %}
|
||||||
|
|
||||||
{% from "components/alert.html" import Alert %}
|
{% from "components/alert.html" import Alert %}
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% from "components/modal.html" import Modal %}
|
{% from "components/modal.html" import Modal %}
|
||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% set secondary_breadcrumb = 'portfolios.applications.new_application_title' | translate %}
|
||||||
|
|
||||||
|
{% block application_content %}
|
||||||
|
|
||||||
{% set modalName = "newApplicationConfirmation" %}
|
{% set modalName = "newApplicationConfirmation" %}
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
|
<div class='subheading'>{{ 'portfolios.applications.settings_heading' | translate }}</div>
|
||||||
|
|
||||||
<new-application inline-template v-bind:initial-data='{{ form.data|tojson }}' modal-name='{{ modalName }}'>
|
<new-application inline-template v-bind:initial-data='{{ form.data|tojson }}' modal-name='{{ modalName }}'>
|
||||||
<form method="POST" action="{{ url_for('portfolios.create_application', portfolio_id=portfolio.id) }}" v-on:submit="handleSubmit">
|
<form method="POST" action="{{ url_for('portfolios.create_application', portfolio_id=portfolio.id) }}" v-on:submit="handleSubmit">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel__content">
|
||||||
|
|
||||||
{% call Modal(name=modalName, dismissable=False) %}
|
{% call Modal(name=modalName, dismissable=False) %}
|
||||||
<h1>Create application !{ name }</h1>
|
<h1>Create application !{ name }</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
When you click <em>Create Application</em>, the environments
|
When you click <em>{{ 'portfolios.applications.create_button_text' | translate }}</em>, the environments
|
||||||
<span v-for="(environment, index) in environments">
|
<span v-for="(environment, index) in environments">
|
||||||
<strong>!{environment.name}</strong><template v-if="index < (environments.length - 1)">, </template>
|
<strong>!{environment.name}</strong><template v-if="index < (environments.length - 1)">, </template>
|
||||||
</span>
|
</span>
|
||||||
@ -25,7 +31,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class='action-group'>
|
<div class='action-group'>
|
||||||
<button autofocus type='submit' class='action-group__action usa-button' tabindex='0'>Create Application</button>
|
<button autofocus type='submit' class='action-group__action usa-button' tabindex='0'>{{ 'portfolios.applications.create_button_text' | translate }}</button>
|
||||||
<button type='button' v-on:click="handleCancelSubmit" class='icon-link action-group__action' tabindex='0'>Cancel</button>
|
<button type='button' v-on:click="handleCancelSubmit" class='icon-link action-group__action' tabindex='0'>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
@ -38,16 +44,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block-list application-list-item">
|
<div class="application-list-item">
|
||||||
<header class="block-list__header block-list__header--grow">
|
<header>
|
||||||
<h2 class="block-list__title">Application Environments</h2>
|
<h2 class="block-list__title">{{ 'portfolios.applications.environments_heading' | translate }}</h2>
|
||||||
<p>
|
<p>
|
||||||
Each environment created within an application is an enclave of cloud resources that is logically separated from each other for increased security.
|
{{ 'portfolios.applications.environments_description' | translate }}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(environment, i) in environments" class="block-list__item application-edit__env-list-item">
|
<li v-for="(environment, i) in environments" class="application-edit__env-list-item">
|
||||||
<div class="usa-input">
|
<div class="usa-input">
|
||||||
<label :for="'environment_names-' + i">Environment Name</label>
|
<label :for="'environment_names-' + i">Environment Name</label>
|
||||||
<input type="text" :id="'environment_names-' + i" v-model="environment.name" placeholder="e.g. Development, Staging, Production"/>
|
<input type="text" :id="'environment_names-' + i" v-model="environment.name" placeholder="e.g. Development, Staging, Production"/>
|
||||||
@ -65,8 +71,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<button class="usa-button usa-button-primary" tabindex="0" type="submit">Create Application</button>
|
<button class="usa-button usa-button-primary" tabindex="0" type="submit">{{ 'portfolios.applications.create_button_text' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</new-application>
|
</new-application>
|
||||||
|
@ -3,13 +3,19 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class='portfolio-panel-container'>
|
<div class='portfolio-panel-container'>
|
||||||
<div class='col'>
|
|
||||||
{% include 'navigation/portfolio_navigation.html' %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='col col--grow'>
|
<div class='col col--grow'>
|
||||||
|
{% block portfolio_breadcrumbs %}
|
||||||
|
{% include "portfolios/breadcrumbs.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
<div class='line'></div>
|
||||||
|
{% block portfolio_header %}
|
||||||
|
{% include "portfolios/header.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
<div class='line'></div>
|
||||||
|
<div class='portfolio-content'>
|
||||||
{% block portfolio_content %}{% endblock %}
|
{% block portfolio_content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
16
templates/portfolios/blank_slate.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
|
{% from "components/tooltip.html" import Tooltip %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{
|
||||||
|
EmptyState(
|
||||||
|
action_href=url_for("task_orders.get_started"),
|
||||||
|
action_label=("portfolios.index.empty.start_button" | translate),
|
||||||
|
icon="cloud",
|
||||||
|
message=("portfolios.index.empty.title" | translate),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
{% endblock %}
|
||||||
|
|
19
templates/portfolios/breadcrumbs.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
<div class="row portfolio-breadcrumbs">
|
||||||
|
<a class="icon-link portfolio-breadcrumbs__home {{ 'icon-link--disabled' if not secondary_breadcrumb }}" href="{{ url_for("portfolios.portfolio_applications", portfolio_id=portfolio.id) }}">
|
||||||
|
{{ Icon("home") }}
|
||||||
|
<span>
|
||||||
|
{{ portfolio.name }} Portfolio
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="portfolio-breadcrumbs__crumb">
|
||||||
|
{% if secondary_breadcrumb %}
|
||||||
|
{{ Icon("caret_right", classes="icon--tiny") }}
|
||||||
|
<div class="icon-link icon-link--disabled">
|
||||||
|
{{ secondary_breadcrumb }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div
|
||||||
|
>
|
@ -1,36 +0,0 @@
|
|||||||
{% extends "portfolios/base.html" %}
|
|
||||||
|
|
||||||
{% from "components/icon.html" import Icon %}
|
|
||||||
{% from "components/text_input.html" import TextInput %}
|
|
||||||
|
|
||||||
{% block portfolio_content %}
|
|
||||||
|
|
||||||
{% include "fragments/flash.html" %}
|
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('portfolios.edit_portfolio', portfolio_id=portfolio.id) }}" autocomplete="false">
|
|
||||||
{{ form.csrf_token }}
|
|
||||||
|
|
||||||
<div class="panel">
|
|
||||||
|
|
||||||
<div class="panel__heading">
|
|
||||||
<h1>Portfolio Settings</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel__content">
|
|
||||||
{{ TextInput(form.name, validation="portfolioName") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class='action-group'>
|
|
||||||
<button type="submit" class="usa-button usa-button-big usa-button-primary" tabindex="0">Save</button>
|
|
||||||
<a href='{{ url_for("portfolios.portfolio_applications", portfolio_id=portfolio.id) }}' class='action-group__action icon-link'>
|
|
||||||
{{ Icon('x') }}
|
|
||||||
<span>Cancel</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
78
templates/portfolios/header.html
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
{% macro Link(icon, text, url, active=False) %}
|
||||||
|
<a class='icon-link {{ "active icon-link--disabled" if active }}' href='{{ url }}'>
|
||||||
|
<div class='col'>
|
||||||
|
<div class='icon-link--icon'>{{ Icon(icon) }}</div>
|
||||||
|
<div class='icon-link--name'>{{ text }}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
<div class='portfolio-header row'>
|
||||||
|
<div class='col col--grow'>
|
||||||
|
<div class='portfolio-header__name'>
|
||||||
|
{{ secondary_breadcrumb or portfolio.name }}
|
||||||
|
</div>
|
||||||
|
<div class='portfolio-header__budget row'>
|
||||||
|
<div class='column-left'>
|
||||||
|
<span>Available budget</span>
|
||||||
|
<button type="button" tabindex="0" class="icon-tooltip" v-tooltip.right="{content: 'The available budget shown includes the available budget of all active task orders', container: false}">
|
||||||
|
{{ Icon('info') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class='portfolio-header__budget--amount'>
|
||||||
|
<span class='portfolio-header__budget--dollars'>
|
||||||
|
{{ portfolio.task_orders | selectattr('is_active') | sum(attribute='budget') | justDollars }}
|
||||||
|
</span>
|
||||||
|
<span class='portfolio-header__budget--cents'>
|
||||||
|
.{{ portfolio.task_orders | selectattr('is_active') | sum(attribute='budget') | justCents }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='row'>
|
||||||
|
<div class='column-left'></div>
|
||||||
|
<div class='column-right portfolio-funding__header--funded-through {{ "funded" if funding_end_date is not none and funded else "unfunded"}}'>
|
||||||
|
{% if funding_end_date and funded %}
|
||||||
|
{{ Icon('ok') }}
|
||||||
|
Funded through
|
||||||
|
<local-datetime
|
||||||
|
timestamp='{{ funding_end_date }}'
|
||||||
|
format="M/D/YYYY">
|
||||||
|
</local-datetime>
|
||||||
|
{% elif funding_end_date and not funded %}
|
||||||
|
{{ Icon('alert') }}
|
||||||
|
Funded period ends
|
||||||
|
<local-datetime
|
||||||
|
timestamp='{{ funding_end_date }}'
|
||||||
|
format="M/D/YYYY">
|
||||||
|
</local-datetime>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='row links'>
|
||||||
|
{% if user_can(permissions.VIEW_USAGE_DOLLARS) %}
|
||||||
|
{{ Link(
|
||||||
|
icon='chart-pie',
|
||||||
|
text='navigation.portfolio_navigation.breadcrumbs.reports' | translate,
|
||||||
|
url=url_for("portfolios.portfolio_reports", portfolio_id=portfolio.id),
|
||||||
|
active=request.url_rule.endpoint == "portfolios.portfolio_reports",
|
||||||
|
) }}
|
||||||
|
{% endif %}
|
||||||
|
{{ Link(
|
||||||
|
icon='dollar-sign',
|
||||||
|
text='navigation.portfolio_navigation.breadcrumbs.funding' | translate,
|
||||||
|
url=url_for("portfolios.portfolio_funding", portfolio_id=portfolio.id),
|
||||||
|
active=request.url_rule.endpoint == "portfolios.portfolio_funding",
|
||||||
|
) }}
|
||||||
|
{% if user_can(permissions.EDIT_PORTFOLIO_INFORMATION) %}
|
||||||
|
{{ Link(
|
||||||
|
icon='cog',
|
||||||
|
text='navigation.portfolio_navigation.breadcrumbs.admin' | translate,
|
||||||
|
url=url_for("portfolios.portfolio_admin", portfolio_id=portfolio.id),
|
||||||
|
active=request.url_rule.endpoint == "portfolios.portfolio_admin",
|
||||||
|
) }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -13,7 +13,10 @@
|
|||||||
<form method="POST" action="{{ url_for('portfolios.update_member', portfolio_id=portfolio.id, member_id=member.user_id) }}" autocomplete="false">
|
<form method="POST" action="{{ url_for('portfolios.update_member', portfolio_id=portfolio.id, member_id=member.user_id) }}" autocomplete="false">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
<div class='panel member-card'>
|
<div class='member-edit'>
|
||||||
|
<div class="subheading">Edit Portfolio Member</div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class='member-card'>
|
||||||
<div class='member-card__header'>
|
<div class='member-card__header'>
|
||||||
<h1 class='member-card__heading'>{{ member.user.full_name }}</h1>
|
<h1 class='member-card__heading'>{{ member.user.full_name }}</h1>
|
||||||
|
|
||||||
@ -61,10 +64,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="manage-access">
|
||||||
<div class="panel__heading panel__heading--tight">
|
<div class="subheading">Manage Access</div>
|
||||||
<h2 class="h3">Manage Access <div class="subtitle">Grant access to an environment</div></h2>
|
<div class="subtitle">Grant access to an environment</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='search-bar'>
|
<div class='search-bar'>
|
||||||
@ -173,6 +175,9 @@
|
|||||||
</edit-application-roles>
|
</edit-application-roles>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='action-group'>
|
<div class='action-group'>
|
||||||
<button class='action-group__action usa-button usa-button-big'>
|
<button class='action-group__action usa-button usa-button-big'>
|
||||||
{% if is_new_member %}Create{% else %}Save{% endif %}
|
{% if is_new_member %}Create{% else %}Save{% endif %}
|
||||||
@ -185,6 +190,4 @@
|
|||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
{% from "components/empty_state.html" import EmptyState %}
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = 'Portfolio Team Management' %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
|
||||||
{% if not portfolio.members %}
|
{% if not portfolio.members %}
|
||||||
@ -25,6 +27,7 @@
|
|||||||
<members-list
|
<members-list
|
||||||
inline-template
|
inline-template
|
||||||
id="search-template"
|
id="search-template"
|
||||||
|
class='member-list'
|
||||||
v-bind:members='{{ members | tojson}}'
|
v-bind:members='{{ members | tojson}}'
|
||||||
v-bind:role_choices='{{ role_choices | tojson}}'
|
v-bind:role_choices='{{ role_choices | tojson}}'
|
||||||
v-bind:status_choices='{{ status_choices | tojson}}'>
|
v-bind:status_choices='{{ status_choices | tojson}}'>
|
||||||
@ -61,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class='responsive-table-wrapper'>
|
<div class='responsive-table-wrapper panel'>
|
||||||
<table v-cloak v-if='searchedList && searchedList.length'>
|
<table v-cloak v-if='searchedList && searchedList.length'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -82,7 +85,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<a :href="member.edit_link" class="icon-link icon-link--large" v-html="member.name"></a>
|
<a :href="member.edit_link" class="icon-link icon-link--large" v-html="member.name"></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell--align-right" v-if='member.num_env'>
|
<td class="table-cell--align-center" v-if='member.num_env'>
|
||||||
<span v-html="member.num_env"></span>
|
<span v-html="member.num_env"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class='table-cell--shrink' v-else>
|
<td class='table-cell--shrink' v-else>
|
||||||
@ -91,6 +94,14 @@
|
|||||||
<td v-html="member.status"></td>
|
<td v-html="member.status"></td>
|
||||||
<td v-html="member.role"></td>
|
<td v-html="member.role"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="add-member-link" colspan=4>
|
||||||
|
<a class="icon-link" href="{{ url_for('portfolios.new_member', portfolio_id=portfolio.id) }}">
|
||||||
|
Add A New Member
|
||||||
|
{{ Icon('plus', classes='icon--circle') }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
@ -1,47 +1,44 @@
|
|||||||
{% extends "portfolios/base.html" %}
|
{% extends "portfolios/base.html" %}
|
||||||
|
|
||||||
{% from "components/alert.html" import Alert %}
|
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% from "components/empty_state.html" import EmptyState %}
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.reports" | translate %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
|
||||||
{{ Alert("Budget Report for Portfolio " + portfolio.name,
|
<div class='portfolio-reports'>
|
||||||
message="<p>Track your monthly and cumulative expenditures for your portfolio, applications, and environments below.</p>\
|
|
||||||
<p>Please note that the projected spend is based on the <em>average expense over the last three completed months</em> and therefore does not account for future changes that might be made in scale or configuration of your cloud services.</p>",
|
|
||||||
actions=[
|
|
||||||
{"label": "Learn More", "href": url_for('atst.helpdocs'), "icon": "info"}
|
|
||||||
] ) }}
|
|
||||||
|
|
||||||
<div v-cloak class='funding-summary-row'>
|
<div v-cloak class='funding-summary-row'>
|
||||||
|
|
||||||
<div class='funding-summary-row__col'>
|
<div class='funding-summary-row__col'>
|
||||||
<div class='panel spend-summary'>
|
<div class='panel spend-summary'>
|
||||||
|
<h4 class='spend-summary__heading subheading'>Portfolio Total Spend</h4>
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<h2 class='spend-summary__heading col'>Portfolio Total Spend</h2>
|
<dl class='spend-summary__budget col col--grow row'>
|
||||||
<dl class='spend-summary__budget'>
|
|
||||||
{% set budget = portfolio_totals['budget'] %}
|
{% set budget = portfolio_totals['budget'] %}
|
||||||
{% set spent = portfolio_totals['spent'] %}
|
{% set spent = portfolio_totals['spent'] %}
|
||||||
{% set remaining = budget - spent %}
|
{% set remaining = budget - spent %}
|
||||||
<div>
|
<dl class='col col--grow'>
|
||||||
<dt>Budget</dt>
|
<dt>Budget</dt>
|
||||||
<dd>{{ budget | dollars }}</dd>
|
<dd>{{ budget | dollars }}</dd>
|
||||||
</div>
|
</dl>
|
||||||
|
|
||||||
<div>
|
<dl class='col col--grow'>
|
||||||
<dt>Remaining</dt>
|
<dt>Remaining</dt>
|
||||||
<dd>{{ remaining | dollars }}</dd>
|
<dd>{{ remaining | dollars }}</dd>
|
||||||
</div>
|
</dl>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<meter value='{{ spent }}' min='0' max='{{ budget }}' title='{{ spent | dollars }} Total spend to date'>
|
<meter value='{{ spent }}' min='0' max='{{ budget }}' title='{{ spent | dollars }} Total spend to date'>
|
||||||
<div class='meter__fallback' style='width:{{ (spent / budget) * 100 if budget else 0 }}%;'></div>
|
<div class='meter__fallback' style='width:{{ (spent / budget) * 100 if budget else 0 }}%;'></div>
|
||||||
</meter>
|
</meter>
|
||||||
|
|
||||||
<dl class='spend-summary__spent'>
|
<dl class='spend-summary__spent'>
|
||||||
<dt>Total spend to date</dt>
|
<dt>Total spending to date</dt>
|
||||||
<dd>{{ spent | dollars }}</dd>
|
<dd>{{ spent | dollars }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
@ -53,18 +50,23 @@
|
|||||||
<div class='to-summary__row'>
|
<div class='to-summary__row'>
|
||||||
|
|
||||||
<div class='to-summary__to'>
|
<div class='to-summary__to'>
|
||||||
<h2 class='to-summary__heading'>Task Order</h2>
|
<h2 class='to-summary__heading subheading'>Current Task Order</h2>
|
||||||
<dl class='to-summary__to-number'>
|
<dl class='to-summary__to-number'>
|
||||||
<dt class='usa-sr-only'>Task Order Number</dt>
|
<dt class='usa-sr-only'>Task Order Number</dt>
|
||||||
<dd>{{ legacy_task_order.number }}</dd>
|
<dd>{{ task_order.number }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
<div class='to-summary__expiration'>
|
<div class='to-summary__expiration'>
|
||||||
<dl>
|
<div class='row'>
|
||||||
|
<h4 class='subheading'>Expiration Date</h4>
|
||||||
|
</div>
|
||||||
|
<div class='row'>
|
||||||
|
|
||||||
|
<div class='col col--grow'>
|
||||||
<div>
|
<div>
|
||||||
<dt>Expires</dt>
|
|
||||||
<dd>
|
|
||||||
{% if expiration_date %}
|
{% if expiration_date %}
|
||||||
<local-datetime
|
<local-datetime
|
||||||
timestamp='{{ expiration_date }}'
|
timestamp='{{ expiration_date }}'
|
||||||
@ -73,32 +75,49 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
-
|
-
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
|
<a href='{{ url_for("portfolios.view_task_order", portfolio_id=portfolio.id, task_order_id=task_order.id) }}' class='icon-link'>
|
||||||
<div>
|
{{ Icon('cog') }}
|
||||||
<dt>Remaining</dt>
|
Manage Task Order
|
||||||
<dd>
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class='col col--grow'>
|
||||||
|
<dl>
|
||||||
|
<dt>Remaining Days</dt>
|
||||||
|
<dd class='{{ 'ending-soon' if remaining_days is not none }}'>
|
||||||
{% if remaining_days is not none %}
|
{% if remaining_days is not none %}
|
||||||
{{ remaining_days }} days
|
{{ Icon('arrow-down') }}
|
||||||
|
<span>{{ remaining_days }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
-
|
-
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
|
||||||
</dl>
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href='{{ url_for("portfolios.portfolio", portfolio_id=portfolio.id) }}' class='icon-link'>
|
|
||||||
Manage Task Order
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
<dl class='to-summary__co'>
|
<dl class='to-summary__co'>
|
||||||
<dt>Contracting Officer</dt>
|
<dt class='subheading'>Contracting Officer</dt>
|
||||||
<dd>
|
<dd class='row'>
|
||||||
{{ jedi_request.contracting_officer_full_name }}
|
<div class='col col--grow'>
|
||||||
<a class='icon-link' href='mailto:{{ jedi_request.contracting_officer_email }}'>{{ jedi_request.contracting_officer_email }}</a>
|
{% if task_order.ko_first_name and task_order.ko_last_name %}
|
||||||
|
{{ task_order.ko_first_name }} {{ task_order.ko_last_name }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='col'>
|
||||||
|
{% if task_order.ko_email %}
|
||||||
|
<a class='icon-link' href='mailto:{{ task_order.ko_email }}'>
|
||||||
|
{{ Icon('envelope') }}
|
||||||
|
{{ task_order.ko_email }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
@ -140,7 +159,7 @@
|
|||||||
|
|
||||||
<div class='budget-chart panel' ref='panel'>
|
<div class='budget-chart panel' ref='panel'>
|
||||||
<header class='budget-chart__header panel__heading panel__heading--tight'>
|
<header class='budget-chart__header panel__heading panel__heading--tight'>
|
||||||
<h2 class='h3'>Cumulative Budget</h2>
|
<h4>Cumulative Budget</h4>
|
||||||
|
|
||||||
<div class='budget-chart__legend'>
|
<div class='budget-chart__legend'>
|
||||||
<dl class='budget-chart__legend__spend'>
|
<dl class='budget-chart__legend__spend'>
|
||||||
@ -329,7 +348,7 @@
|
|||||||
|
|
||||||
<div class='spend-table responsive-table-wrapper'>
|
<div class='spend-table responsive-table-wrapper'>
|
||||||
<div class='spend-table__header'>
|
<div class='spend-table__header'>
|
||||||
<h2 class='spend-table__title'>Total spend per month </h2>
|
<h2 class='spend-table__title'>Total spent per month</h2>
|
||||||
|
|
||||||
<select name='month' id='month' onchange='location = this.value' class='spend-table__month-select'>
|
<select name='month' id='month' onchange='location = this.value' class='spend-table__month-select'>
|
||||||
{% for m in cumulative_budget["months"] %}
|
{% for m in cumulative_budget["months"] %}
|
||||||
@ -366,12 +385,12 @@
|
|||||||
<th scope='col' class='table-cell--align-right previous-month'>{{ two_months_ago.strftime('%B %Y') }}</th>
|
<th scope='col' class='table-cell--align-right previous-month'>{{ two_months_ago.strftime('%B %Y') }}</th>
|
||||||
<th scope='col' class='table-cell--align-right previous-month'>{{ prev_month.strftime('%B %Y') }}</th>
|
<th scope='col' class='table-cell--align-right previous-month'>{{ prev_month.strftime('%B %Y') }}</th>
|
||||||
<th scope='col' class='table-cell--align-right current-month'>{{ current_month.strftime('%B %Y') }}</th>
|
<th scope='col' class='table-cell--align-right current-month'>{{ current_month.strftime('%B %Y') }}</th>
|
||||||
<th class='current-month'>% of total spend this month</th>
|
<th class='current-month'></th>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody class='spend-table__portfolio'>
|
<tbody class='spend-table__portfolio'>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope='row'>Total</th>
|
<th scope='row'>Portfolio Total</th>
|
||||||
<td class='table-cell--align-right previous-month'>{{ portfolio_totals.get(two_months_ago_index, 0) | dollars }}</td>
|
<td class='table-cell--align-right previous-month'>{{ portfolio_totals.get(two_months_ago_index, 0) | dollars }}</td>
|
||||||
<td class='table-cell--align-right previous-month'>{{ portfolio_totals.get(prev_month_index, 0) | dollars }}</td>
|
<td class='table-cell--align-right previous-month'>{{ portfolio_totals.get(prev_month_index, 0) | dollars }}</td>
|
||||||
<td class='table-cell--align-right current-month'>{{ portfolio_totals.get(current_month_index, 0) | dollars }}</td>
|
<td class='table-cell--align-right current-month'>{{ portfolio_totals.get(current_month_index, 0) | dollars }}</td>
|
||||||
@ -387,7 +406,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope='rowgroup'>
|
<th scope='rowgroup'>
|
||||||
<button v-on:click='toggle($event, name)' class='icon-link icon-link--large spend-table__application__toggler'>
|
<button v-on:click='toggle($event, name)' class='icon-link icon-link--large spend-table__application__toggler'>
|
||||||
<template v-if='application.isVisible'>{{ Icon('caret_down') }}</template>
|
<template v-if='application.isVisible'>{{ Icon('caret_down') }}<div class='open-indicator'></div></template>
|
||||||
<template v-else>{{ Icon('caret_right') }}</template>
|
<template v-else>{{ Icon('caret_right') }}</template>
|
||||||
<span v-html='name'></span>
|
<span v-html='name'></span>
|
||||||
</button>
|
</button>
|
||||||
@ -416,10 +435,9 @@
|
|||||||
|
|
||||||
<tr v-for='(environment, envName) in environments[name]' v-show='application.isVisible' class='spend-table__application__env'>
|
<tr v-for='(environment, envName) in environments[name]' v-show='application.isVisible' class='spend-table__application__env'>
|
||||||
<th scope='rowgroup'>
|
<th scope='rowgroup'>
|
||||||
<a href='#' class='icon-link spend-table__application__env'>
|
<div class='icon-link spend-table__application__env'>
|
||||||
{{ Icon('link') }}
|
|
||||||
<span v-html='envName'></span>
|
<span v-html='envName'></span>
|
||||||
</a>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<td class='table-cell--align-right previous-month'>
|
<td class='table-cell--align-right previous-month'>
|
||||||
@ -440,7 +458,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</spend-table>
|
</spend-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
{% extends "portfolios/base.html" %}
|
{% extends "portfolios/base.html" %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.funding" | translate %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
|
||||||
{% macro ViewLink(task_order) %}
|
{% macro ViewLink(task_order) %}
|
||||||
@ -81,6 +83,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{{ caller and caller() }}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -89,33 +92,15 @@
|
|||||||
|
|
||||||
<div class="portfolio-funding">
|
<div class="portfolio-funding">
|
||||||
|
|
||||||
<div class='panel'>
|
<div class='portfolio-funding__header row'>
|
||||||
<div class='panel__content portfolio-funding__header row'>
|
|
||||||
<h3>Portfolio Funding</h3>
|
|
||||||
<div class='portfolio-funding__header--funded-through {{ "funded" if funding_end_date is not none and funded else "unfunded"}}'>
|
|
||||||
{% if funding_end_date and funded %}
|
|
||||||
{{ Icon('ok') }}
|
|
||||||
Funded through
|
|
||||||
<local-datetime
|
|
||||||
timestamp='{{ funding_end_date }}'
|
|
||||||
format="M/D/YYYY">
|
|
||||||
</local-datetime>
|
|
||||||
{% elif funding_end_date and not funded %}
|
|
||||||
{{ Icon('alert') }}
|
|
||||||
Funded period ends
|
|
||||||
<local-datetime
|
|
||||||
timestamp='{{ funding_end_date }}'
|
|
||||||
format="M/D/YYYY">
|
|
||||||
</local-datetime>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<a href="{{ url_for("task_orders.new", screen=1, portfolio_id=portfolio.id) }}" class="usa-button">Start a New Task Order</a>
|
<a href="{{ url_for("task_orders.new", screen=1, portfolio_id=portfolio.id) }}" class="usa-button">Start a New Task Order</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for task_order in pending_task_orders %}
|
{% for task_order in pending_task_orders %}
|
||||||
<div class='panel'>
|
<div class='subheading'>
|
||||||
<div class='panel__content pending-task-order row'>
|
Pending
|
||||||
|
</div>
|
||||||
|
<div class='panel pending-task-order row'>
|
||||||
<span class='label label--warning'>Pending</span>
|
<span class='label label--warning'>Pending</span>
|
||||||
<div class="pending-task-order__started col">
|
<div class="pending-task-order__started col">
|
||||||
<dt>Started</dt>
|
<dt>Started</dt>
|
||||||
@ -132,7 +117,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ ViewLink(task_order) }}
|
{{ ViewLink(task_order) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
@ -146,16 +130,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if active_task_orders %}
|
{% if active_task_orders %}
|
||||||
{{ TaskOrderList(active_task_orders, label='success', funded=funded) }}
|
<div class='subheading'>Active</div>
|
||||||
<div class='panel portfolio-total-balance'>
|
{% call TaskOrderList(active_task_orders, label='success', funded=funded) %}
|
||||||
<div class='panel__content row'>
|
<tr class='total-balance'>
|
||||||
<span>{{ total_balance | dollars }}</span>
|
<td colspan='4'>
|
||||||
<span class='label label--success'>Total Active Balance</span>
|
<span class='label label--success'>Total Active Balance</span>
|
||||||
</div>
|
<span>{{ total_balance | dollars }}</span>
|
||||||
</div>
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
{% endcall %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if expired_task_orders %}
|
{% if expired_task_orders %}
|
||||||
|
<div class='subheading'>Expired</div>
|
||||||
{{ TaskOrderList(expired_task_orders, label='expired', expired=True) }}
|
{{ TaskOrderList(expired_task_orders, label='expired', expired=True) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{% extends "portfolios/base.html" %}
|
{% extends "portfolios/base.html" %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.funding" | translate %}
|
||||||
|
|
||||||
{% from "components/checkbox_input.html" import CheckboxInput %}
|
{% from "components/checkbox_input.html" import CheckboxInput %}
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
@ -11,11 +13,12 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro EditOfficerInfo(form, officer_type) -%}
|
{% macro EditOfficerInfo(form, officer_type, invited) -%}
|
||||||
<div class='officer__form'>
|
|
||||||
<template v-if="editing">
|
<template v-if="editing">
|
||||||
<div class='alert'>
|
<div class='officer__form'>
|
||||||
<div class='alert__content'>
|
<div class="edit-officer">
|
||||||
|
<h4>{{ ("task_orders.invitations." + officer_type + ".edit_title") | translate}}</h4>
|
||||||
|
</div>
|
||||||
<div class='form-row'>
|
<div class='form-row'>
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(form.first_name) }}
|
{{ TextInput(form.first_name) }}
|
||||||
@ -35,9 +38,18 @@
|
|||||||
{{ TextInput(form.phone_number, placeholder='(123) 456-7890', validation='usPhone') }}
|
{{ TextInput(form.phone_number, placeholder='(123) 456-7890', validation='usPhone') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if form.dod_id.data %}
|
<div class='form-row officer__form--dodId'>
|
||||||
{{ TextInput(form.dod_id, validation='dodId', disabled=True)}}
|
<div class="form-col">
|
||||||
|
{% if not invited %}
|
||||||
|
<div class='form-row'>
|
||||||
|
{{ CheckboxInput(form.invite, label=(("forms.officers." + officer_type + "_invite") | translate)) }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class='form-row'>
|
||||||
|
{{ TextInput(form.dod_id, tooltip="task_orders.new.oversight.dod_id_tooltip" | translate, tooltip_title='Why', validation='dodId', disabled=invited)}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class='alert__actions officer__form--actions'>
|
<div class='alert__actions officer__form--actions'>
|
||||||
<a href="#{{ officer_type }}" v-on:click="cancel" class="icon-link">
|
<a href="#{{ officer_type }}" v-on:click="cancel" class="icon-link">
|
||||||
{{ Icon("x") }}
|
{{ Icon("x") }}
|
||||||
@ -46,9 +58,7 @@
|
|||||||
<input type='submit' class='usa-button usa-button-primary' value='Save Changes' />
|
<input type='submit' class='usa-button usa-button-primary' value='Save Changes' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro OfficerInfo(task_order, officer_type, form) %}
|
{% macro OfficerInfo(task_order, officer_type, form) %}
|
||||||
@ -65,8 +75,11 @@
|
|||||||
{% set email = task_order[prefix + "_email"] %}
|
{% set email = task_order[prefix + "_email"] %}
|
||||||
{% set phone_number = task_order[prefix + "_phone_number"] %}
|
{% set phone_number = task_order[prefix + "_phone_number"] %}
|
||||||
{% set dod_id = task_order[prefix + "_dod_id"] %}
|
{% set dod_id = task_order[prefix + "_dod_id"] %}
|
||||||
|
{% set invited = False %}
|
||||||
|
|
||||||
|
|
||||||
{% if task_order[officer_type] %}
|
{% if task_order[officer_type] %}
|
||||||
|
{% set invited = True %}
|
||||||
<div class="officer__info">
|
<div class="officer__info">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="officer__info--name">{{ first_name }} {{ last_name }}</div>
|
<div class="officer__info--name">{{ first_name }} {{ last_name }}</div>
|
||||||
@ -99,7 +112,7 @@
|
|||||||
<div class="officer__actions">
|
<div class="officer__actions">
|
||||||
{{ Link("Update", "edit", onClick="edit") }}
|
{{ Link("Update", "edit", onClick="edit") }}
|
||||||
{{ Link("Remove", "trash", classes="remove") }}
|
{{ Link("Remove", "trash", classes="remove") }}
|
||||||
<button type='button' class='usa-button usa-button-primary'>
|
<button v-if="!editing" type='button' class='usa-button usa-button-primary' v-on:click="edit">
|
||||||
{{ ("task_orders.invitations." + officer_type + ".invite_button_text") | translate }}
|
{{ ("task_orders.invitations." + officer_type + ".invite_button_text") | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -111,13 +124,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="officer__actions">
|
<div class="officer__actions">
|
||||||
<button type='button' class='usa-button usa-button-primary'>
|
<button v-if="!editing" type='button' class='usa-button usa-button-primary' v-on:click="edit">
|
||||||
{{ ("task_orders.invitations." + officer_type + ".add_button_text") | translate }}
|
{{ ("task_orders.invitations." + officer_type + ".add_button_text") | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ EditOfficerInfo(form, officer_type) }}
|
{{ EditOfficerInfo(form, officer_type, invited) }}
|
||||||
</div>
|
</div>
|
||||||
</edit-officer-form>
|
</edit-officer-form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.funding" | translate %}
|
||||||
|
|
||||||
{% from "components/edit_link.html" import EditLink %}
|
{% from "components/edit_link.html" import EditLink %}
|
||||||
{% from "components/required_label.html" import RequiredLabel %}
|
{% from "components/required_label.html" import RequiredLabel %}
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
@ -7,6 +9,7 @@
|
|||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
{% from "components/alert.html" import Alert %}
|
{% from "components/alert.html" import Alert %}
|
||||||
{% from "components/review_field.html" import ReviewField %}
|
{% from "components/review_field.html" import ReviewField %}
|
||||||
|
{% from "components/upload_input.html" import UploadInput %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -14,21 +17,26 @@
|
|||||||
|
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
|
{% block form_action %}
|
||||||
<form method='POST' action="{{ url_for('portfolios.submit_ko_review', portfolio_id=portfolio.id, task_order_id=task_order.id, form=form) }}" autocomplete="off" enctype="multipart/form-data">
|
<form method='POST' action="{{ url_for('portfolios.submit_ko_review', portfolio_id=portfolio.id, task_order_id=task_order.id, form=form) }}" autocomplete="off" enctype="multipart/form-data">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
|
|
||||||
{% set message = "task_orders.ko_review.submitted_by" | translate({"name": task_order.creator.full_name}) %}
|
<div class="top-message">
|
||||||
|
<h1 class="subheading title">
|
||||||
{{ Alert(("task_orders.ko_review.alert_title" | translate), message, level='warning',
|
{{ "task_orders.ko_review.title" | translate }}
|
||||||
fragment="fragments/ko_review_alert.html") }}
|
</h1>
|
||||||
|
{% include "fragments/ko_review_message.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|
||||||
<div class="panel__heading">
|
<div class="panel__heading">
|
||||||
<h1 class="task-order-form__heading subheading">
|
<h1 class="task-order-form__heading subheading">
|
||||||
<div class="h2">{{ "task_orders.ko_review.title" | translate }}</div>
|
<div class="h2">{{ "task_orders.ko_review.review_title" | translate }}</div>
|
||||||
{{ "task_orders.new.review.section_title"| translate }}
|
{{ "task_orders.new.review.section_title"| translate }}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@ -58,11 +66,7 @@
|
|||||||
<div class="h2">{{ "task_orders.ko_review.task_order_information"| translate }}</div>
|
<div class="h2">{{ "task_orders.ko_review.task_order_information"| translate }}</div>
|
||||||
|
|
||||||
<div class="form__sub-fields">
|
<div class="form__sub-fields">
|
||||||
<div class="usa-input">
|
{{ UploadInput(form.pdf) }}
|
||||||
<div class="usa-input__title">{{ form.pdf.label }}</div>
|
|
||||||
{{ form.pdf.description }}
|
|
||||||
{{ form.pdf }}
|
|
||||||
</div>
|
|
||||||
{{ TextInput(form.number) }}
|
{{ TextInput(form.number) }}
|
||||||
{{ TextInput(form.loa) }}
|
{{ TextInput(form.loa) }}
|
||||||
{{ TextInput(form.custom_clauses, paragraph=True) }}
|
{{ TextInput(form.custom_clauses, paragraph=True) }}
|
||||||
|
@ -1,22 +1,47 @@
|
|||||||
{% extends "portfolios/base.html" %}
|
{% extends "portfolios/base.html" %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.funding" | translate %}
|
||||||
|
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
|
||||||
{% macro Step(title="", description="", link_text=None, complete=True) %}
|
{% macro officer_name(officer) -%}
|
||||||
<div class="task-order-next-steps__step panel__content row">
|
{%- if not officer -%}
|
||||||
|
Not specified
|
||||||
|
{%- elif officer == g.current_user -%}
|
||||||
|
You
|
||||||
|
{%- else -%}
|
||||||
|
{{ officer.full_name }}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{% macro Step(description="", complete=True, button_text=None, button_url=None) %}
|
||||||
|
<div class="task-order-next-steps__step panel__content">
|
||||||
|
<div class="row">
|
||||||
<div class="task-order-next-steps__icon col">
|
<div class="task-order-next-steps__icon col">
|
||||||
<span>{{ Icon("ok", classes="complete" if complete else "incomplete") }}</span>
|
{% if complete %}
|
||||||
|
<span class="label label--success">Completed</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="label">Not Started</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="task-order-next-steps__text col">
|
<div class="task-order-next-steps__text col col--grow">
|
||||||
<div class="task-order-next-steps__heading row">
|
<div class="task-order-next-steps__heading row">
|
||||||
<h4>{{ title }}</h4>
|
<span>{{ description }}</span>
|
||||||
</div>
|
|
||||||
<div class="task-order-next-steps__description">
|
|
||||||
{{ description }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="task-order-next-steps__action col">
|
||||||
|
{% if not task_order.is_active and button_text and button_url %}
|
||||||
|
<a
|
||||||
|
href="{{ button_url }}"
|
||||||
|
class="usa-button usa-button-primary">
|
||||||
|
{{ button_text }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if caller %}
|
{% if caller %}
|
||||||
{{ caller() }}
|
{{ caller() }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -27,7 +52,7 @@
|
|||||||
{% set disabled = not link_url %}
|
{% set disabled = not link_url %}
|
||||||
<div class="task-order-document-link">
|
<div class="task-order-document-link">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<a href="{{ link_url }}" class="icon-link {{ 'icon-link--disabled' if disabled }}" aria-disabled="{{ 'true' if disabled else 'false' }}">
|
<a {% if not disabled %}href="{{ link_url }}"{% endif %} class="icon-link {{ 'icon-link--disabled' if disabled }}" aria-disabled="{{ 'true' if disabled else 'false' }}">
|
||||||
<div class="task-order-document-link__icon col">
|
<div class="task-order-document-link__icon col">
|
||||||
<span>{{ Icon("download") }}</span>
|
<span>{{ Icon("download") }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -44,15 +69,28 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro InvitationStatus(title, officer) %}
|
{% macro InvitationStatus(title, officer, officer_info) %}
|
||||||
{% set class = "invited" if officer else "uninvited" %}
|
{% set class = "invited" if officer else "uninvited" %}
|
||||||
<div class="task-order-invitation-status row">
|
<div class="task-order-invitation-status row">
|
||||||
<div class="task-order-invitation-status__icon col">
|
<div class="task-order-invitation-status__icon col">
|
||||||
<span>{{ Icon("ok" if officer else "alert", classes=class) }}</span>
|
<span>{{ Icon("avatar" if officer else "alert", classes=class) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-order-invitation-status__title col {{ class }}">
|
<div class="col">
|
||||||
|
<div class="task-order-invitation-status__title {{ class }}">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="task-order-invitation-details">
|
||||||
|
{% if officer_info %}
|
||||||
|
<div class="col">
|
||||||
|
<div>{{ officer_info.first_name }} {{ officer_info.last_name }}</div>
|
||||||
|
<div>{{ officer_info.email }}</div>
|
||||||
|
<div>{{ officer_info.phone_number | usPhone }}</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span>Not specified</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
@ -85,42 +123,42 @@
|
|||||||
<div class="task-order-details">
|
<div class="task-order-details">
|
||||||
<div id="next-steps" class="task-order-next-steps">
|
<div id="next-steps" class="task-order-next-steps">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3 class="task-order-next-steps__panel-head panel__content">What's next?</h3>
|
<h3 class="task-order-next-steps__panel-head panel__content">{{ "task_orders.view.whats_next" | translate }}</h3>
|
||||||
{% call Step(
|
{% call Step(
|
||||||
title="Submit draft Task Order",
|
description="task_orders.view.steps.draft" | translate({
|
||||||
description="Complete initial task order request form.",
|
"contact": officer_name(task_order.creator)
|
||||||
link_text="edit",
|
})| safe,
|
||||||
|
button_url=url_for("task_orders.new", screen=1, task_order_id=task_order.id),
|
||||||
|
button_text='Edit',
|
||||||
complete=all_sections_complete) %}
|
complete=all_sections_complete) %}
|
||||||
<div class="task-order-next-steps__action col">
|
|
||||||
{% if user == task_order.contracting_officer %}
|
|
||||||
{% set url=url_for("portfolios.ko_review", portfolio_id=portfolio.id, task_order_id=task_order.id) %}
|
|
||||||
{% else %}
|
|
||||||
{% set url = url_for("task_orders.new", screen=1, task_order_id=task_order.id) %}
|
|
||||||
{% endif %}
|
|
||||||
<a
|
|
||||||
href="{{ url }}"
|
|
||||||
class="usa-button usa-button-primary">
|
|
||||||
Edit
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{{ Step(
|
{{ Step(
|
||||||
title="Complete a Security Requirements Document",
|
description="task_orders.view.steps.security" | translate({
|
||||||
description="The IA Security Official you specified received an email invitation to complete and sign a DD-254: Security Requirements document that's been customized for the JEDI program here.",
|
"security_officer": officer_name(task_order.security_officer)
|
||||||
|
}) | safe,
|
||||||
complete=False) }}
|
complete=False) }}
|
||||||
|
{% call Step(
|
||||||
|
description="task_orders.view.steps.record" | translate({
|
||||||
|
"contracting_officer": officer_name(task_order.contracting_officer),
|
||||||
|
"contracting_officer_representative": officer_name(task_order.contracting_officer_representative)
|
||||||
|
}) | safe,
|
||||||
|
complete=False) %}
|
||||||
|
<div class='alert alert--warning'>
|
||||||
|
<div class='alert__content'>
|
||||||
|
<div class='alert__message'>
|
||||||
|
{{ "task_orders.view.steps.record_description" | translate | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endcall %}
|
||||||
|
{% set is_ko = user == task_order.contracting_officer %}
|
||||||
{{ Step(
|
{{ Step(
|
||||||
title="Prepare the Task Order Documents for your organization's contracting system",
|
description="task_orders.view.steps.sign" | translate({
|
||||||
description="You'll file your task order in your organization's contracting system. Change the formatting based on your office prefers.",
|
"contracting_officer": officer_name(task_order.contracting_officer)
|
||||||
|
}) | safe,
|
||||||
|
button_url=is_ko and url_for("portfolios.ko_review", portfolio_id=portfolio.id, task_order_id=task_order.id),
|
||||||
|
button_text=is_ko and 'Sign',
|
||||||
complete=False) }}
|
complete=False) }}
|
||||||
{{ Step(
|
|
||||||
title="Get a funding document",
|
|
||||||
description="User your organization's normal process to get a funding document, typically from your financial manager. Your Contracting Officer's Representative (COR) or Contracting Officer (KO) can help with this, too.",
|
|
||||||
complete=False) }}
|
|
||||||
{{ Step(
|
|
||||||
title="Have your KO submit your final task order",
|
|
||||||
description="Your KO will submit the final task order into your organization's contracting system and receive an official task order number. Your KO should enter your task order number in this system, along with a copy of the submitted task order.",
|
|
||||||
complete=False) }}
|
|
||||||
<h4 class="panel__content">Once your required information is submitted in this system, you're funded and ready to start using JEDI cloud services!</h4>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-order-sidebar col">
|
<div class="task-order-sidebar col">
|
||||||
@ -133,13 +171,23 @@
|
|||||||
format="M/D/YYYY">
|
format="M/D/YYYY">
|
||||||
</local-datetime>
|
</local-datetime>
|
||||||
{%- endset %}
|
{%- endset %}
|
||||||
|
{% if task_order.pdf %}
|
||||||
|
{{ DocumentLink(
|
||||||
|
title="Task Order",
|
||||||
|
link_url=url_for('task_orders.download_task_order_pdf', task_order_id=task_order.id),
|
||||||
|
description=description) }}
|
||||||
|
{% else %}
|
||||||
{{ DocumentLink(
|
{{ DocumentLink(
|
||||||
title="Task Order Draft",
|
title="Task Order Draft",
|
||||||
link_url=all_sections_complete and url_for('task_orders.download_summary', task_order_id=task_order.id),
|
link_url=all_sections_complete and url_for('task_orders.download_summary', task_order_id=task_order.id),
|
||||||
description=description) }}
|
description=description) }}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
|
{{ DocumentLink(
|
||||||
|
title="Instruction Sheet",
|
||||||
|
link_url="#") }}
|
||||||
{{ DocumentLink(
|
{{ DocumentLink(
|
||||||
title="Cloud Services Estimate",
|
title="Cloud Services Estimate",
|
||||||
link_url=task_order.csp_estimate and url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) ) }}
|
link_url=task_order.csp_estimate and url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) ) }}
|
||||||
@ -153,16 +201,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="task-order-invitations panel">
|
<div class="task-order-invitations panel">
|
||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
|
<div class="task-order-invitations__heading row">
|
||||||
<h3>Invitations</h3>
|
<h3>Invitations</h3>
|
||||||
{{ InvitationStatus('Contracting Officer', task_order.contracting_officer) }}
|
|
||||||
{{ InvitationStatus('Contracting Officer Representative', task_order.contracting_officer_representative) }}
|
|
||||||
{{ InvitationStatus('IA Security Officer', officer=task_order.security_officer) }}
|
|
||||||
|
|
||||||
<a href="{{ url_for('portfolios.task_order_invitations', portfolio_id=portfolio.id, task_order_id=task_order.id) }}" class="icon-link">
|
<a href="{{ url_for('portfolios.task_order_invitations', portfolio_id=portfolio.id, task_order_id=task_order.id) }}" class="icon-link">
|
||||||
|
<span>manage</span>
|
||||||
{{ Icon("edit") }}
|
{{ Icon("edit") }}
|
||||||
<span>manage invitations</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{{ InvitationStatus('Contracting Officer', task_order.contracting_officer, officer_info=task_order.officer_dictionary('contracting_officer')) }}
|
||||||
|
{{ InvitationStatus('Contracting Officer Representative', task_order.contracting_officer_representative, officer_info=task_order.officer_dictionary('contracting_officer_representative')) }}
|
||||||
|
{{ InvitationStatus('IA Security Officer', officer=task_order.security_officer, officer_info=task_order.officer_dictionary('security_officer')) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
|
|
||||||
|
|
||||||
<!-- App Info Section -->
|
<!-- App Info Section -->
|
||||||
<h3 class="task-order-form__heading subheading">{{ "task_orders.new.app_info.basic_info_title"| translate }}</h3>
|
<h3 class="task-order-form__heading subheading">{{ "task_orders.new.app_info.basic_info_title"| translate }}</h3>
|
||||||
{{ TextInput(form.portfolio_name, placeholder="The name of your office or organization", validation="portfolioName") }}
|
{{ TextInput(form.portfolio_name, placeholder="The name of your office or organization", validation="portfolioName") }}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
{% from "components/options_input.html" import OptionsInput %}
|
{% from "components/options_input.html" import OptionsInput %}
|
||||||
{% from "components/date_input.html" import DateInput %}
|
{% from "components/date_input.html" import DateInput %}
|
||||||
|
{% from "components/upload_input.html" import UploadInput %}
|
||||||
|
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
@ -32,22 +33,7 @@
|
|||||||
{{ Icon("link")}} Go to Cloud Service Provider’s estimate calculator
|
{{ Icon("link")}} Go to Cloud Service Provider’s estimate calculator
|
||||||
</a></p>
|
</a></p>
|
||||||
<p>{{ "task_orders.new.funding.estimate_usage_paragraph" | translate }}</p>
|
<p>{{ "task_orders.new.funding.estimate_usage_paragraph" | translate }}</p>
|
||||||
<template v-if="showUpload">
|
{{ UploadInput(form.csp_estimate, show_label=True) }}
|
||||||
<div class="usa-input {% if form.csp_estimate.errors %} usa-input--error {% endif %}">
|
|
||||||
{{ form.csp_estimate.label }}
|
|
||||||
{{ form.csp_estimate.description }}
|
|
||||||
{{ form.csp_estimate }}
|
|
||||||
{% for error in form.csp_estimate.errors %}
|
|
||||||
<span class="usa-input__message">{{error}}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<p>Uploaded {{ form.csp_estimate.data.filename }}</p>
|
|
||||||
<div>
|
|
||||||
<button type="button" v-on:click="showUploadInput">Change</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<h1 class="panel__content">Let's get started</h1>
|
<h1 class="panel__content">Let's get started</h1>
|
||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
<p class="centered">
|
<p class="centered">
|
||||||
To create a portfolio of JEDI cloud applications, you'll complete and submit a task order in your organization's system of record. We'll help you complete the necessary pieces of that task order, including your:
|
To create a portfolio of JEDI cloud applications, you'll need to submit a task order in your organization's system of record. We'll walk you through the necessary steps of that task order, including the following:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="task-order-get-started__list panel__content">
|
<span class="task-order-get-started__list panel__content">
|
||||||
@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel task-order-needs">
|
<div class="panel task-order-needs">
|
||||||
<h1 class="panel__content">You'll need help getting a task order</h1>
|
<h1 class="panel__content">You'll need a little help getting a task order</h1>
|
||||||
<div class="panel__content task-order-needs__list">
|
<div class="panel__content task-order-needs__list">
|
||||||
{{ Help(
|
{{ Help(
|
||||||
name="Development Lead",
|
name="Development Lead",
|
||||||
@ -43,13 +43,13 @@
|
|||||||
{{ Help(
|
{{ Help(
|
||||||
name="Security Lead",
|
name="Security Lead",
|
||||||
icon_name="shield",
|
icon_name="shield",
|
||||||
description="Your security lead will review and approve your security classification needs, as well as a standardized DD-254.",
|
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") }}
|
link_text="You'll need their DoD ID number") }}
|
||||||
{{ Help(
|
{{ Help(
|
||||||
name="Contracting Officer",
|
name="Contracting Officer",
|
||||||
icon_name="dollar-sign",
|
icon_name="dollar-sign",
|
||||||
description="Your contracting officer will review your funding needs and ultimately approve your task order.",
|
description="Your contracting officer will review your funding needs and ultimately approve your task order.",
|
||||||
link_text="You'll need their DOD ID number") }}
|
link_text="You'll need their DoD ID number") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -62,7 +62,7 @@
|
|||||||
<a href="{{ url_for("task_orders.new", screen=1) }}" class="usa-button usa-button-big">Let's do cloud!</a>
|
<a href="{{ url_for("task_orders.new", screen=1) }}" class="usa-button usa-button-big">Let's do cloud!</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="centered">Create a JEDI cloud application portfolio & start building a task order</p>
|
<p class="centered">Create a portfolio by starting a new task order</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
{{ CheckboxInput(form.ko_invite) }}
|
{{ CheckboxInput(form.ko_invite) }}
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<dodid v-bind:initial-invite="ko_invite" inline-template v-if="ko_invite">
|
<dodid v-bind:initial-invite="ko_invite" inline-template v-if="ko_invite">
|
||||||
{{ TextInput(form.ko_dod_id, placeholder="1234567890", tooltip="Why", tooltip_title='Why', validation='dodId', classes="task-order__invite-officer")}}
|
{{ 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")}}
|
||||||
</dodid>
|
</dodid>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
|
|
||||||
@ -35,7 +35,7 @@
|
|||||||
{{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_phone_number) }}
|
{{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_phone_number) }}
|
||||||
{{ CheckboxInput(form.cor_invite) }}
|
{{ CheckboxInput(form.cor_invite) }}
|
||||||
<template v-if="cor_invite">
|
<template v-if="cor_invite">
|
||||||
{{ TextInput(form.cor_dod_id, placeholder="1234567890", tooltip="Why", tooltip_title='Why', validation='dodId', classes="task-order__invite-officer")}}
|
{{ TextInput(form.cor_dod_id, placeholder="1234567890", tooltip="task_orders.new.oversight.dod_id_tooltip" | translate, tooltip_title='Why', validation='dodId', classes="task-order__invite-officer")}}
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</cordata>
|
</cordata>
|
||||||
@ -49,10 +49,11 @@
|
|||||||
{{ CheckboxInput(form.so_invite) }}
|
{{ CheckboxInput(form.so_invite) }}
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<dodid v-bind:initial-invite="so_invite" inline-template v-if="so_invite">
|
<dodid v-bind:initial-invite="so_invite" inline-template v-if="so_invite">
|
||||||
{{ TextInput(form.so_dod_id, placeholder="1234567890", tooltip="Why", tooltip_title='Why', validation='dodId', classes="task-order__invite-officer")}}
|
{{ 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")}}
|
||||||
</dodid>
|
</dodid>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
</oversight>
|
</oversight>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
56
templates/task_orders/signing/signature_requested.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
{% from "components/checkbox_input.html" import CheckboxInput %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST" action='{{ url_for("task_orders.record_signature", task_order_id=task_order_id) }}'>
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
<div class="row row--pad">
|
||||||
|
<div class="col col--pad">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel__heading">
|
||||||
|
<h1 class="task-order-form__heading subheading">
|
||||||
|
<div class="h2">{{ "task_orders.sign.task_order_builder_title" | translate }}</div>
|
||||||
|
{{ "task_orders.sign.title" | translate }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel__content">
|
||||||
|
<div is="levelofwarrant" inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
||||||
|
<div>
|
||||||
|
<span v-bind:class="{ hide: !unlimited_level_of_warrant }">
|
||||||
|
{{ TextInput(form.level_of_warrant, validation='dollars', placeholder='$0.00', disabled=True) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-bind:class="{ hide: unlimited_level_of_warrant }">
|
||||||
|
{{ TextInput(form.level_of_warrant, validation='dollars', placeholder='$0.00') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
{{ CheckboxInput(form.unlimited_level_of_warrant) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ CheckboxInput(form.signature) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-group">
|
||||||
|
<button class="usa-button usa-button-big usa-button-primary">
|
||||||
|
{{ "common.sign" | translate }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{{ request.referrer or url_for("atst.home") }}"
|
||||||
|
class="action-group__action icon-link">
|
||||||
|
{{ Icon('caret_left') }}
|
||||||
|
<span class="icon icon--x"></span>
|
||||||
|
{{ "common.back" | translate }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
22
tests/models/test_application.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from atst.domain.environments import Environments
|
||||||
|
from tests.factories import ApplicationFactory, UserFactory
|
||||||
|
|
||||||
|
|
||||||
|
def test_application_num_users():
|
||||||
|
application = ApplicationFactory.create(
|
||||||
|
environments=[{"name": "dev"}, {"name": "staging"}, {"name": "prod"}]
|
||||||
|
)
|
||||||
|
assert application.num_users == 0
|
||||||
|
|
||||||
|
first_env = application.environments[0]
|
||||||
|
user1 = UserFactory()
|
||||||
|
Environments.add_member(first_env, user1, "developer")
|
||||||
|
assert application.num_users == 1
|
||||||
|
|
||||||
|
second_env = application.environments[-1]
|
||||||
|
Environments.add_member(second_env, user1, "developer")
|
||||||
|
assert application.num_users == 1
|
||||||
|
|
||||||
|
user2 = UserFactory()
|
||||||
|
Environments.add_member(second_env, user2, "developer")
|
||||||
|
assert application.num_users == 2
|
@ -9,11 +9,14 @@ from tests.mocks import PDF_FILENAME
|
|||||||
|
|
||||||
|
|
||||||
class TestTaskOrderStatus:
|
class TestTaskOrderStatus:
|
||||||
def test_pending_status(self):
|
def test_started_status(self):
|
||||||
to = TaskOrder()
|
to = TaskOrder()
|
||||||
assert to.status == Status.PENDING
|
assert to.status == Status.STARTED
|
||||||
|
|
||||||
to = TaskOrder(number="42", start_date=random_future_date())
|
def test_pending_status(self):
|
||||||
|
to = TaskOrder(
|
||||||
|
number="42", start_date=random_future_date(), end_date=random_future_date()
|
||||||
|
)
|
||||||
assert to.status == Status.PENDING
|
assert to.status == Status.PENDING
|
||||||
|
|
||||||
def test_active_status(self):
|
def test_active_status(self):
|
||||||
@ -47,7 +50,7 @@ class TestCSPEstimate:
|
|||||||
attachment = Attachment(filename="sample.pdf", object_name="sample")
|
attachment = Attachment(filename="sample.pdf", object_name="sample")
|
||||||
to.csp_estimate = attachment
|
to.csp_estimate = attachment
|
||||||
|
|
||||||
assert to.attachment_id == attachment.id
|
assert to.csp_attachment_id == attachment.id
|
||||||
|
|
||||||
def test_setting_estimate_with_file_storage(self):
|
def test_setting_estimate_with_file_storage(self):
|
||||||
to = TaskOrder()
|
to = TaskOrder()
|
||||||
@ -77,3 +80,41 @@ class TestCSPEstimate:
|
|||||||
|
|
||||||
to.csp_estimate = ""
|
to.csp_estimate = ""
|
||||||
assert to.csp_estimate is None
|
assert to.csp_estimate is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestPDF:
|
||||||
|
def test_setting_pdf_with_attachment(self):
|
||||||
|
to = TaskOrder()
|
||||||
|
attachment = Attachment(filename="sample.pdf", object_name="sample")
|
||||||
|
to.pdf = attachment
|
||||||
|
|
||||||
|
assert to.pdf_attachment_id == attachment.id
|
||||||
|
|
||||||
|
def test_setting_pdf_with_file_storage(self):
|
||||||
|
to = TaskOrder()
|
||||||
|
with open(PDF_FILENAME, "rb") as fp:
|
||||||
|
fs = FileStorage(fp, content_type="application/pdf")
|
||||||
|
to.pdf = fs
|
||||||
|
|
||||||
|
assert to.pdf is not None
|
||||||
|
assert to.pdf.filename == PDF_FILENAME
|
||||||
|
|
||||||
|
def test_setting_pdf_with_invalid_object(self):
|
||||||
|
to = TaskOrder()
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
to.pdf = "invalid"
|
||||||
|
|
||||||
|
def test_setting_pdf_with_empty_value(self):
|
||||||
|
to = TaskOrder()
|
||||||
|
assert to.pdf is None
|
||||||
|
|
||||||
|
to.pdf = ""
|
||||||
|
assert to.pdf is None
|
||||||
|
|
||||||
|
def test_removing_pdf(self):
|
||||||
|
attachment = Attachment(filename="sample.pdf", object_name="sample")
|
||||||
|
to = TaskOrder(pdf=attachment)
|
||||||
|
assert to.pdf is not None
|
||||||
|
|
||||||
|
to.pdf = ""
|
||||||
|
assert to.pdf is None
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import pytest
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
@ -20,7 +21,7 @@ def test_user_with_permission_has_budget_report_link(client, user_session):
|
|||||||
user_session(portfolio.owner)
|
user_session(portfolio.owner)
|
||||||
response = client.get("/portfolios/{}/applications".format(portfolio.id))
|
response = client.get("/portfolios/{}/applications".format(portfolio.id))
|
||||||
assert (
|
assert (
|
||||||
'href="/portfolios/{}/reports"'.format(portfolio.id).encode() in response.data
|
"href='/portfolios/{}/reports'".format(portfolio.id).encode() in response.data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ def test_user_without_permission_has_no_budget_report_link(client, user_session)
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Temporarily no add activity log link")
|
||||||
def test_user_with_permission_has_activity_log_link(client, user_session):
|
def test_user_with_permission_has_activity_log_link(client, user_session):
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
ccpo = UserFactory.from_atat_role("ccpo")
|
ccpo = UserFactory.from_atat_role("ccpo")
|
||||||
@ -69,6 +71,7 @@ def test_user_with_permission_has_activity_log_link(client, user_session):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Temporarily no add activity log link")
|
||||||
def test_user_without_permission_has_no_activity_log_link(client, user_session):
|
def test_user_without_permission_has_no_activity_log_link(client, user_session):
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
developer = UserFactory.create()
|
developer = UserFactory.create()
|
||||||
@ -87,6 +90,7 @@ def test_user_without_permission_has_no_activity_log_link(client, user_session):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Temporarily no add application link")
|
||||||
def test_user_with_permission_has_add_application_link(client, user_session):
|
def test_user_with_permission_has_add_application_link(client, user_session):
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
user_session(portfolio.owner)
|
user_session(portfolio.owner)
|
||||||
@ -97,6 +101,7 @@ def test_user_with_permission_has_add_application_link(client, user_session):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Temporarily no add application link")
|
||||||
def test_user_without_permission_has_no_add_application_link(client, user_session):
|
def test_user_without_permission_has_no_add_application_link(client, user_session):
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
|
@ -212,27 +212,20 @@ def test_existing_member_invite_resent_to_email_submitted_in_form(
|
|||||||
|
|
||||||
def test_contracting_officer_accepts_invite(monkeypatch, client, user_session):
|
def test_contracting_officer_accepts_invite(monkeypatch, client, user_session):
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
task_order = TaskOrderFactory.create(portfolio=portfolio)
|
|
||||||
user_info = UserFactory.dictionary()
|
user_info = UserFactory.dictionary()
|
||||||
|
task_order = TaskOrderFactory.create(
|
||||||
|
portfolio=portfolio,
|
||||||
|
ko_first_name=user_info["first_name"],
|
||||||
|
ko_last_name=user_info["last_name"],
|
||||||
|
ko_email=user_info["email"],
|
||||||
|
ko_phone_number=user_info["phone_number"],
|
||||||
|
ko_dod_id=user_info["dod_id"],
|
||||||
|
ko_invite=True,
|
||||||
|
)
|
||||||
|
|
||||||
# create contracting officer
|
# create contracting officer
|
||||||
user_session(portfolio.owner)
|
user_session(portfolio.owner)
|
||||||
client.post(
|
client.post(url_for("task_orders.invite", task_order_id=task_order.id))
|
||||||
url_for("task_orders.new", screen=3, task_order_id=task_order.id),
|
|
||||||
data={
|
|
||||||
"portfolio_role": "contracting_officer",
|
|
||||||
"ko_first_name": user_info["first_name"],
|
|
||||||
"ko_last_name": user_info["last_name"],
|
|
||||||
"ko_email": user_info["email"],
|
|
||||||
"ko_phone_number": user_info["phone_number"],
|
|
||||||
"ko_dod_id": user_info["dod_id"],
|
|
||||||
"cor_phone_number": user_info["phone_number"],
|
|
||||||
"so_phone_number": user_info["phone_number"],
|
|
||||||
"so_dod_id": task_order.so_dod_id,
|
|
||||||
"cor_dod_id": task_order.cor_dod_id,
|
|
||||||
"ko_invite": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# contracting officer accepts invitation
|
# contracting officer accepts invitation
|
||||||
user = Users.get_by_dod_id(user_info["dod_id"])
|
user = Users.get_by_dod_id(user_info["dod_id"])
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import pytest
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
@ -36,6 +37,7 @@ def create_portfolio_and_invite_user(
|
|||||||
return portfolio
|
return portfolio
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Temporarily no add member link")
|
||||||
def test_user_with_permission_has_add_member_link(client, user_session):
|
def test_user_with_permission_has_add_member_link(client, user_session):
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
user_session(portfolio.owner)
|
user_session(portfolio.owner)
|
||||||
@ -46,6 +48,7 @@ def test_user_with_permission_has_add_member_link(client, user_session):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Temporarily no add member link")
|
||||||
def test_user_without_permission_has_no_add_member_link(client, user_session):
|
def test_user_without_permission_has_no_add_member_link(client, user_session):
|
||||||
user = UserFactory.create()
|
user = UserFactory.create()
|
||||||
portfolio = PortfolioFactory.create()
|
portfolio = PortfolioFactory.create()
|
||||||
@ -89,6 +92,7 @@ def test_create_member(client, user_session):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
assert user.full_name in response.data.decode()
|
||||||
assert user.has_portfolios
|
assert user.has_portfolios
|
||||||
assert user.invitations
|
assert user.invitations
|
||||||
assert len(queue.get_queue()) == queue_length + 1
|
assert len(queue.get_queue()) == queue_length + 1
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from tests.factories import PortfolioFactory
|
from tests.factories import PortfolioFactory, UserFactory
|
||||||
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
|
|
||||||
def test_update_portfolio_name(client, user_session):
|
def test_update_portfolio_name(client, user_session):
|
||||||
@ -13,3 +14,29 @@ def test_update_portfolio_name(client, user_session):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert portfolio.name == "a cool new name"
|
assert portfolio.name == "a cool new name"
|
||||||
|
|
||||||
|
|
||||||
|
def test_portfolio_index_with_existing_portfolios(client, user_session):
|
||||||
|
portfolio = PortfolioFactory.create()
|
||||||
|
user_session(portfolio.owner)
|
||||||
|
|
||||||
|
response = client.get(url_for("portfolios.portfolios"))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert portfolio.name.encode("utf8") in response.data
|
||||||
|
assert (
|
||||||
|
translate("portfolios.index.empty.start_button").encode("utf8")
|
||||||
|
not in response.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_portfolio_index_without_existing_portfolios(client, user_session):
|
||||||
|
user = UserFactory.create()
|
||||||
|
user_session(user)
|
||||||
|
|
||||||
|
response = client.get(url_for("portfolios.portfolios"))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert (
|
||||||
|
translate("portfolios.index.empty.start_button").encode("utf8") in response.data
|
||||||
|
)
|
||||||
|