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:
|
||||
|
||||
* `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/)
|
||||
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`
|
||||
ATST requires `pipenv` to be installed for python dependency management. `pipenv`
|
||||
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).
|
||||
|
||||
* `yarn`
|
||||
@ -35,7 +36,10 @@ locally:
|
||||
|
||||
* `postgres` >= 9.6
|
||||
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.
|
||||
|
||||
* `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 collections import OrderedDict
|
||||
import pendulum
|
||||
@ -31,131 +32,152 @@ class MockApplication:
|
||||
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):
|
||||
FIXTURE_MONTHS = generate_sample_dates()
|
||||
|
||||
MONTHLY_SPEND_BY_ENVIRONMENT = {
|
||||
"LC04_Integ": {
|
||||
"02/2018": 284,
|
||||
"03/2018": 1210,
|
||||
"04/2018": 1430,
|
||||
"05/2018": 1366,
|
||||
"06/2018": 1169,
|
||||
"07/2018": 991,
|
||||
"08/2018": 978,
|
||||
"09/2018": 737,
|
||||
FIXTURE_MONTHS[7]: 284,
|
||||
FIXTURE_MONTHS[6]: 1210,
|
||||
FIXTURE_MONTHS[5]: 1430,
|
||||
FIXTURE_MONTHS[4]: 1366,
|
||||
FIXTURE_MONTHS[3]: 1169,
|
||||
FIXTURE_MONTHS[2]: 991,
|
||||
FIXTURE_MONTHS[1]: 978,
|
||||
FIXTURE_MONTHS[0]: 737,
|
||||
},
|
||||
"LC04_PreProd": {
|
||||
"02/2018": 812,
|
||||
"03/2018": 1389,
|
||||
"04/2018": 1425,
|
||||
"05/2018": 1306,
|
||||
"06/2018": 1112,
|
||||
"07/2018": 936,
|
||||
"08/2018": 921,
|
||||
"09/2018": 694,
|
||||
FIXTURE_MONTHS[7]: 812,
|
||||
FIXTURE_MONTHS[6]: 1389,
|
||||
FIXTURE_MONTHS[5]: 1425,
|
||||
FIXTURE_MONTHS[4]: 1306,
|
||||
FIXTURE_MONTHS[3]: 1112,
|
||||
FIXTURE_MONTHS[2]: 936,
|
||||
FIXTURE_MONTHS[1]: 921,
|
||||
FIXTURE_MONTHS[0]: 694,
|
||||
},
|
||||
"LC04_Prod": {
|
||||
"02/2018": 1742,
|
||||
"03/2018": 1716,
|
||||
"04/2018": 1866,
|
||||
"05/2018": 1809,
|
||||
"06/2018": 1839,
|
||||
"07/2018": 1633,
|
||||
"08/2018": 1654,
|
||||
"09/2018": 1103,
|
||||
FIXTURE_MONTHS[7]: 1742,
|
||||
FIXTURE_MONTHS[6]: 1716,
|
||||
FIXTURE_MONTHS[5]: 1866,
|
||||
FIXTURE_MONTHS[4]: 1809,
|
||||
FIXTURE_MONTHS[3]: 1839,
|
||||
FIXTURE_MONTHS[2]: 1633,
|
||||
FIXTURE_MONTHS[1]: 1654,
|
||||
FIXTURE_MONTHS[0]: 1103,
|
||||
},
|
||||
"SF18_Integ": {
|
||||
"04/2018": 1498,
|
||||
"05/2018": 1400,
|
||||
"06/2018": 1394,
|
||||
"07/2018": 1171,
|
||||
"08/2018": 1200,
|
||||
"09/2018": 963,
|
||||
FIXTURE_MONTHS[5]: 1498,
|
||||
FIXTURE_MONTHS[4]: 1400,
|
||||
FIXTURE_MONTHS[3]: 1394,
|
||||
FIXTURE_MONTHS[2]: 1171,
|
||||
FIXTURE_MONTHS[1]: 1200,
|
||||
FIXTURE_MONTHS[0]: 963,
|
||||
},
|
||||
"SF18_PreProd": {
|
||||
"04/2018": 1780,
|
||||
"05/2018": 1667,
|
||||
"06/2018": 1703,
|
||||
"07/2018": 1474,
|
||||
"08/2018": 1441,
|
||||
"09/2018": 933,
|
||||
FIXTURE_MONTHS[5]: 1780,
|
||||
FIXTURE_MONTHS[4]: 1667,
|
||||
FIXTURE_MONTHS[3]: 1703,
|
||||
FIXTURE_MONTHS[2]: 1474,
|
||||
FIXTURE_MONTHS[1]: 1441,
|
||||
FIXTURE_MONTHS[0]: 933,
|
||||
},
|
||||
"SF18_Prod": {
|
||||
"04/2018": 1686,
|
||||
"05/2018": 1779,
|
||||
"06/2018": 1792,
|
||||
"07/2018": 1570,
|
||||
"08/2018": 1539,
|
||||
"09/2018": 986,
|
||||
FIXTURE_MONTHS[5]: 1686,
|
||||
FIXTURE_MONTHS[4]: 1779,
|
||||
FIXTURE_MONTHS[3]: 1792,
|
||||
FIXTURE_MONTHS[2]: 1570,
|
||||
FIXTURE_MONTHS[1]: 1539,
|
||||
FIXTURE_MONTHS[0]: 986,
|
||||
},
|
||||
"Canton_Prod": {
|
||||
"05/2018": 28699,
|
||||
"06/2018": 26766,
|
||||
"07/2018": 22619,
|
||||
"08/2018": 24090,
|
||||
"09/2018": 16719,
|
||||
FIXTURE_MONTHS[4]: 28699,
|
||||
FIXTURE_MONTHS[3]: 26766,
|
||||
FIXTURE_MONTHS[2]: 22619,
|
||||
FIXTURE_MONTHS[1]: 24090,
|
||||
FIXTURE_MONTHS[0]: 16719,
|
||||
},
|
||||
"BD04_Integ": {},
|
||||
"BD04_PreProd": {
|
||||
"02/2018": 7019,
|
||||
"03/2018": 3004,
|
||||
"04/2018": 2691,
|
||||
"05/2018": 2901,
|
||||
"06/2018": 3463,
|
||||
"07/2018": 3314,
|
||||
"08/2018": 3432,
|
||||
"09/2018": 723,
|
||||
FIXTURE_MONTHS[7]: 7019,
|
||||
FIXTURE_MONTHS[6]: 3004,
|
||||
FIXTURE_MONTHS[5]: 2691,
|
||||
FIXTURE_MONTHS[4]: 2901,
|
||||
FIXTURE_MONTHS[3]: 3463,
|
||||
FIXTURE_MONTHS[2]: 3314,
|
||||
FIXTURE_MONTHS[1]: 3432,
|
||||
FIXTURE_MONTHS[0]: 723,
|
||||
},
|
||||
"SCV18_Dev": {"05/2019": 9797},
|
||||
"SCV18_Dev": {FIXTURE_MONTHS[1]: 9797},
|
||||
"Crown_CR Portal Dev": {
|
||||
"03/2018": 208,
|
||||
"04/2018": 457,
|
||||
"05/2018": 671,
|
||||
"06/2018": 136,
|
||||
"07/2018": 1524,
|
||||
"08/2018": 2077,
|
||||
"09/2018": 1858,
|
||||
FIXTURE_MONTHS[6]: 208,
|
||||
FIXTURE_MONTHS[5]: 457,
|
||||
FIXTURE_MONTHS[4]: 671,
|
||||
FIXTURE_MONTHS[3]: 136,
|
||||
FIXTURE_MONTHS[2]: 1524,
|
||||
FIXTURE_MONTHS[1]: 2077,
|
||||
FIXTURE_MONTHS[0]: 1858,
|
||||
},
|
||||
"Crown_CR Staging": {
|
||||
"03/2018": 208,
|
||||
"04/2018": 457,
|
||||
"05/2018": 671,
|
||||
"06/2018": 136,
|
||||
"07/2018": 1524,
|
||||
"08/2018": 2077,
|
||||
"09/2018": 1858,
|
||||
FIXTURE_MONTHS[6]: 208,
|
||||
FIXTURE_MONTHS[5]: 457,
|
||||
FIXTURE_MONTHS[4]: 671,
|
||||
FIXTURE_MONTHS[3]: 136,
|
||||
FIXTURE_MONTHS[2]: 1524,
|
||||
FIXTURE_MONTHS[1]: 2077,
|
||||
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": {
|
||||
"03/2018": 145,
|
||||
"04/2018": 719,
|
||||
"05/2018": 1243,
|
||||
"06/2018": 2214,
|
||||
"07/2018": 2959,
|
||||
"08/2018": 4151,
|
||||
"09/2018": 4260,
|
||||
FIXTURE_MONTHS[6]: 145,
|
||||
FIXTURE_MONTHS[5]: 719,
|
||||
FIXTURE_MONTHS[4]: 1243,
|
||||
FIXTURE_MONTHS[3]: 2214,
|
||||
FIXTURE_MONTHS[2]: 2959,
|
||||
FIXTURE_MONTHS[1]: 4151,
|
||||
FIXTURE_MONTHS[0]: 4260,
|
||||
},
|
||||
"NP02_Integ": {"08/2018": 284, "09/2018": 1210},
|
||||
"NP02_PreProd": {"08/2018": 812, "09/2018": 1389},
|
||||
"NP02_Prod": {"08/2018": 3742, "09/2018": 4716},
|
||||
"FM_Integ": {"08/2018": 1498},
|
||||
"FM_Prod": {"09/2018": 5686},
|
||||
"NP02_Integ": {FIXTURE_MONTHS[1]: 284, FIXTURE_MONTHS[0]: 1210},
|
||||
"NP02_PreProd": {FIXTURE_MONTHS[1]: 812, FIXTURE_MONTHS[0]: 1389},
|
||||
"NP02_Prod": {FIXTURE_MONTHS[1]: 3742, FIXTURE_MONTHS[0]: 4716},
|
||||
"FM_Integ": {FIXTURE_MONTHS[1]: 1498},
|
||||
"FM_Prod": {FIXTURE_MONTHS[0]: 5686},
|
||||
}
|
||||
|
||||
CUMULATIVE_BUDGET_AARDVARK = {
|
||||
"02/2018": {"spend": 9857, "cumulative": 9857},
|
||||
"03/2018": {"spend": 7881, "cumulative": 17738},
|
||||
"04/2018": {"spend": 14010, "cumulative": 31748},
|
||||
"05/2018": {"spend": 43510, "cumulative": 75259},
|
||||
"06/2018": {"spend": 41725, "cumulative": 116_984},
|
||||
"07/2018": {"spend": 41328, "cumulative": 158_312},
|
||||
"08/2018": {"spend": 47491, "cumulative": 205_803},
|
||||
"09/2018": {"spend": 36028, "cumulative": 241_831},
|
||||
FIXTURE_MONTHS[7]: {"spend": 9857, "cumulative": 9857},
|
||||
FIXTURE_MONTHS[6]: {"spend": 7881, "cumulative": 17738},
|
||||
FIXTURE_MONTHS[5]: {"spend": 14010, "cumulative": 31748},
|
||||
FIXTURE_MONTHS[4]: {"spend": 43510, "cumulative": 75259},
|
||||
FIXTURE_MONTHS[3]: {"spend": 41725, "cumulative": 116_984},
|
||||
FIXTURE_MONTHS[2]: {"spend": 41328, "cumulative": 158_312},
|
||||
FIXTURE_MONTHS[1]: {"spend": 47491, "cumulative": 205_803},
|
||||
FIXTURE_MONTHS[0]: {"spend": 36028, "cumulative": 241_831},
|
||||
}
|
||||
|
||||
CUMULATIVE_BUDGET_BELUGA = {
|
||||
"08/2018": {"spend": 4838, "cumulative": 4838},
|
||||
"09/2018": {"spend": 14500, "cumulative": 19338},
|
||||
FIXTURE_MONTHS[1]: {"spend": 4838, "cumulative": 4838},
|
||||
FIXTURE_MONTHS[0]: {"spend": 14500, "cumulative": 19338},
|
||||
}
|
||||
|
||||
REPORT_FIXTURE_MAP = {
|
||||
|
@ -19,7 +19,19 @@ def dollars(value):
|
||||
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):
|
||||
if not number:
|
||||
return ""
|
||||
phone = re.sub(r"\D", "", number)
|
||||
return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:])
|
||||
|
||||
@ -99,6 +111,8 @@ def normalizeOrder(title):
|
||||
def register_filters(app):
|
||||
app.jinja_env.filters["iconSvg"] = iconSvg
|
||||
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["readableInteger"] = readableInteger
|
||||
app.jinja_env.filters["getOptionLabel"] = getOptionLabel
|
||||
|
@ -1,5 +1,5 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms.fields import StringField
|
||||
from wtforms.fields import StringField, BooleanField
|
||||
from wtforms.fields.html5 import TelField
|
||||
from wtforms.validators import Email, Length, Optional
|
||||
|
||||
@ -15,6 +15,7 @@ class OfficerForm(FlaskForm):
|
||||
email = StringField("Email", validators=[Optional(), Email()])
|
||||
phone_number = TelField("Phone Number", validators=[PhoneNumber()])
|
||||
dod_id = StringField("DoD ID", validators=[Optional(), Length(min=10), IsNumber()])
|
||||
invite = BooleanField("Invite to Task Order Builder")
|
||||
|
||||
|
||||
class EditTaskOrderOfficersForm(CacheableForm):
|
||||
|
@ -222,3 +222,28 @@ class OversightForm(CacheableForm):
|
||||
|
||||
class ReviewForm(CacheableForm):
|
||||
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")
|
||||
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
|
||||
def displayname(self):
|
||||
return self.name
|
||||
|
@ -2,7 +2,16 @@ from enum import Enum
|
||||
from datetime import date
|
||||
|
||||
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.types import ARRAY
|
||||
from sqlalchemy.orm import relationship
|
||||
@ -12,6 +21,7 @@ from atst.models import Attachment, Base, types, mixins
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
STARTED = "Started"
|
||||
PENDING = "Pending"
|
||||
ACTIVE = "Active"
|
||||
EXPIRED = "Expired"
|
||||
@ -51,8 +61,8 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
start_date = Column(Date) # Period of Performance
|
||||
end_date = Column(Date)
|
||||
performance_length = Column(Integer)
|
||||
attachment_id = Column(ForeignKey("attachments.id"))
|
||||
_csp_estimate = relationship("Attachment")
|
||||
csp_attachment_id = Column(ForeignKey("attachments.id"))
|
||||
_csp_estimate = relationship("Attachment", foreign_keys=[csp_attachment_id])
|
||||
clin_01 = Column(Numeric(scale=2))
|
||||
clin_02 = 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_phone_number = Column(String) # Phone Number
|
||||
ko_dod_id = Column(String) # DOD ID
|
||||
ko_invite = Column(Boolean)
|
||||
cor_first_name = Column(String) # First Name
|
||||
cor_last_name = Column(String) # Last Name
|
||||
cor_email = Column(String) # Email
|
||||
cor_phone_number = Column(String) # Phone Number
|
||||
cor_dod_id = Column(String) # DOD ID
|
||||
cor_invite = Column(Boolean)
|
||||
so_first_name = Column(String) # First Name
|
||||
so_last_name = Column(String) # Last Name
|
||||
so_email = Column(String) # Email
|
||||
so_phone_number = Column(String) # Phone Number
|
||||
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
|
||||
loa = Column(String) # Line of Accounting (LOA)
|
||||
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
|
||||
def csp_estimate(self):
|
||||
@ -82,26 +101,38 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
|
||||
@csp_estimate.setter
|
||||
def csp_estimate(self, new_csp_estimate):
|
||||
if isinstance(new_csp_estimate, Attachment):
|
||||
self._csp_estimate = new_csp_estimate
|
||||
elif isinstance(new_csp_estimate, FileStorage):
|
||||
self._csp_estimate = Attachment.attach(
|
||||
new_csp_estimate, "task_order", self.id
|
||||
)
|
||||
elif not new_csp_estimate and self._csp_estimate:
|
||||
self._csp_estimate = None
|
||||
elif new_csp_estimate:
|
||||
raise TypeError("Could not set csp_estimate with invalid type")
|
||||
self._csp_estimate = self._set_attachment(new_csp_estimate, "_csp_estimate")
|
||||
|
||||
@hybrid_property
|
||||
def pdf(self):
|
||||
return self._pdf
|
||||
|
||||
@pdf.setter
|
||||
def pdf(self, new_pdf):
|
||||
self._pdf = self._set_attachment(new_pdf, "_pdf")
|
||||
|
||||
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
|
||||
def is_submitted(self):
|
||||
|
||||
return (
|
||||
self.number is not None
|
||||
and self.start_date is not None
|
||||
and self.end_date is not None
|
||||
)
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.status == Status.ACTIVE
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
if self.is_submitted:
|
||||
@ -112,7 +143,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
return Status.EXPIRED
|
||||
return Status.ACTIVE
|
||||
else:
|
||||
return Status.PENDING
|
||||
return Status.STARTED
|
||||
|
||||
@property
|
||||
def display_status(self):
|
||||
@ -142,6 +173,44 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
def is_pending(self):
|
||||
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):
|
||||
return {
|
||||
"portfolio_name": self.portfolio_name,
|
||||
|
@ -1,5 +1,14 @@
|
||||
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 jinja2.exceptions import TemplateNotFound
|
||||
@ -56,7 +65,7 @@ def home():
|
||||
num_portfolios = len([role for role in user.portfolio_roles if role.is_active])
|
||||
|
||||
if num_portfolios == 0:
|
||||
return redirect(url_for("requests.requests_index"))
|
||||
return redirect(url_for("portfolios.portfolios"))
|
||||
elif num_portfolios == 1:
|
||||
portfolio_role = user.portfolio_roles[0]
|
||||
portfolio_id = portfolio_role.portfolio.id
|
||||
@ -131,7 +140,9 @@ def login_redirect():
|
||||
@bp.route("/logout")
|
||||
def 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")
|
||||
|
@ -1,4 +1,5 @@
|
||||
from flask import Blueprint, request as http_request, g, render_template
|
||||
from operator import attrgetter
|
||||
|
||||
portfolios_bp = Blueprint("portfolios", __name__)
|
||||
|
||||
@ -31,4 +32,24 @@ def portfolio():
|
||||
)
|
||||
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")
|
||||
def portfolios():
|
||||
portfolios = Portfolios.for_user(g.current_user)
|
||||
return render_template("portfolios/index.html", page=5, portfolios=portfolios)
|
||||
|
||||
if 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")
|
||||
def portfolio(portfolio_id):
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/admin")
|
||||
def portfolio_admin(portfolio_id):
|
||||
portfolio = Portfolios.get_for_update_information(g.current_user, portfolio_id)
|
||||
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"])
|
||||
@ -62,9 +75,11 @@ def portfolio_reports(portfolio_id):
|
||||
prev_month = current_month - timedelta(days=28)
|
||||
two_months_ago = prev_month - timedelta(days=28)
|
||||
|
||||
expiration_date = (
|
||||
portfolio.legacy_task_order and portfolio.legacy_task_order.expiration_date
|
||||
task_order = next(
|
||||
(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:
|
||||
remaining_difference = expiration_date - today
|
||||
remaining_days = remaining_difference.days
|
||||
@ -76,8 +91,7 @@ def portfolio_reports(portfolio_id):
|
||||
cumulative_budget=Reports.cumulative_budget(portfolio),
|
||||
portfolio_totals=Reports.portfolio_totals(portfolio),
|
||||
monthly_totals=Reports.monthly_totals(portfolio),
|
||||
jedi_request=portfolio.request,
|
||||
legacy_task_order=portfolio.legacy_task_order,
|
||||
task_order=task_order,
|
||||
current_month=current_month,
|
||||
prev_month=prev_month,
|
||||
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
|
||||
|
||||
|
||||
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")
|
||||
def portfolio_members(portfolio_id):
|
||||
portfolio = Portfolios.get_with_members(g.current_user, portfolio_id)
|
||||
new_member_name = http_request.args.get("newMemberName")
|
||||
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
|
||||
]
|
||||
members_list = [serialize_portfolio_role(k) for k in portfolio.members]
|
||||
|
||||
return render_template(
|
||||
"portfolios/members/index.html",
|
||||
@ -50,7 +49,21 @@ def portfolio_members(portfolio_id):
|
||||
role_choices=PORTFOLIO_ROLE_DEFINITIONS,
|
||||
status_choices=MEMBER_STATUS_CHOICES,
|
||||
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()
|
||||
|
||||
flash("new_portfolio_member", new_member=new_member, portfolio=portfolio)
|
||||
flash("new_portfolio_member", new_member=member, portfolio=portfolio)
|
||||
|
||||
return redirect(
|
||||
url_for("portfolios.portfolio_members", portfolio_id=portfolio.id)
|
||||
|
@ -1,5 +1,4 @@
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
|
||||
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)
|
||||
|
||||
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])
|
||||
|
||||
return render_template(
|
||||
"portfolios/task_orders/index.html",
|
||||
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,
|
||||
expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []),
|
||||
funding_end_date=funding_end_date,
|
||||
funded=funded,
|
||||
total_balance=total_balance,
|
||||
)
|
||||
|
||||
@ -101,11 +95,7 @@ def submit_ko_review(portfolio_id, task_order_id, form=None):
|
||||
if form.validate():
|
||||
TaskOrders.update(user=g.current_user, task_order=task_order, **form.data)
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.view_task_order",
|
||||
portfolio_id=portfolio_id,
|
||||
task_order_id=task_order_id,
|
||||
)
|
||||
url_for("task_orders.signature_requested", task_order_id=task_order_id)
|
||||
)
|
||||
else:
|
||||
return render_template(
|
||||
|
@ -5,3 +5,4 @@ task_orders_bp = Blueprint("task_orders", __name__)
|
||||
from . import new
|
||||
from . import index
|
||||
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>")
|
||||
def download_csp_estimate(task_order_id):
|
||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||
if task_order.csp_estimate:
|
||||
estimate = task_order.csp_estimate
|
||||
generator = app.csp.files.download(estimate.object_name)
|
||||
return Response(
|
||||
generator,
|
||||
headers={
|
||||
"Content-Disposition": "attachment; filename={}".format(
|
||||
estimate.filename
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return send_file(task_order.csp_estimate)
|
||||
else:
|
||||
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,17 +3,68 @@ from flask import redirect, url_for, g
|
||||
from . import task_orders_bp
|
||||
from atst.domain.task_orders import TaskOrders
|
||||
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"])
|
||||
def invite(task_order_id):
|
||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||
portfolio = task_order.portfolio
|
||||
flash("task_order_congrats", portfolio=portfolio)
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.view_task_order",
|
||||
portfolio_id=task_order.portfolio_id,
|
||||
task_order_id=task_order.id,
|
||||
if TaskOrders.all_sections_complete(task_order):
|
||||
update_officer_invitations(g.current_user, task_order)
|
||||
|
||||
portfolio = task_order.portfolio
|
||||
flash("task_order_congrats", portfolio=portfolio)
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.view_task_order",
|
||||
portfolio_id=task_order.portfolio_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 atst.domain.task_orders import TaskOrders
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.domain.portfolio_roles import PortfolioRoles
|
||||
import atst.forms.task_order as task_order_form
|
||||
from atst.services.invitation import Invitation as InvitationService
|
||||
|
||||
|
||||
TASK_ORDER_SECTIONS = [
|
||||
@ -173,7 +171,7 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
||||
def validate(self):
|
||||
return self.form.validate()
|
||||
|
||||
def _update_task_order(self):
|
||||
def update(self):
|
||||
if self.task_order:
|
||||
if "portfolio_name" in self.form.data:
|
||||
new_name = self.form.data["portfolio_name"]
|
||||
@ -189,65 +187,6 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
||||
self._task_order = TaskOrders.create(portfolio=pf, creator=self.user)
|
||||
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
|
||||
|
||||
|
||||
|
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
|
||||
|
||||
MESSAGES = {
|
||||
"task_order_signed": {
|
||||
"title_template": "Task Order Signed",
|
||||
"message_template": """
|
||||
<p>Task order has been signed successfully</p>
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
"new_portfolio_member": {
|
||||
"title_template": "Member added successfully",
|
||||
"message_template": """
|
||||
@ -128,6 +135,13 @@ MESSAGES = {
|
||||
""",
|
||||
"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 werkzeug.datastructures import FileStorage
|
||||
from datetime import date
|
||||
from atst.models.attachment import Attachment
|
||||
|
||||
@ -7,6 +8,8 @@ class CustomJSONEncoder(JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Attachment):
|
||||
return obj.filename
|
||||
if isinstance(obj, date):
|
||||
elif isinstance(obj, date):
|
||||
return obj.strftime("%Y-%m-%d")
|
||||
elif isinstance(obj, FileStorage):
|
||||
return obj.filename
|
||||
return JSONEncoder.default(self, obj)
|
||||
|
@ -4,6 +4,7 @@ import { conformToMask } from 'vue-text-mask'
|
||||
import FormMixin from '../../mixins/form'
|
||||
import textinput from '../text_input'
|
||||
import optionsinput from '../options_input'
|
||||
import uploadinput from '../upload_input'
|
||||
|
||||
export default {
|
||||
name: 'funding',
|
||||
@ -13,6 +14,7 @@ export default {
|
||||
components: {
|
||||
textinput,
|
||||
optionsinput,
|
||||
uploadinput,
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -32,7 +34,6 @@ export default {
|
||||
clin_02 = 0,
|
||||
clin_03 = 0,
|
||||
clin_04 = 0,
|
||||
csp_estimate,
|
||||
} = this.initialData
|
||||
|
||||
return {
|
||||
@ -40,7 +41,6 @@ export default {
|
||||
clin_02,
|
||||
clin_03,
|
||||
clin_04,
|
||||
showUpload: !csp_estimate || this.uploadErrors.length > 0,
|
||||
}
|
||||
},
|
||||
|
||||
@ -63,9 +63,6 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
showUploadInput: function() {
|
||||
this.showUpload = true
|
||||
},
|
||||
updateBudget: function() {
|
||||
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: {
|
||||
members: Array,
|
||||
role_choices: Array,
|
||||
status_choices: Array,
|
||||
role_choices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
status_choices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data: function() {
|
||||
@ -87,7 +93,7 @@ export default {
|
||||
displayName: 'Environments',
|
||||
attr: 'num_env',
|
||||
sortFunc: numericSort,
|
||||
class: 'table-cell--align-right',
|
||||
class: 'table-cell--align-center',
|
||||
},
|
||||
{
|
||||
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 }) {
|
||||
// Validate the value
|
||||
|
@ -1,32 +1,14 @@
|
||||
import ToggleMixin from '../mixins/toggle'
|
||||
|
||||
export default {
|
||||
name: 'toggler',
|
||||
|
||||
mixins: [ToggleMixin],
|
||||
|
||||
props: {
|
||||
defaultVisible: {
|
||||
type: Boolean,
|
||||
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 VTooltip from 'v-tooltip'
|
||||
|
||||
import levelofwarrant from './components/levelofwarrant'
|
||||
import optionsinput from './components/options_input'
|
||||
import multicheckboxinput from './components/multi_checkbox_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 EditApplicationRoles from './components/forms/edit_application_roles'
|
||||
import funding from './components/forms/funding'
|
||||
import uploadinput from './components/upload_input'
|
||||
import Modal from './mixins/modal'
|
||||
import selector from './components/selector'
|
||||
import BudgetChart from './components/charts/budget_chart'
|
||||
@ -32,6 +34,7 @@ import RequestsList from './components/requests_list'
|
||||
import ConfirmationPopover from './components/confirmation_popover'
|
||||
import { isNotInVerticalViewport } from './lib/viewport'
|
||||
import DateSelector from './components/date_selector'
|
||||
import SidenavToggler from './components/sidenav_toggler'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
@ -43,6 +46,7 @@ const app = new Vue({
|
||||
el: '#app-root',
|
||||
components: {
|
||||
toggler,
|
||||
levelofwarrant,
|
||||
optionsinput,
|
||||
multicheckboxinput,
|
||||
textinput,
|
||||
@ -64,8 +68,10 @@ const app = new Vue({
|
||||
RequestsList,
|
||||
ConfirmationPopover,
|
||||
funding,
|
||||
uploadinput,
|
||||
DateSelector,
|
||||
EditOfficerForm,
|
||||
SidenavToggler,
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
|
@ -5,6 +5,10 @@ export const formatDollars = (value, cents = true) => {
|
||||
currency: 'USD',
|
||||
})
|
||||
} else if (typeof value === 'string') {
|
||||
if (value === '') {
|
||||
return value
|
||||
}
|
||||
|
||||
return parseFloat(value).toLocaleString('us-US', {
|
||||
style: 'currency',
|
||||
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/panels';
|
||||
@import 'elements/block_lists';
|
||||
@import 'elements/accordians';
|
||||
@import 'elements/tables';
|
||||
@import 'elements/sidenav';
|
||||
@import 'elements/action_group';
|
||||
@ -23,6 +24,7 @@
|
||||
@import 'elements/menu';
|
||||
|
||||
@import 'components/topbar';
|
||||
@import 'components/top_message';
|
||||
@import 'components/global_layout';
|
||||
@import 'components/global_navigation';
|
||||
@import 'components/portfolio_layout';
|
||||
|
@ -1,6 +1,6 @@
|
||||
.app-footer {
|
||||
background-color: $color-white;
|
||||
border-top: 1px solid $color-black;
|
||||
border-top: 1px solid $color-gray-lightest;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
@ -18,6 +18,7 @@
|
||||
|
||||
.app-footer__info__link {
|
||||
margin: (-$gap * 2) (-$gap);
|
||||
font-weight: normal;
|
||||
|
||||
.icon--footer {
|
||||
@include icon-size(16);
|
||||
|
@ -1,5 +1,5 @@
|
||||
#app-root {
|
||||
background-color: $color-gray-lightest;
|
||||
background-color: $color-offwhite;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
@ -2,29 +2,16 @@
|
||||
background-color: $color-white;
|
||||
|
||||
.sidenav__link {
|
||||
padding-right: $gap;
|
||||
padding-right: $gap * 2;
|
||||
|
||||
@include media($large-screen) {
|
||||
padding-right: $gap * 2;
|
||||
}
|
||||
}
|
||||
|
||||
.sidenav__link-label {
|
||||
@include hide;
|
||||
|
||||
@include media($large-screen) {
|
||||
@include unhide;
|
||||
padding-left: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
&.global-navigation__context--portfolio {
|
||||
.sidenav__link {
|
||||
padding-right: $gap;
|
||||
}
|
||||
|
||||
.sidenav__link-label {
|
||||
@include hide;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,61 +2,289 @@
|
||||
@include media($large-screen) {
|
||||
@include grid-row;
|
||||
}
|
||||
|
||||
margin-left: 2 * $gap;
|
||||
|
||||
.line {
|
||||
box-sizing: border-box;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
border: 1px solid $color-gray-lightest;
|
||||
}
|
||||
}
|
||||
|
||||
.portfolio-navigation {
|
||||
@include panel-margin;
|
||||
margin-bottom: $gap * 4;
|
||||
.portfolio-breadcrumbs {
|
||||
margin-bottom: $gap * 2;
|
||||
color: $color-gray-medium;
|
||||
font-size: $h5-font-size;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
li {
|
||||
flex-grow: 1;
|
||||
.icon-link {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media($medium-screen) {
|
||||
margin-bottom: $gap * 5;
|
||||
.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;
|
||||
@include media($small-screen) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@include media($large-screen) {
|
||||
width: 20rem;
|
||||
margin-right: $gap * 2;
|
||||
margin: 2 * $gap;
|
||||
|
||||
ul {
|
||||
display: block;
|
||||
.portfolio-header__name {
|
||||
@include h1;
|
||||
}
|
||||
|
||||
.portfolio-header__budget {
|
||||
font-size: $small-font-size;
|
||||
align-items: center;
|
||||
|
||||
.icon-tooltip {
|
||||
margin-left: -$gap / 2;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.portfolio-funding__header--funded-through {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.unfunded {
|
||||
color: $color-red;
|
||||
.icon {
|
||||
@include icon-color($color-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
padding: 0;
|
||||
margin: 0 $gap;
|
||||
|
||||
align-items: center;
|
||||
|
||||
.portfolio-funding__header--funded-through {
|
||||
padding: 2 * $gap;
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.funded {
|
||||
color: $color-green;
|
||||
.icon {
|
||||
@include icon-color($color-green);
|
||||
}
|
||||
}
|
||||
|
||||
.unfunded {
|
||||
color: $color-red;
|
||||
.icon {
|
||||
@include icon-color($color-red);
|
||||
}
|
||||
}
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.pending-task-order {
|
||||
@ -64,6 +292,7 @@
|
||||
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
margin-bottom: 2 * $gap;
|
||||
padding: 2 * $gap;
|
||||
|
||||
dt {
|
||||
@ -96,34 +325,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
.portfolio-total-balance {
|
||||
margin-top: -$gap;
|
||||
margin-bottom: 3rem;
|
||||
.total-balance {
|
||||
margin-right: 2 * $gap;
|
||||
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 {
|
||||
th{
|
||||
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;
|
||||
|
||||
.icon {
|
||||
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 {
|
||||
@ -146,7 +380,6 @@
|
||||
&.funded .to-expiration-alert {
|
||||
color: $color-blue;
|
||||
|
||||
|
||||
.icon {
|
||||
@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;
|
||||
flex-wrap: wrap;
|
||||
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
|
||||
@media (min-width:1000px) {
|
||||
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-darker: #205493;
|
||||
$color-blue-darkest: #112e51;
|
||||
$color-blue-light: #e5f1ff;
|
||||
|
||||
$color-aqua: #02bfe7;
|
||||
$color-aqua-dark: #00a6d2;
|
||||
@ -57,12 +58,13 @@ $color-red-light: #e59393;
|
||||
$color-red-lightest: #f9dede;
|
||||
|
||||
$color-white: #ffffff;
|
||||
$color-offwhite: #fbfbfd;
|
||||
$color-black: #000000;
|
||||
$color-black-light: #212121;
|
||||
|
||||
$color-gray-dark: #323a45;
|
||||
$color-gray: #5b616b;
|
||||
$color-gray-medium: #757575;
|
||||
$color-gray-medium: #9b9b9b;
|
||||
$color-gray-light: #aeb0b5;
|
||||
$color-gray-lighter: #d6d7d9;
|
||||
$color-gray-lightest: #f1f1f1;
|
||||
@ -83,7 +85,7 @@ $color-green-lighter: #94bfa2;
|
||||
$color-green-lightest: #e7f4e4;
|
||||
|
||||
$color-cool-blue: #205493;
|
||||
$color-cool-blue-light: #4773aa;
|
||||
$color-cool-blue-light: #4190e2;
|
||||
$color-cool-blue-lighter: #8ba6ca;
|
||||
$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 {
|
||||
@include panel-margin;
|
||||
@include shadow-panel
|
||||
padding: 0;
|
||||
|
||||
ul, dl {
|
||||
list-style: none;
|
||||
@ -15,6 +17,9 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
background-color: $color-gray-lightest;
|
||||
padding: $gap 2 * $gap;
|
||||
color: $color-gray;
|
||||
|
||||
.icon-tooltip {
|
||||
margin: -$gap;
|
||||
@ -59,7 +64,7 @@
|
||||
margin: 0;
|
||||
padding: $gap * 2;
|
||||
border-top: 0;
|
||||
border-bottom: 1px dashed $color-gray-light;
|
||||
border-bottom: 1px solid $color-gray-lightest;
|
||||
|
||||
@at-root li#{&} {
|
||||
&:last-child {
|
||||
|
@ -47,6 +47,7 @@
|
||||
.icon-link {
|
||||
@include icon-link;
|
||||
@include icon-link-color($color-primary);
|
||||
text-decoration: underline;
|
||||
|
||||
&.icon-link--vertical {
|
||||
@include icon-link-vertical;
|
||||
@ -67,6 +68,7 @@
|
||||
&.icon-link--disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.icon-link--left {
|
||||
|
@ -67,7 +67,24 @@
|
||||
@include icon-color($color-gray);
|
||||
}
|
||||
|
||||
&.icon--blue {
|
||||
@include icon-color($color-blue-darker);
|
||||
}
|
||||
|
||||
&.icon--medium {
|
||||
@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) {
|
||||
$border-width: 1px;
|
||||
$state-color: $color-gray;
|
||||
$state-color: $color-blue;
|
||||
|
||||
@if $state == 'error' {
|
||||
$border-width: 2px;
|
||||
@ -283,6 +283,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include input-state('default');
|
||||
|
||||
&.usa-input--error {
|
||||
@include input-state('error');
|
||||
}
|
||||
|
@ -46,6 +46,13 @@
|
||||
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 {
|
||||
@include panel-base;
|
||||
@include panel-theme-default;
|
||||
|
@ -1,160 +1,181 @@
|
||||
.sidenav {
|
||||
@include hide;
|
||||
@mixin sidenav__header {
|
||||
padding: $gap ($gap * 2);
|
||||
font-size: $small-font-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@include media($large-screen) {
|
||||
@include unhide;
|
||||
.sidenav-container {
|
||||
position: relative;
|
||||
|
||||
.global-navigation.sidenav {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidenav {
|
||||
@include media($large-screen) {
|
||||
margin: 0px;
|
||||
}
|
||||
width: 25rem;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
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 {
|
||||
color: $color-gray-dark;
|
||||
padding: $gap ($gap * 2);
|
||||
text-transform: uppercase;
|
||||
opacity: 0.54;
|
||||
font-size: $small-font-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul {
|
||||
&.sidenav__list--padded {
|
||||
margin: 4 * $gap 0;
|
||||
.sidenav__title {
|
||||
@include sidenav__header;
|
||||
text-transform: uppercase;
|
||||
width: 50%;
|
||||
color: $color-gray-dark;
|
||||
opacity: 0.54;
|
||||
}
|
||||
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
.sidenav__toggle {
|
||||
@include sidenav__header;
|
||||
float: right;
|
||||
color: $color-blue-darker;
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
display: block;
|
||||
.toggle-arrows {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
ul {
|
||||
&.sidenav__list--padded {
|
||||
margin: 4 * $gap 0;
|
||||
}
|
||||
|
||||
.sidenav__divider--small {
|
||||
display: block;
|
||||
width: 4 * $gap;
|
||||
border: 1px solid #D6D7D9;
|
||||
margin-left: 2 * $gap;
|
||||
margin-bottom: $gap;
|
||||
}
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
.sidenav__link {
|
||||
display: block;
|
||||
padding: $gap ($gap * 2);
|
||||
color: $color-black;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.sidenav__link-icon {
|
||||
margin-left: - ($gap * .5);
|
||||
}
|
||||
|
||||
&.sidenav__link--disabled {
|
||||
color: $color-shadow;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.sidenav__link--add {
|
||||
color: $color-blue;
|
||||
font-size: $small-font-size;
|
||||
.icon {
|
||||
@include icon-color($color-blue);
|
||||
@include icon-size(14);
|
||||
li {
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.sidenav__link--active {
|
||||
@include h4;
|
||||
color: $color-primary;
|
||||
background-color: $color-aqua-lightest;
|
||||
box-shadow: inset ($gap / 2) 0 0 0 $color-primary;
|
||||
.sidenav__divider--small {
|
||||
display: block;
|
||||
width: 4 * $gap;
|
||||
border: 1px solid #D6D7D9;
|
||||
margin-left: 2 * $gap;
|
||||
margin-bottom: $gap;
|
||||
}
|
||||
|
||||
.sidenav__text {
|
||||
margin: 2 * $gap;
|
||||
color: $color-gray;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sidenav__link {
|
||||
display: block;
|
||||
padding: $gap ($gap * 2);
|
||||
color: $color-black;
|
||||
text-decoration: underline;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.sidenav__link-icon {
|
||||
@include icon-style-active;
|
||||
margin-left: - ($gap * .5);
|
||||
}
|
||||
|
||||
position: relative;
|
||||
.sidenav__link-active_indicator .icon {
|
||||
@include icon-color($color-primary);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
&.sidenav__link--disabled {
|
||||
color: $color-shadow;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.sidenav__link--add {
|
||||
color: $color-blue;
|
||||
font-size: $small-font-size;
|
||||
.icon {
|
||||
@include icon-color($color-blue);
|
||||
@include icon-size(14);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.sidenav__link--active {
|
||||
@include h4;
|
||||
color: $color-primary;
|
||||
background-color: $color-aqua-lightest;
|
||||
box-shadow: inset ($gap / 2) 0 0 0 $color-primary;
|
||||
|
||||
.sidenav__link-icon {
|
||||
@include icon-style-active;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
.sidenav__link-active_indicator .icon {
|
||||
@include icon-color($color-primary);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
+ ul {
|
||||
background-color: $color-primary;
|
||||
|
||||
.sidenav__link {
|
||||
color: $color-white;
|
||||
background-color: $color-primary;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-blue-darker;
|
||||
}
|
||||
|
||||
&--active {
|
||||
@include h5;
|
||||
color: $color-white;
|
||||
background-color: $color-primary;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include icon-color($color-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ ul {
|
||||
background-color: $color-primary;
|
||||
|
||||
.sidenav__link {
|
||||
color: $color-white;
|
||||
background-color: $color-primary;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-blue-darker;
|
||||
}
|
||||
|
||||
&--active {
|
||||
li {
|
||||
.sidenav__link {
|
||||
@include h5;
|
||||
color: $color-white;
|
||||
background-color: $color-primary;
|
||||
box-shadow: none;
|
||||
}
|
||||
padding: $gap * .75;
|
||||
padding-left: 4.5rem;
|
||||
border: 0;
|
||||
font-weight: normal;
|
||||
|
||||
.icon {
|
||||
@include icon-color($color-white);
|
||||
.sidenav__link-icon {
|
||||
@include icon-size(12);
|
||||
flex-shrink: 0;
|
||||
margin-right: 1.5rem;
|
||||
margin-left: -3rem
|
||||
}
|
||||
|
||||
.sidenav__link-label {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ ul {
|
||||
// padding-bottom: $gap / 2;
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
background-color: $color-aqua-lightest;
|
||||
|
||||
li {
|
||||
.sidenav__link {
|
||||
@include h5;
|
||||
padding: $gap * .75;
|
||||
padding-left: 4.5rem;
|
||||
border: 0;
|
||||
font-weight: normal;
|
||||
|
||||
.sidenav__link-icon {
|
||||
@include icon-size(12);
|
||||
flex-shrink: 0;
|
||||
margin-right: 1.5rem;
|
||||
margin-left: -3rem
|
||||
}
|
||||
|
||||
.sidenav__link-label {
|
||||
padding-left: 0;
|
||||
}
|
||||
.sidenav__link-icon {
|
||||
@include icon-style-active;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
background-color: $color-aqua-lightest;
|
||||
.sidenav--minimized {
|
||||
@extend .sidenav;
|
||||
|
||||
.sidenav__link-icon {
|
||||
@include icon-style-active;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidenav--minimized {
|
||||
@extend .sidenav;
|
||||
|
||||
@include unhide;
|
||||
margin: 0px;
|
||||
|
||||
@include media($large-screen) {
|
||||
@include hide;
|
||||
width: auto;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,10 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.table-cell--align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.table-cell--shrink {
|
||||
width: 1%;
|
||||
}
|
||||
@ -32,9 +36,20 @@
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sorting-direction {
|
||||
position: absolute;
|
||||
thead {
|
||||
tr th {
|
||||
.sorting-direction {
|
||||
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 {
|
||||
@include grid-row;
|
||||
padding: $gap*2;
|
||||
|
@ -7,6 +7,11 @@
|
||||
|
||||
.funding-summary-row__col {
|
||||
|
||||
hr {
|
||||
margin: 2 * $gap 0;
|
||||
border-bottom: 1px solid $color-gray-lightest;
|
||||
}
|
||||
|
||||
@include media($medium-screen) {
|
||||
@include grid-pad;
|
||||
flex-grow: 1;
|
||||
@ -36,6 +41,11 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.subheading {
|
||||
@include h4;
|
||||
margin: 0 $gap 2 * $gap 0;
|
||||
-ms-flex-negative: 1;
|
||||
}
|
||||
|
||||
// Spending Summary
|
||||
// ===============================
|
||||
@ -53,40 +63,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.spend-summary__heading {
|
||||
@include h3;
|
||||
margin: 0 $gap 0 0;
|
||||
-ms-flex-negative: 1;
|
||||
}
|
||||
|
||||
.spend-summary__budget {
|
||||
margin: 0 0 0 $gap;
|
||||
|
||||
@include ie-only {
|
||||
margin: $gap 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
text-align: right;
|
||||
margin: 0 0 ($gap / 2) 0;
|
||||
dl {
|
||||
text-align: left;
|
||||
margin: 0 0 ($gap / 2) 0;
|
||||
|
||||
@include ie-only {
|
||||
text-align: left;
|
||||
}
|
||||
@include ie-only {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
dd, dt {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: $color-gray;
|
||||
margin-right: $gap;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
dd {
|
||||
font-weight: bold;
|
||||
}
|
||||
dt {
|
||||
text-transform: uppercase;
|
||||
color: $color-gray-light;
|
||||
margin-right: $gap;
|
||||
font-weight: bold;
|
||||
font-size: $small-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,42 +94,42 @@
|
||||
}
|
||||
|
||||
.spend-summary__spent {
|
||||
margin: $gap 0 0 0;
|
||||
margin: 2 * $gap 0;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
dd, dt {
|
||||
@include h5;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: normal;
|
||||
margin-left: $gap;
|
||||
letter-spacing: 0.47px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Task Order Summary
|
||||
// ===============================
|
||||
&.to-summary {
|
||||
|
||||
.to-summary__row {
|
||||
.to-summary__heading {
|
||||
@include h3;
|
||||
margin: 0;
|
||||
}
|
||||
.icon-link {
|
||||
font-weight: $font-normal
|
||||
}
|
||||
|
||||
.to-summary__to-number {
|
||||
margin: 0;
|
||||
dd {
|
||||
&::before {
|
||||
content: '#';
|
||||
color: $color-gray;
|
||||
margin-right: $gap;
|
||||
}
|
||||
.subheading {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.to-summary__heading {
|
||||
@include h4;
|
||||
margin: 0 $gap 0 0;
|
||||
}
|
||||
|
||||
.to-summary__to-number {
|
||||
margin: 0;
|
||||
dd {
|
||||
&::before {
|
||||
content: '#';
|
||||
color: $color-gray;
|
||||
margin-right: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,23 +160,26 @@
|
||||
.to-summary__expiration {
|
||||
|
||||
dl {
|
||||
margin: ($gap * 2) 0 0 0;
|
||||
text-align: right;
|
||||
margin-top: -2 * $gap;
|
||||
|
||||
> div {
|
||||
margin: 0 0 ($gap / 2) 0;
|
||||
dd, dt {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
dd, dt {
|
||||
display: block;
|
||||
}
|
||||
dt {
|
||||
font-size: $small-font-size;
|
||||
text-transform: uppercase;
|
||||
font-weight: $font-bold;
|
||||
color: $color-gray-light;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: $color-gray;
|
||||
margin-right: $gap;
|
||||
font-weight: normal;
|
||||
}
|
||||
dd.ending-soon {
|
||||
font-size: $h2-font-size;
|
||||
white-space: nowrap;
|
||||
|
||||
dd {
|
||||
font-weight: bold;
|
||||
.icon {
|
||||
@include icon-size(28);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -203,9 +203,12 @@
|
||||
|
||||
|
||||
.spend-table {
|
||||
box-shadow: 0 6px 18px 0 rgba(144,164,183,0.3);
|
||||
|
||||
.spend-table__header {
|
||||
@include panel-base;
|
||||
@include panel-theme-default;
|
||||
border-top: none;
|
||||
border-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -215,8 +218,8 @@
|
||||
padding: $gap * 2;
|
||||
|
||||
.spend-table__title {
|
||||
@include h3;
|
||||
margin: 0;
|
||||
@include h4;
|
||||
font-size: $lead-font-size;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
@ -227,6 +230,12 @@
|
||||
}
|
||||
|
||||
table {
|
||||
thead th {
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid $color-gray-lightest;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
th, td {
|
||||
white-space: nowrap;
|
||||
|
||||
@ -234,10 +243,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.current-month {
|
||||
background-color: $color-aqua-lightest;
|
||||
}
|
||||
|
||||
&.previous-month {
|
||||
color: $color-gray;
|
||||
}
|
||||
@ -286,28 +291,53 @@
|
||||
.spend-table__portfolio {
|
||||
th, td {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid $color-gray-lightest;
|
||||
}
|
||||
}
|
||||
|
||||
.spend-table__application {
|
||||
.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;
|
||||
color: $color-blue;
|
||||
|
||||
.icon {
|
||||
@include icon-size(12);
|
||||
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 {
|
||||
margin-left: $gap;
|
||||
margin-left: 2 * $gap;
|
||||
|
||||
&:last-child {
|
||||
td, th {
|
||||
padding-bottom: $gap * 5;
|
||||
box-shadow: inset 0 (-$gap * 2.5) 0 $color-gray-lightest;
|
||||
th, td {
|
||||
.icon-link {
|
||||
font-weight: $font-normal;
|
||||
font-size: $base-font-size;
|
||||
}
|
||||
|
||||
border-bottom: 1px dashed $color-white;
|
||||
background-color: $color-blue-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,11 @@
|
||||
text-align: center;
|
||||
padding: 4rem 6rem;
|
||||
|
||||
.panel {
|
||||
@include shadow-panel;
|
||||
margin-bottom: 6 * $gap;
|
||||
}
|
||||
|
||||
.task-order-get-started__list {
|
||||
ul {
|
||||
list-style: none;
|
||||
@ -49,6 +54,10 @@
|
||||
top: 2.5rem;
|
||||
margin-left: -23rem;
|
||||
}
|
||||
|
||||
.usa-button {
|
||||
margin: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
@ -58,6 +67,9 @@
|
||||
}
|
||||
|
||||
.task-order-summary {
|
||||
.panel {
|
||||
@include shadow-panel;
|
||||
}
|
||||
|
||||
.alert .alert__actions {
|
||||
margin-top: 2 * $gap;
|
||||
@ -67,7 +79,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label--pending {
|
||||
.label--pending, .label--started {
|
||||
background-color: $color-gold;
|
||||
}
|
||||
|
||||
@ -112,6 +124,11 @@
|
||||
|
||||
.task-order-next-steps {
|
||||
flex-grow: 1;
|
||||
|
||||
.panel {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@include media($xlarge-screen) {
|
||||
padding-right: $gap;
|
||||
}
|
||||
@ -135,8 +152,17 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-top: 3 * $gap;
|
||||
margin-bottom: 0;
|
||||
padding: 2 * $gap;
|
||||
|
||||
.alert__message {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-next-steps__icon {
|
||||
width: 8%;
|
||||
padding: $gap $gap 0 0;
|
||||
justify-content: center;
|
||||
.complete {
|
||||
@ -147,34 +173,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-next-steps__text {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.task-order-next-steps__action {
|
||||
min-width: 10 * $gap;
|
||||
padding: $gap 0 0 $gap;
|
||||
width: 32%;
|
||||
a.usa-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-next-steps__heading {
|
||||
|
||||
h4 {
|
||||
@include ie-only {
|
||||
width: 100%;
|
||||
.task-order-next-steps__text {
|
||||
display: flex;
|
||||
.task-order-next-steps__heading {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
margin: $gap $gap 0 0;
|
||||
}
|
||||
}
|
||||
.task-order-next-steps__description {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-sidebar {
|
||||
@include media($xlarge-screen) {
|
||||
padding-left: 3 * $gap;
|
||||
}
|
||||
min-width: 35rem;
|
||||
|
||||
hr {
|
||||
@ -193,18 +214,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-invitation-status {
|
||||
.invited {
|
||||
color: $color-green;
|
||||
@include icon-color($color-green);
|
||||
}
|
||||
.uninvited {
|
||||
color: $color-red;
|
||||
@include icon-color($color-red);
|
||||
.task-order-invitations {
|
||||
.task-order-invitations__heading {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.task-order-invitation-status__icon {
|
||||
padding: 0 0.5rem;
|
||||
.task-order-invitation-status {
|
||||
margin-bottom: 3 * $gap;
|
||||
.task-order-invitation-status__title {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
.invited {
|
||||
color: $color-green;
|
||||
@include icon-color($color-green);
|
||||
}
|
||||
.uninvited {
|
||||
color: $color-red;
|
||||
@include icon-color($color-red);
|
||||
}
|
||||
|
||||
.task-order-invitation-status__icon {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-invitation-details {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -230,14 +266,18 @@
|
||||
}
|
||||
|
||||
.task-order-invite-message {
|
||||
font-weight: $font-bold;
|
||||
|
||||
&.not-sent {
|
||||
color: $color-red;
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
&.sent {
|
||||
color: $color-green;
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
color: $color-gold-dark;
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,6 +397,48 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
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 }}'>
|
||||
<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 %}">
|
||||
<legend>
|
||||
{{ field() }}
|
||||
{{ field.label }}
|
||||
<label for={{field.name}}>
|
||||
{{ label }}
|
||||
</label>
|
||||
|
||||
{% if field.description %}
|
||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||
|
@ -64,6 +64,7 @@
|
||||
|
||||
<masked-input
|
||||
v-on:input='onInput'
|
||||
v-on:blur='onBlur'
|
||||
v-on:change='onChange'
|
||||
v-bind:value='value'
|
||||
v-bind:mask='mask'
|
||||
@ -87,6 +88,9 @@
|
||||
<template v-if='showError'>
|
||||
<span class='usa-input__message' v-html='validationError'></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class='usa-input__message'></span>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</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 %}
|
||||
|
||||
<section class="block-list">
|
||||
<header class="block-list__header">
|
||||
<h1 class="block-list__title">{{ "audit_log.header_title" | translate }}</h1>
|
||||
</header>
|
||||
|
||||
<section class="block-list activity-log">
|
||||
<div class='subheading'>{{ "portfolios.admin.activity_log_title" | translate }}</div>
|
||||
<ul>
|
||||
{% for event in audit_events %}
|
||||
<li class="block-list__item">
|
||||
|
@ -1,18 +1,8 @@
|
||||
{% 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 }}
|
||||
<div class="panel">
|
||||
<div class="panel__heading panel__heading--grow">
|
||||
<h1>{{ title_text }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel__content">
|
||||
<p>
|
||||
{{ "fragments.edit_application_form.explain" | translate }}
|
||||
</p>
|
||||
{{ TextInput(form.name) }}
|
||||
{{ TextInput(form.description, paragraph=True) }}
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
{{ "fragments.edit_application_form.explain" | translate }}
|
||||
</p>
|
||||
{{ TextInput(form.name) }}
|
||||
{{ TextInput(form.description, paragraph=True) }}
|
||||
|
@ -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>
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<h4 class='task-order-form__heading'>{{ heading | translate }}</h4>
|
||||
{{ first_name }} {{ last_name }}<br>
|
||||
{{ email }}<br>
|
||||
{% if phone_number %}
|
||||
{{ phone_number | usPhone }}
|
||||
{{ officer_data.first_name }} {{ officer_data.last_name }}<br>
|
||||
{{ officer_data.email }}<br>
|
||||
{% if officer_data.phone_number %}
|
||||
{{ officer_data.phone_number | usPhone }}
|
||||
{% endif %}
|
||||
<br>
|
||||
{{ "task_orders.new.review.dod_id" | translate }} {{ dod_id}}<br>
|
||||
{% if officer %}
|
||||
{{ "task_orders.new.review.dod_id" | translate }} {{ officer_data.dod_id}}<br>
|
||||
{% if has_officer %}
|
||||
{{ 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 %}
|
||||
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_invited"| translate }}</span>
|
||||
{% endif %}
|
||||
@ -17,9 +19,24 @@
|
||||
{% endmacro %}
|
||||
|
||||
<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("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) }}
|
||||
</div>
|
||||
<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) }}
|
||||
</div>
|
||||
{{ ReviewOfficerInfo(
|
||||
"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 class="row">
|
||||
{{ ReviewOfficerInfo(
|
||||
"task_orders.new.review.so",
|
||||
task_order.officer_dictionary("security_officer"),
|
||||
task_order.security_officer,
|
||||
task_order.so_invitable
|
||||
) }}
|
||||
</div>
|
||||
|
@ -2,23 +2,39 @@
|
||||
{% from "components/sidenav_item.html" import SidenavItem %}
|
||||
|
||||
|
||||
<div class="global-navigation sidenav">
|
||||
<div class="sidenav__title">Portfolios</div>
|
||||
<ul class="sidenav__list--padded">
|
||||
{% for other_portfolio in portfolios|sort(attribute='name') %}
|
||||
{{ SidenavItem(other_portfolio.name,
|
||||
href=url_for("portfolios.show_portfolio", portfolio_id=other_portfolio.id),
|
||||
active=(other_portfolio.id | string) == request.view_args.get('portfolio_id')
|
||||
) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<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">
|
||||
<span class="sidenav__link-label">Fund a New Portfolio</span>
|
||||
{{ Icon("plus", classes="sidenav__link-icon") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="global-navigation sidenav--minimized">
|
||||
<div class="sidenav__title">Show >>></div>
|
||||
<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>
|
||||
<ul class="sidenav__list--padded">
|
||||
{% if portfolios %}
|
||||
{% for other_portfolio in portfolios|sort(attribute='name') %}
|
||||
{{ SidenavItem(other_portfolio.name,
|
||||
href=url_for("portfolios.show_portfolio", portfolio_id=other_portfolio.id),
|
||||
active=(other_portfolio.id | string) == request.view_args.get('portfolio_id')
|
||||
) }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li><span class="sidenav__text">You have no portfolios yet</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<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">
|
||||
<span class="sidenav__link-label">Fund a New Portfolio</span>
|
||||
{{ Icon("plus", classes="sidenav__link-icon icon--circle") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</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" %}
|
||||
{% from "components/pagination.html" import Pagination %}
|
||||
|
||||
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.admin" | translate %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
<div v-cloak>
|
||||
{% 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 %}
|
||||
|
||||
{% 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) }}">
|
||||
<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">
|
||||
<header class="block-list__header block-list__header--grow">
|
||||
<h2 class="block-list__title">Application Environments</h2>
|
||||
<p>
|
||||
Each environment created within an application is an enclave of cloud resources that is logically separated from each other for increased security.
|
||||
</p>
|
||||
</header>
|
||||
<div class="application-list-item">
|
||||
<header>
|
||||
<h2 class="block-list__title">{{ 'portfolios.applications.environments_heading' | translate }}</h2>
|
||||
<p>
|
||||
{{ 'portfolios.applications.environments_description' | translate }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
{% for environment in application.environments %}
|
||||
<li class="block-list__item application-edit__env-list-item">
|
||||
<div class="usa-input">
|
||||
<label>Environment Name</label>
|
||||
<input type="text" value="{{ environment.name }}" readonly />
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<ul>
|
||||
{% for environment in application.environments %}
|
||||
<li class="application-edit__env-list-item">
|
||||
<div class="usa-input input--disabled">
|
||||
<label>Environment Name</label>
|
||||
<input type="text" disabled value="{{ environment.name }}" readonly />
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
|
||||
|
@ -3,53 +3,90 @@
|
||||
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% if not portfolio.applications %}
|
||||
|
||||
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %}
|
||||
|
||||
{{ EmptyState(
|
||||
'This portfolio doesn’t have any applications yet.',
|
||||
action_label='Add a New Application' if can_create_applications else None,
|
||||
action_href=url_for('portfolios.new_application', portfolio_id=portfolio.id) if can_create_applications else None,
|
||||
icon='cloud',
|
||||
sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.'
|
||||
) }}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% for application in portfolio.applications %}
|
||||
<div v-cloak class='block-list application-list-item'>
|
||||
<header class='block-list__header'>
|
||||
<h2 class='block-list__title'>{{ application.name }} ({{ application.environments|length }} environments)</h2>
|
||||
{% 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) }}'>
|
||||
{{ Icon('edit') }}
|
||||
<span>edit</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</header>
|
||||
<ul>
|
||||
{% for environment in application.environments %}
|
||||
<li class='block-list__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'>
|
||||
{{ Icon('link') }}
|
||||
<span>{{ environment.name }}</span>
|
||||
</a>
|
||||
|
||||
<div class='application-list-item__environment__members'>
|
||||
<div class='label'>{{ environment.num_users }}</div>
|
||||
<span>members</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% if not portfolio.applications %}
|
||||
|
||||
{{ EmptyState(
|
||||
'This portfolio doesn’t have any applications yet.',
|
||||
action_label='Add a New Application' if can_create_applications else None,
|
||||
action_href=url_for('portfolios.new_application', portfolio_id=portfolio.id) if can_create_applications else None,
|
||||
icon='cloud',
|
||||
sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.'
|
||||
) }}
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class='application-list'>
|
||||
{% for application in portfolio.applications|sort(attribute='name') %}
|
||||
<div is='toggler' v-cloak class='accordian application-list-item'>
|
||||
<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) %}
|
||||
<a class='icon-link' href='{{ url_for("portfolios.edit_application", portfolio_id=portfolio.id, application_id=application.id) }}'>
|
||||
<span>{{ "portfolios.applications.app_settings_text" | translate }}</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>
|
||||
{% 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>
|
||||
<ul v-if="props.isVisible">
|
||||
{% for environment in application.environments %}
|
||||
<li class='accordian__item application-list-item__environment'>
|
||||
<div class='application-list-item__environment__name'>
|
||||
<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>
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</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,72 +1,82 @@
|
||||
{% extends "portfolios/base.html" %}
|
||||
{% extends "portfolios/applications/base.html" %}
|
||||
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/modal.html" import Modal %}
|
||||
{% 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" %}
|
||||
{% 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 }}'>
|
||||
<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) %}
|
||||
<h1>Create application !{ name }</h1>
|
||||
{% call Modal(name=modalName, dismissable=False) %}
|
||||
<h1>Create application !{ name }</h1>
|
||||
|
||||
<p>
|
||||
When you click <em>Create Application</em>, the environments
|
||||
<span v-for="(environment, index) in environments">
|
||||
<strong>!{environment.name}</strong><template v-if="index < (environments.length - 1)">, </template>
|
||||
</span>
|
||||
will be created as individual cloud resource groups under <strong>!{ name }</strong> application.
|
||||
</p>
|
||||
<p>
|
||||
When you click <em>{{ 'portfolios.applications.create_button_text' | translate }}</em>, the environments
|
||||
<span v-for="(environment, index) in environments">
|
||||
<strong>!{environment.name}</strong><template v-if="index < (environments.length - 1)">, </template>
|
||||
</span>
|
||||
will be created as individual cloud resource groups under <strong>!{ name }</strong> application.
|
||||
</p>
|
||||
|
||||
<div class='action-group'>
|
||||
<button autofocus type='submit' class='action-group__action usa-button' tabindex='0'>Create Application</button>
|
||||
<button type='button' v-on:click="handleCancelSubmit" class='icon-link action-group__action' tabindex='0'>Cancel</button>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% include "fragments/edit_application_form.html" %}
|
||||
|
||||
<div> {# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #}
|
||||
<div v-cloak v-for="title in errors" :key="title">
|
||||
{{ Alert(message=None, level="error", vue_template=True) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block-list application-list-item">
|
||||
<header class="block-list__header block-list__header--grow">
|
||||
<h2 class="block-list__title">Application Environments</h2>
|
||||
<p>
|
||||
Each environment created within an application is an enclave of cloud resources that is logically separated from each other for increased security.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
<li v-for="(environment, i) in environments" class="block-list__item application-edit__env-list-item">
|
||||
<div class="usa-input">
|
||||
<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="hidden" :name="'environment_names-' + i" v-model="environment.name"/>
|
||||
<div class='action-group'>
|
||||
<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>
|
||||
</div>
|
||||
<button v-on:click="removeEnvironment(i)" v-if="environments.length > 1" type="button" class='application-edit__env-list-item__remover'>
|
||||
{{ Icon('trash') }}
|
||||
<span>Remove</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% include "fragments/edit_application_form.html" %}
|
||||
|
||||
<div> {# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #}
|
||||
<div v-cloak v-for="title in errors" :key="title">
|
||||
{{ Alert(message=None, level="error", vue_template=True) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="application-list-item">
|
||||
<header>
|
||||
<h2 class="block-list__title">{{ 'portfolios.applications.environments_heading' | translate }}</h2>
|
||||
<p>
|
||||
{{ 'portfolios.applications.environments_description' | translate }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
<li v-for="(environment, i) in environments" class="application-edit__env-list-item">
|
||||
<div class="usa-input">
|
||||
<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="hidden" :name="'environment_names-' + i" v-model="environment.name"/>
|
||||
</div>
|
||||
<button v-on:click="removeEnvironment(i)" v-if="environments.length > 1" type="button" class='application-edit__env-list-item__remover'>
|
||||
{{ Icon('trash') }}
|
||||
<span>Remove</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="block-list__footer">
|
||||
<button v-on:click="addEnvironment" class="icon-link" tabindex="0" type="button">Add another environment</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block-list__footer">
|
||||
<button v-on:click="addEnvironment" class="icon-link" tabindex="0" type="button">Add another environment</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
</new-application>
|
||||
|
@ -3,12 +3,18 @@
|
||||
{% block content %}
|
||||
|
||||
<div class='portfolio-panel-container'>
|
||||
<div class='col'>
|
||||
{% include 'navigation/portfolio_navigation.html' %}
|
||||
</div>
|
||||
|
||||
<div class='col col--grow'>
|
||||
{% block portfolio_content %}{% endblock %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
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,165 +13,170 @@
|
||||
<form method="POST" action="{{ url_for('portfolios.update_member', portfolio_id=portfolio.id, member_id=member.user_id) }}" autocomplete="false">
|
||||
{{ form.csrf_token }}
|
||||
|
||||
<div class='panel member-card'>
|
||||
<div class='member-card__header'>
|
||||
<h1 class='member-card__heading'>{{ member.user.full_name }}</h1>
|
||||
<div class='member-edit'>
|
||||
<div class="subheading">Edit Portfolio Member</div>
|
||||
<div class="panel">
|
||||
<div class='member-card'>
|
||||
<div class='member-card__header'>
|
||||
<h1 class='member-card__heading'>{{ member.user.full_name }}</h1>
|
||||
|
||||
<div class="usa-input member-card__input">
|
||||
{{ Selector(form.portfolio_role) }}
|
||||
</div>
|
||||
|
||||
<div class="usa-input member-card__input">
|
||||
{{ Selector(form.portfolio_role) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class='member-card__details'>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>DOD ID:</dt>
|
||||
<dd>{{ member.user.dod_id }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Email:</dt>
|
||||
<dd>{{ member.user.email }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{% if editable %}
|
||||
<a href='{{ url_for("users.user") }}' class='icon-link'>edit account details</a>
|
||||
{% endif %}
|
||||
<div>
|
||||
{% if member.latest_invitation.is_revokable %}
|
||||
{{ ConfirmationButton(
|
||||
"Revoke Invitation",
|
||||
url_for("portfolios.revoke_invitation", portfolio_id=portfolio.id, token=member.latest_invitation.token),
|
||||
) }}
|
||||
{% endif %}
|
||||
{% if member.can_resend_invitation %}
|
||||
{{ ConfirmationButton (
|
||||
"Resend Invitation",
|
||||
url_for("portfolios.resend_invitation", portfolio_id=portfolio.id, token=member.latest_invitation.token),
|
||||
confirm_msg="Are you sure? This will send an email to invite the user to join this portfolio."
|
||||
)}}
|
||||
{% endif %}
|
||||
{% if can_revoke_access %}
|
||||
{{ ConfirmationButton (
|
||||
"Remove Portfolio Access",
|
||||
url_for("portfolios.revoke_access", portfolio_id=portfolio.id, member_id=member.id),
|
||||
confirm_msg="Are you sure? This will remove this user from the portfolio.",
|
||||
)}}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel__heading panel__heading--tight">
|
||||
<h2 class="h3">Manage Access <div class="subtitle">Grant access to an environment</div></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='search-bar'>
|
||||
<div class='usa-input search-input'>
|
||||
<label for='application-search'>Search by application name</label>
|
||||
<input type='search' id='application-search' name='application-search' placeholder="Search by application name"/>
|
||||
<button type="submit">
|
||||
<span class="hide">Search</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for application in applications %}
|
||||
{% set revoke_modal_name = (application.id|string) + 'RevokeModal' %}
|
||||
<edit-application-roles inline-template name="{{ application.name }}" id="{{ application.id }}">
|
||||
<div is='toggler' default-visible class='block-list application-list-item'>
|
||||
<template slot-scope='props'>
|
||||
<header class='block-list__header'>
|
||||
<button v-on:click='props.toggle' class='icon-link icon-link--large icon-link--default spend-table__application__toggler'>
|
||||
<template v-if='props.isVisible'>{{ Icon('caret_down') }}</template>
|
||||
<template v-else>{{ Icon('caret_right') }}</template>
|
||||
<h3 class="block-list__title">{{ application.name }}</h3>
|
||||
</button>
|
||||
<span><a v-on:click="openModal('{{ revoke_modal_name }}')" class="icon-link icon-link--danger">revoke all access</a></span>
|
||||
</header>
|
||||
{% call Modal(name=revoke_modal_name, dismissable=False) %}
|
||||
<div class='member-card__details'>
|
||||
<dl>
|
||||
<div>
|
||||
<h1>Revoke Access</h1>
|
||||
<p>
|
||||
Confirming will revoke access for {{ member.user.full_name }} to any environments associated with {{ application.name }}.
|
||||
</p>
|
||||
<div class='action-group'>
|
||||
<a v-on:click="doRevoke(); closeModal('{{ revoke_modal_name }}')" class='action-group__action usa-button'>Confirm</a>
|
||||
<a class='action-group__action icon-link icon-link--danger' v-on:click="closeModal('{{ revoke_modal_name }}'); cancel();">Cancel</a>
|
||||
</div>
|
||||
<dt>DOD ID:</dt>
|
||||
<dd>{{ member.user.dod_id }}</dd>
|
||||
</div>
|
||||
{% endcall %}
|
||||
<ul v-show='props.isVisible'>
|
||||
{% for env in application.environments %}
|
||||
|
||||
{% set role = EnvironmentRoles.get(member.user_id, env.id).role %}
|
||||
{% set env_modal_name = (env.id|string) + 'RolesModal' %}
|
||||
|
||||
<li class='block-list__item'>
|
||||
<edit-environment-role inline-template initial-data='{{ role or "" }}' v-bind:choices='{{ choices | tojson }}' v-bind:application-id="'{{ application.id }}'">
|
||||
<div class='application-list-item__environment'>
|
||||
<span class='application-list-item__environment__link'>
|
||||
{{ env.name }}
|
||||
</span>
|
||||
|
||||
<div class='application-list-item__environment__actions'>
|
||||
<span v-bind:class="label_class" v-html:on=displayName></span>
|
||||
<button v-on:click="openModal('{{env_modal_name}}')" type="button" class="icon-link">set role</button>
|
||||
{% call Modal(name=env_modal_name, dismissable=False) %}
|
||||
<div class='block-list'>
|
||||
<div class='block-list__header'>
|
||||
<div>
|
||||
{% if env_role_modal_description %}
|
||||
<h1>{{ env_role_modal_description.header }}</h1>
|
||||
<p>{{ env_role_modal_description.body | safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
{% for choice in choices %}
|
||||
<li class='block-list__item block-list__item--selectable'>
|
||||
<input
|
||||
name='radio_input_{{ env.id }}'
|
||||
v-on:change.prevent='change'
|
||||
type='radio'
|
||||
id="env_{{ env.id }}_{{ choice[0] }}"
|
||||
value='{{ choice[0] }}'
|
||||
:checked="new_role === '{{ choice[0]}}'"
|
||||
/>
|
||||
<label for="env_{{ env.id }}_{{ choice[0] }}">
|
||||
{% if choice[1].description %}
|
||||
<dl>
|
||||
<dt>{{ choice[1].name }}</dt>
|
||||
<dd>{{ choice[1].description }}</dd>
|
||||
</dl>
|
||||
{% else %}
|
||||
{{ choice[1].name }}
|
||||
{% endif %}
|
||||
</label>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<input type='hidden' name='env_{{ env.id }}' v-bind:value='newRole'/>
|
||||
<div class='block-list__footer'>
|
||||
<div class='action-group'>
|
||||
<a v-on:click="closeModal('{{env_modal_name}}')" class='action-group__action usa-button'>Select Access Role</a>
|
||||
<a class='action-group__action icon-link icon-link--danger' v-on:click="closeModal('{{env_modal_name}}'); cancel();" value="{{ value if value == role else role }}" >Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
</div>
|
||||
<div>
|
||||
<dt>Email:</dt>
|
||||
<dd>{{ member.user.email }}</dd>
|
||||
</div>
|
||||
</edit-environment-role>
|
||||
</li>
|
||||
</dl>
|
||||
{% if editable %}
|
||||
<a href='{{ url_for("users.user") }}' class='icon-link'>edit account details</a>
|
||||
{% endif %}
|
||||
<div>
|
||||
{% if member.latest_invitation.is_revokable %}
|
||||
{{ ConfirmationButton(
|
||||
"Revoke Invitation",
|
||||
url_for("portfolios.revoke_invitation", portfolio_id=portfolio.id, token=member.latest_invitation.token),
|
||||
) }}
|
||||
{% endif %}
|
||||
{% if member.can_resend_invitation %}
|
||||
{{ ConfirmationButton (
|
||||
"Resend Invitation",
|
||||
url_for("portfolios.resend_invitation", portfolio_id=portfolio.id, token=member.latest_invitation.token),
|
||||
confirm_msg="Are you sure? This will send an email to invite the user to join this portfolio."
|
||||
)}}
|
||||
{% endif %}
|
||||
{% if can_revoke_access %}
|
||||
{{ ConfirmationButton (
|
||||
"Remove Portfolio Access",
|
||||
url_for("portfolios.revoke_access", portfolio_id=portfolio.id, member_id=member.id),
|
||||
confirm_msg="Are you sure? This will remove this user from the portfolio.",
|
||||
)}}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="manage-access">
|
||||
<div class="subheading">Manage Access</div>
|
||||
<div class="subtitle">Grant access to an environment</div>
|
||||
</div>
|
||||
|
||||
<div class='search-bar'>
|
||||
<div class='usa-input search-input'>
|
||||
<label for='application-search'>Search by application name</label>
|
||||
<input type='search' id='application-search' name='application-search' placeholder="Search by application name"/>
|
||||
<button type="submit">
|
||||
<span class="hide">Search</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for application in applications %}
|
||||
{% set revoke_modal_name = (application.id|string) + 'RevokeModal' %}
|
||||
<edit-application-roles inline-template name="{{ application.name }}" id="{{ application.id }}">
|
||||
<div is='toggler' default-visible class='block-list application-list-item'>
|
||||
<template slot-scope='props'>
|
||||
<header class='block-list__header'>
|
||||
<button v-on:click='props.toggle' class='icon-link icon-link--large icon-link--default spend-table__application__toggler'>
|
||||
<template v-if='props.isVisible'>{{ Icon('caret_down') }}</template>
|
||||
<template v-else>{{ Icon('caret_right') }}</template>
|
||||
<h3 class="block-list__title">{{ application.name }}</h3>
|
||||
</button>
|
||||
<span><a v-on:click="openModal('{{ revoke_modal_name }}')" class="icon-link icon-link--danger">revoke all access</a></span>
|
||||
</header>
|
||||
{% call Modal(name=revoke_modal_name, dismissable=False) %}
|
||||
<div>
|
||||
<h1>Revoke Access</h1>
|
||||
<p>
|
||||
Confirming will revoke access for {{ member.user.full_name }} to any environments associated with {{ application.name }}.
|
||||
</p>
|
||||
<div class='action-group'>
|
||||
<a v-on:click="doRevoke(); closeModal('{{ revoke_modal_name }}')" class='action-group__action usa-button'>Confirm</a>
|
||||
<a class='action-group__action icon-link icon-link--danger' v-on:click="closeModal('{{ revoke_modal_name }}'); cancel();">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
<ul v-show='props.isVisible'>
|
||||
{% for env in application.environments %}
|
||||
|
||||
{% set role = EnvironmentRoles.get(member.user_id, env.id).role %}
|
||||
{% set env_modal_name = (env.id|string) + 'RolesModal' %}
|
||||
|
||||
<li class='block-list__item'>
|
||||
<edit-environment-role inline-template initial-data='{{ role or "" }}' v-bind:choices='{{ choices | tojson }}' v-bind:application-id="'{{ application.id }}'">
|
||||
<div class='application-list-item__environment'>
|
||||
<span class='application-list-item__environment__link'>
|
||||
{{ env.name }}
|
||||
</span>
|
||||
|
||||
<div class='application-list-item__environment__actions'>
|
||||
<span v-bind:class="label_class" v-html:on=displayName></span>
|
||||
<button v-on:click="openModal('{{env_modal_name}}')" type="button" class="icon-link">set role</button>
|
||||
{% call Modal(name=env_modal_name, dismissable=False) %}
|
||||
<div class='block-list'>
|
||||
<div class='block-list__header'>
|
||||
<div>
|
||||
{% if env_role_modal_description %}
|
||||
<h1>{{ env_role_modal_description.header }}</h1>
|
||||
<p>{{ env_role_modal_description.body | safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
{% for choice in choices %}
|
||||
<li class='block-list__item block-list__item--selectable'>
|
||||
<input
|
||||
name='radio_input_{{ env.id }}'
|
||||
v-on:change.prevent='change'
|
||||
type='radio'
|
||||
id="env_{{ env.id }}_{{ choice[0] }}"
|
||||
value='{{ choice[0] }}'
|
||||
:checked="new_role === '{{ choice[0]}}'"
|
||||
/>
|
||||
<label for="env_{{ env.id }}_{{ choice[0] }}">
|
||||
{% if choice[1].description %}
|
||||
<dl>
|
||||
<dt>{{ choice[1].name }}</dt>
|
||||
<dd>{{ choice[1].description }}</dd>
|
||||
</dl>
|
||||
{% else %}
|
||||
{{ choice[1].name }}
|
||||
{% endif %}
|
||||
</label>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<input type='hidden' name='env_{{ env.id }}' v-bind:value='newRole'/>
|
||||
<div class='block-list__footer'>
|
||||
<div class='action-group'>
|
||||
<a v-on:click="closeModal('{{env_modal_name}}')" class='action-group__action usa-button'>Select Access Role</a>
|
||||
<a class='action-group__action icon-link icon-link--danger' v-on:click="closeModal('{{env_modal_name}}'); cancel();" value="{{ value if value == role else role }}" >Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
</edit-environment-role>
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</edit-application-roles>
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</edit-application-roles>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class='action-group'>
|
||||
<button class='action-group__action usa-button usa-button-big'>
|
||||
@ -183,8 +188,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -3,6 +3,8 @@
|
||||
{% from "components/empty_state.html" import EmptyState %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% set secondary_breadcrumb = 'Portfolio Team Management' %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% if not portfolio.members %}
|
||||
@ -25,6 +27,7 @@
|
||||
<members-list
|
||||
inline-template
|
||||
id="search-template"
|
||||
class='member-list'
|
||||
v-bind:members='{{ members | tojson}}'
|
||||
v-bind:role_choices='{{ role_choices | tojson}}'
|
||||
v-bind:status_choices='{{ status_choices | tojson}}'>
|
||||
@ -61,7 +64,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class='responsive-table-wrapper'>
|
||||
<div class='responsive-table-wrapper panel'>
|
||||
<table v-cloak v-if='searchedList && searchedList.length'>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -82,7 +85,7 @@
|
||||
<td>
|
||||
<a :href="member.edit_link" class="icon-link icon-link--large" v-html="member.name"></a>
|
||||
</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>
|
||||
</td>
|
||||
<td class='table-cell--shrink' v-else>
|
||||
@ -91,6 +94,14 @@
|
||||
<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>
|
||||
|
@ -1,47 +1,44 @@
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/empty_state.html" import EmptyState %}
|
||||
|
||||
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.reports" | translate %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
{{ Alert("Budget Report for Portfolio " + portfolio.name,
|
||||
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 class='portfolio-reports'>
|
||||
<div v-cloak class='funding-summary-row'>
|
||||
|
||||
<div class='funding-summary-row__col'>
|
||||
<div class='panel spend-summary'>
|
||||
<h4 class='spend-summary__heading subheading'>Portfolio Total Spend</h4>
|
||||
<div class='row'>
|
||||
<h2 class='spend-summary__heading col'>Portfolio Total Spend</h2>
|
||||
<dl class='spend-summary__budget'>
|
||||
<dl class='spend-summary__budget col col--grow row'>
|
||||
{% set budget = portfolio_totals['budget'] %}
|
||||
{% set spent = portfolio_totals['spent'] %}
|
||||
{% set remaining = budget - spent %}
|
||||
<div>
|
||||
<dt>Budget </dt>
|
||||
<dl class='col col--grow'>
|
||||
<dt>Budget</dt>
|
||||
<dd>{{ budget | dollars }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div>
|
||||
<dl class='col col--grow'>
|
||||
<dt>Remaining</dt>
|
||||
<dd>{{ remaining | dollars }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
</meter>
|
||||
|
||||
<dl class='spend-summary__spent'>
|
||||
<dt>Total spend to date</dt>
|
||||
<dt>Total spending to date</dt>
|
||||
<dd>{{ spent | dollars }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
@ -53,18 +50,23 @@
|
||||
<div class='to-summary__row'>
|
||||
|
||||
<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'>
|
||||
<dt class='usa-sr-only'>Task Order Number</dt>
|
||||
<dd>{{ legacy_task_order.number }}</dd>
|
||||
<dd>{{ task_order.number }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<div class='to-summary__expiration'>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Expires</dt>
|
||||
<dd>
|
||||
<div class='row'>
|
||||
<h4 class='subheading'>Expiration Date</h4>
|
||||
</div>
|
||||
<div class='row'>
|
||||
|
||||
<div class='col col--grow'>
|
||||
<div>
|
||||
{% if expiration_date %}
|
||||
<local-datetime
|
||||
timestamp='{{ expiration_date }}'
|
||||
@ -73,32 +75,49 @@
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<a href='{{ url_for("portfolios.view_task_order", portfolio_id=portfolio.id, task_order_id=task_order.id) }}' class='icon-link'>
|
||||
{{ Icon('cog') }}
|
||||
Manage Task Order
|
||||
</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 %}
|
||||
{{ Icon('arrow-down') }}
|
||||
<span>{{ remaining_days }}</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>Remaining</dt>
|
||||
<dd>
|
||||
{% if remaining_days is not none %}
|
||||
{{ remaining_days }} days
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<a href='{{ url_for("portfolios.portfolio", portfolio_id=portfolio.id) }}' class='icon-link'>
|
||||
Manage Task Order
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<dl class='to-summary__co'>
|
||||
<dt>Contracting Officer</dt>
|
||||
<dd>
|
||||
{{ jedi_request.contracting_officer_full_name }}
|
||||
<a class='icon-link' href='mailto:{{ jedi_request.contracting_officer_email }}'>{{ jedi_request.contracting_officer_email }}</a>
|
||||
<dt class='subheading'>Contracting Officer</dt>
|
||||
<dd class='row'>
|
||||
<div class='col col--grow'>
|
||||
{% 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>
|
||||
</dl>
|
||||
|
||||
@ -140,7 +159,7 @@
|
||||
|
||||
<div class='budget-chart panel' ref='panel'>
|
||||
<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'>
|
||||
<dl class='budget-chart__legend__spend'>
|
||||
@ -329,7 +348,7 @@
|
||||
|
||||
<div class='spend-table responsive-table-wrapper'>
|
||||
<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'>
|
||||
{% 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'>{{ prev_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>
|
||||
|
||||
<tbody class='spend-table__portfolio'>
|
||||
<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(prev_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>
|
||||
<th scope='rowgroup'>
|
||||
<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>
|
||||
<span v-html='name'></span>
|
||||
</button>
|
||||
@ -416,10 +435,9 @@
|
||||
|
||||
<tr v-for='(environment, envName) in environments[name]' v-show='application.isVisible' class='spend-table__application__env'>
|
||||
<th scope='rowgroup'>
|
||||
<a href='#' class='icon-link spend-table__application__env'>
|
||||
{{ Icon('link') }}
|
||||
<div class='icon-link spend-table__application__env'>
|
||||
<span v-html='envName'></span>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<td class='table-cell--align-right previous-month'>
|
||||
@ -440,7 +458,7 @@
|
||||
</table>
|
||||
</spend-table>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.funding" | translate %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% macro ViewLink(task_order) %}
|
||||
@ -81,6 +83,7 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ caller and caller() }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -89,49 +92,30 @@
|
||||
|
||||
<div class="portfolio-funding">
|
||||
|
||||
<div class='panel'>
|
||||
<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>
|
||||
</div>
|
||||
<div class='portfolio-funding__header row'>
|
||||
<a href="{{ url_for("task_orders.new", screen=1, portfolio_id=portfolio.id) }}" class="usa-button">Start a New Task Order</a>
|
||||
</div>
|
||||
|
||||
{% for task_order in pending_task_orders %}
|
||||
<div class='panel'>
|
||||
<div class='panel__content pending-task-order row'>
|
||||
<span class='label label--warning'>Pending</span>
|
||||
<div class="pending-task-order__started col">
|
||||
<dt>Started</dt>
|
||||
<dd>
|
||||
<local-datetime
|
||||
timestamp="{{ task_order.time_created }}"
|
||||
format="M/D/YYYY">
|
||||
</local-datetime>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pending-task-order__value col">
|
||||
<dt>Value</dt>
|
||||
<dd>{{ task_order.budget | dollars }}</dd>
|
||||
</div>
|
||||
{{ ViewLink(task_order) }}
|
||||
<div class='subheading'>
|
||||
Pending
|
||||
</div>
|
||||
<div class='panel pending-task-order row'>
|
||||
<span class='label label--warning'>Pending</span>
|
||||
<div class="pending-task-order__started col">
|
||||
<dt>Started</dt>
|
||||
<dd>
|
||||
<local-datetime
|
||||
timestamp="{{ task_order.time_created }}"
|
||||
format="M/D/YYYY">
|
||||
</local-datetime>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pending-task-order__value col">
|
||||
<dt>Value</dt>
|
||||
<dd>{{ task_order.budget | dollars }}</dd>
|
||||
</div>
|
||||
{{ ViewLink(task_order) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@ -146,16 +130,20 @@
|
||||
{% endif %}
|
||||
|
||||
{% if active_task_orders %}
|
||||
{{ TaskOrderList(active_task_orders, label='success', funded=funded) }}
|
||||
<div class='panel portfolio-total-balance'>
|
||||
<div class='panel__content row'>
|
||||
<span>{{ total_balance | dollars }}</span>
|
||||
<span class='label label--success'>Total Active Balance</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='subheading'>Active</div>
|
||||
{% call TaskOrderList(active_task_orders, label='success', funded=funded) %}
|
||||
<tr class='total-balance'>
|
||||
<td colspan='4'>
|
||||
<span class='label label--success'>Total Active Balance</span>
|
||||
<span>{{ total_balance | dollars }}</span>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
{% if expired_task_orders %}
|
||||
<div class='subheading'>Expired</div>
|
||||
{{ TaskOrderList(expired_task_orders, label='expired', expired=True) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -1,5 +1,7 @@
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.funding" | translate %}
|
||||
|
||||
{% from "components/checkbox_input.html" import CheckboxInput %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
@ -11,44 +13,52 @@
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro EditOfficerInfo(form, officer_type) -%}
|
||||
<div class='officer__form'>
|
||||
{% macro EditOfficerInfo(form, officer_type, invited) -%}
|
||||
<template v-if="editing">
|
||||
<div class='alert'>
|
||||
<div class='alert__content'>
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(form.first_name) }}
|
||||
</div>
|
||||
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(form.last_name) }}
|
||||
</div>
|
||||
<div class='officer__form'>
|
||||
<div class="edit-officer">
|
||||
<h4>{{ ("task_orders.invitations." + officer_type + ".edit_title") | translate}}</h4>
|
||||
</div>
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(form.first_name) }}
|
||||
</div>
|
||||
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(form.email, placeholder='name@mail.mil', validation='email') }}
|
||||
</div>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(form.last_name) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(form.phone_number, placeholder='(123) 456-7890', validation='usPhone') }}
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(form.email, placeholder='name@mail.mil', validation='email') }}
|
||||
</div>
|
||||
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(form.phone_number, placeholder='(123) 456-7890', validation='usPhone') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class='form-row officer__form--dodId'>
|
||||
<div class="form-col">
|
||||
{% if not invited %}
|
||||
<div class='form-row'>
|
||||
{{ CheckboxInput(form.invite, label=(("forms.officers." + officer_type + "_invite") | translate)) }}
|
||||
</div>
|
||||
{% 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>
|
||||
{% if form.dod_id.data %}
|
||||
{{ TextInput(form.dod_id, validation='dodId', disabled=True)}}
|
||||
{% endif %}
|
||||
<div class='alert__actions officer__form--actions'>
|
||||
<a href="#{{ officer_type }}" v-on:click="cancel" class="icon-link">
|
||||
{{ Icon("x") }}
|
||||
<span>Cancel</span>
|
||||
</a>
|
||||
<input type='submit' class='usa-button usa-button-primary' value='Save Changes' />
|
||||
</div>
|
||||
</div>
|
||||
<div class='alert__actions officer__form--actions'>
|
||||
<a href="#{{ officer_type }}" v-on:click="cancel" class="icon-link">
|
||||
{{ Icon("x") }}
|
||||
<span>Cancel</span>
|
||||
</a>
|
||||
<input type='submit' class='usa-button usa-button-primary' value='Save Changes' />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro OfficerInfo(task_order, officer_type, form) %}
|
||||
@ -65,8 +75,11 @@
|
||||
{% set email = task_order[prefix + "_email"] %}
|
||||
{% set phone_number = task_order[prefix + "_phone_number"] %}
|
||||
{% set dod_id = task_order[prefix + "_dod_id"] %}
|
||||
{% set invited = False %}
|
||||
|
||||
|
||||
{% if task_order[officer_type] %}
|
||||
{% set invited = True %}
|
||||
<div class="officer__info">
|
||||
<div class="row">
|
||||
<div class="officer__info--name">{{ first_name }} {{ last_name }}</div>
|
||||
@ -99,7 +112,7 @@
|
||||
<div class="officer__actions">
|
||||
{{ Link("Update", "edit", onClick="edit") }}
|
||||
{{ 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 }}
|
||||
</button>
|
||||
</div>
|
||||
@ -111,13 +124,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 }}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ EditOfficerInfo(form, officer_type) }}
|
||||
{{ EditOfficerInfo(form, officer_type, invited) }}
|
||||
</div>
|
||||
</edit-officer-form>
|
||||
</div>
|
||||
|
@ -1,5 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.funding" | translate %}
|
||||
|
||||
{% from "components/edit_link.html" import EditLink %}
|
||||
{% from "components/required_label.html" import RequiredLabel %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
@ -7,6 +9,7 @@
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/review_field.html" import ReviewField %}
|
||||
{% from "components/upload_input.html" import UploadInput %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -14,21 +17,26 @@
|
||||
|
||||
{% 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">
|
||||
{% endblock %}
|
||||
|
||||
{{ form.csrf_token }}
|
||||
|
||||
{% block form %}
|
||||
|
||||
{% set message = "task_orders.ko_review.submitted_by" | translate({"name": task_order.creator.full_name}) %}
|
||||
|
||||
{{ Alert(("task_orders.ko_review.alert_title" | translate), message, level='warning',
|
||||
fragment="fragments/ko_review_alert.html") }}
|
||||
<div class="top-message">
|
||||
<h1 class="subheading title">
|
||||
{{ "task_orders.ko_review.title" | translate }}
|
||||
</h1>
|
||||
{% include "fragments/ko_review_message.html" %}
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
|
||||
<div class="panel__heading">
|
||||
<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 }}
|
||||
</h1>
|
||||
</div>
|
||||
@ -58,11 +66,7 @@
|
||||
<div class="h2">{{ "task_orders.ko_review.task_order_information"| translate }}</div>
|
||||
|
||||
<div class="form__sub-fields">
|
||||
<div class="usa-input">
|
||||
<div class="usa-input__title">{{ form.pdf.label }}</div>
|
||||
{{ form.pdf.description }}
|
||||
{{ form.pdf }}
|
||||
</div>
|
||||
{{ UploadInput(form.pdf) }}
|
||||
{{ TextInput(form.number) }}
|
||||
{{ TextInput(form.loa) }}
|
||||
{{ TextInput(form.custom_clauses, paragraph=True) }}
|
||||
|
@ -1,22 +1,47 @@
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.funding" | translate %}
|
||||
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% macro Step(title="", description="", link_text=None, complete=True) %}
|
||||
<div class="task-order-next-steps__step panel__content row">
|
||||
<div class="task-order-next-steps__icon col">
|
||||
<span>{{ Icon("ok", classes="complete" if complete else "incomplete") }}</span>
|
||||
</div>
|
||||
<div class="task-order-next-steps__text col">
|
||||
<div class="task-order-next-steps__heading row">
|
||||
<h4>{{ title }}</h4>
|
||||
{% macro officer_name(officer) -%}
|
||||
{%- 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">
|
||||
{% if complete %}
|
||||
<span class="label label--success">Completed</span>
|
||||
{% else %}
|
||||
<span class="label">Not Started</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="task-order-next-steps__description">
|
||||
{{ description }}
|
||||
<div class="task-order-next-steps__text col col--grow">
|
||||
<div class="task-order-next-steps__heading row">
|
||||
<span>{{ description }}</span>
|
||||
</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 %}
|
||||
{{ caller() }}
|
||||
{% endif %}
|
||||
@ -27,7 +52,7 @@
|
||||
{% set disabled = not link_url %}
|
||||
<div class="task-order-document-link">
|
||||
<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">
|
||||
<span>{{ Icon("download") }}</span>
|
||||
</div>
|
||||
@ -44,14 +69,27 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro InvitationStatus(title, officer) %}
|
||||
{% macro InvitationStatus(title, officer, officer_info) %}
|
||||
{% set class = "invited" if officer else "uninvited" %}
|
||||
<div class="task-order-invitation-status row">
|
||||
<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 class="task-order-invitation-status__title col {{ class }}">
|
||||
{{ title }}
|
||||
<div class="col">
|
||||
<div class="task-order-invitation-status__title {{ class }}">
|
||||
{{ title }}
|
||||
</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>
|
||||
{% endmacro %}
|
||||
@ -85,42 +123,42 @@
|
||||
<div class="task-order-details">
|
||||
<div id="next-steps" class="task-order-next-steps">
|
||||
<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(
|
||||
title="Submit draft Task Order",
|
||||
description="Complete initial task order request form.",
|
||||
link_text="edit",
|
||||
description="task_orders.view.steps.draft" | translate({
|
||||
"contact": officer_name(task_order.creator)
|
||||
})| safe,
|
||||
button_url=url_for("task_orders.new", screen=1, task_order_id=task_order.id),
|
||||
button_text='Edit',
|
||||
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 %}
|
||||
{{ Step(
|
||||
title="Complete a Security Requirements Document",
|
||||
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.",
|
||||
description="task_orders.view.steps.security" | translate({
|
||||
"security_officer": officer_name(task_order.security_officer)
|
||||
}) | safe,
|
||||
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(
|
||||
title="Prepare the Task Order Documents for your organization's contracting system",
|
||||
description="You'll file your task order in your organization's contracting system. Change the formatting based on your office prefers.",
|
||||
description="task_orders.view.steps.sign" | translate({
|
||||
"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) }}
|
||||
{{ 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 class="task-order-sidebar col">
|
||||
@ -133,13 +171,23 @@
|
||||
format="M/D/YYYY">
|
||||
</local-datetime>
|
||||
{%- endset %}
|
||||
{{ DocumentLink(
|
||||
title="Task Order Draft",
|
||||
link_url=all_sections_complete and url_for('task_orders.download_summary', task_order_id=task_order.id),
|
||||
description=description) }}
|
||||
{% 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(
|
||||
title="Task Order Draft",
|
||||
link_url=all_sections_complete and url_for('task_orders.download_summary', task_order_id=task_order.id),
|
||||
description=description) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr />
|
||||
<div class="panel__content">
|
||||
{{ DocumentLink(
|
||||
title="Instruction Sheet",
|
||||
link_url="#") }}
|
||||
{{ DocumentLink(
|
||||
title="Cloud Services Estimate",
|
||||
link_url=task_order.csp_estimate and url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) ) }}
|
||||
@ -153,15 +201,16 @@
|
||||
</div>
|
||||
<div class="task-order-invitations panel">
|
||||
<div class="panel__content">
|
||||
<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">
|
||||
{{ Icon("edit") }}
|
||||
<span>manage invitations</span>
|
||||
</a>
|
||||
<div class="task-order-invitations__heading row">
|
||||
<h3>Invitations</h3>
|
||||
<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") }}
|
||||
</a>
|
||||
</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>
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
{% block form %}
|
||||
|
||||
|
||||
<!-- App Info Section -->
|
||||
<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") }}
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/options_input.html" import OptionsInput %}
|
||||
{% from "components/date_input.html" import DateInput %}
|
||||
{% from "components/upload_input.html" import UploadInput %}
|
||||
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
@ -32,22 +33,7 @@
|
||||
{{ Icon("link")}} Go to Cloud Service Provider’s estimate calculator
|
||||
</a></p>
|
||||
<p>{{ "task_orders.new.funding.estimate_usage_paragraph" | translate }}</p>
|
||||
<template v-if="showUpload">
|
||||
<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>
|
||||
{{ UploadInput(form.csp_estimate, show_label=True) }}
|
||||
|
||||
<hr>
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
<h1 class="panel__content">Let's get started</h1>
|
||||
<div class="panel__content">
|
||||
<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>
|
||||
</div>
|
||||
<span class="task-order-get-started__list panel__content">
|
||||
@ -33,7 +33,7 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{{ Help(
|
||||
name="Development Lead",
|
||||
@ -43,13 +43,13 @@
|
||||
{{ Help(
|
||||
name="Security Lead",
|
||||
icon_name="shield",
|
||||
description="Your security lead will review and approve your security classification needs, as well as a standardized DD-254.",
|
||||
link_text="You'll need their DOD ID number") }}
|
||||
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") }}
|
||||
{{ Help(
|
||||
name="Contracting Officer",
|
||||
icon_name="dollar-sign",
|
||||
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>
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
<a href="{{ url_for("task_orders.new", screen=1) }}" class="usa-button usa-button-big">Let's do cloud!</a>
|
||||
</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>
|
||||
|
@ -19,7 +19,7 @@
|
||||
{{ CheckboxInput(form.ko_invite) }}
|
||||
<keep-alive>
|
||||
<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>
|
||||
</keep-alive>
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
{{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_phone_number) }}
|
||||
{{ CheckboxInput(form.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>
|
||||
</div>
|
||||
</cordata>
|
||||
@ -49,10 +49,11 @@
|
||||
{{ CheckboxInput(form.so_invite) }}
|
||||
<keep-alive>
|
||||
<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>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</oversight>
|
||||
|
||||
|
||||
{% 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:
|
||||
def test_pending_status(self):
|
||||
def test_started_status(self):
|
||||
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
|
||||
|
||||
def test_active_status(self):
|
||||
@ -47,7 +50,7 @@ class TestCSPEstimate:
|
||||
attachment = Attachment(filename="sample.pdf", object_name="sample")
|
||||
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):
|
||||
to = TaskOrder()
|
||||
@ -77,3 +80,41 @@ class TestCSPEstimate:
|
||||
|
||||
to.csp_estimate = ""
|
||||
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 tests.factories import (
|
||||
@ -20,7 +21,7 @@ def test_user_with_permission_has_budget_report_link(client, user_session):
|
||||
user_session(portfolio.owner)
|
||||
response = client.get("/portfolios/{}/applications".format(portfolio.id))
|
||||
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):
|
||||
portfolio = PortfolioFactory.create()
|
||||
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):
|
||||
portfolio = PortfolioFactory.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):
|
||||
portfolio = PortfolioFactory.create()
|
||||
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):
|
||||
user = UserFactory.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):
|
||||
portfolio = PortfolioFactory.create()
|
||||
task_order = TaskOrderFactory.create(portfolio=portfolio)
|
||||
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
|
||||
user_session(portfolio.owner)
|
||||
client.post(
|
||||
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,
|
||||
},
|
||||
)
|
||||
client.post(url_for("task_orders.invite", task_order_id=task_order.id))
|
||||
|
||||
# contracting officer accepts invitation
|
||||
user = Users.get_by_dod_id(user_info["dod_id"])
|
||||
|
@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
from flask import url_for
|
||||
|
||||
from tests.factories import (
|
||||
@ -36,6 +37,7 @@ def create_portfolio_and_invite_user(
|
||||
return portfolio
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Temporarily no add member link")
|
||||
def test_user_with_permission_has_add_member_link(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
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):
|
||||
user = UserFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
@ -89,6 +92,7 @@ def test_create_member(client, user_session):
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert user.full_name in response.data.decode()
|
||||
assert user.has_portfolios
|
||||
assert user.invitations
|
||||
assert len(queue.get_queue()) == queue_length + 1
|
||||
|
@ -1,6 +1,7 @@
|
||||
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):
|
||||
@ -13,3 +14,29 @@ def test_update_portfolio_name(client, user_session):
|
||||
)
|
||||
assert response.status_code == 200
|
||||
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
|
||||
)
|
||||
|