Refactor mock reporting data and class methods

All mock reporting data was moved to a JSON file. The concept of
what JEDI CLIN a particular environment drew money from was added to the
data.

This change had a cascade effect to the reporting class methods,
templates, and Vue components that ingested that reporting data. Many of
these files were modified to adapt to these changes. This also included
modifying the obligated funding bar graphs to reflect new design
changes.
This commit is contained in:
graham-dds 2019-12-09 09:20:31 -05:00
parent d9c79b9b58
commit dc9a21a501
11 changed files with 676 additions and 400 deletions

View File

@ -0,0 +1,390 @@
{
"A-Wing": {
"applications": [
{
"name": "LC04",
"environments": [
{
"name": "Integ",
"spending": {
"this_month": {
"JEDI_CLIN_1": 663,
"JEDI_CLIN_2": 397
},
"last_month": {
"JEDI_CLIN_1": 590,
"JEDI_CLIN_2": 829
},
"total": {
"JEDI_CLIN_1": 42467,
"JEDI_CLIN_2": 33873
}
}
},
{
"name": "PreProd",
"spending": {
"this_month": {
"JEDI_CLIN_1": 1000,
"JEDI_CLIN_2": 626
},
"last_month": {
"JEDI_CLIN_1": 685,
"JEDI_CLIN_2": 331
},
"total": {
"JEDI_CLIN_1": 21874,
"JEDI_CLIN_2": 25506
}
}
},
{
"name": "Prod",
"spending": {
"this_month": {
"JEDI_CLIN_1": 856,
"JEDI_CLIN_2": 627
},
"last_month": {
"JEDI_CLIN_1": 921,
"JEDI_CLIN_2": 473
},
"total": {
"JEDI_CLIN_1": 35566,
"JEDI_CLIN_2": 42514
}
}
}
]
},
{
"name": "SF18",
"environments": [
{
"name": "Integ",
"spending": {
"this_month": {
"JEDI_CLIN_1": 777,
"JEDI_CLIN_2": 850
},
"last_month": {
"JEDI_CLIN_1": 584,
"JEDI_CLIN_2": 362
},
"total": {
"JEDI_CLIN_1": 44505,
"JEDI_CLIN_2": 21378
}
}
},
{
"name": "PreProd",
"spending": {
"this_month": {
"JEDI_CLIN_1": 487,
"JEDI_CLIN_2": 733
},
"last_month": {
"JEDI_CLIN_1": 542,
"JEDI_CLIN_2": 999
},
"total": {
"JEDI_CLIN_1": 8713,
"JEDI_CLIN_2": 10586
}
}
},
{
"name": "Prod",
"spending": {
"this_month": {
"JEDI_CLIN_1": 420,
"JEDI_CLIN_2": 503
},
"last_month": {
"JEDI_CLIN_1": 756,
"JEDI_CLIN_2": 941
},
"total": {
"JEDI_CLIN_1": 43003,
"JEDI_CLIN_2": 20601
}
}
}
]
},
{
"name": "Canton",
"environments": [
{
"name": "Prod",
"spending": {
"this_month": {
"JEDI_CLIN_1": 661,
"JEDI_CLIN_2": 599
},
"last_month": {
"JEDI_CLIN_1": 962,
"JEDI_CLIN_2": 383
},
"total": {
"JEDI_CLIN_1": 24501,
"JEDI_CLIN_2": 7551
}
}
}
]
},
{
"name": "BD04",
"environments": [
{
"name": "Integ",
"spending": {
"this_month": {
"JEDI_CLIN_1": 790,
"JEDI_CLIN_2": 513
},
"last_month": {
"JEDI_CLIN_1": 886,
"JEDI_CLIN_2": 991
},
"total": {
"JEDI_CLIN_1": 43684,
"JEDI_CLIN_2": 40196
}
}
},
{
"name": "PreProd",
"spending": {
"this_month": {
"JEDI_CLIN_1": 513,
"JEDI_CLIN_2": 706
},
"last_month": {
"JEDI_CLIN_1": 945,
"JEDI_CLIN_2": 380
},
"total": {
"JEDI_CLIN_1": 28189,
"JEDI_CLIN_2": 9759
}
}
}
]
},
{
"name": "SCV18",
"environments": [
{
"name": "Dev",
"spending": {
"this_month": {
"JEDI_CLIN_1": 933,
"JEDI_CLIN_2": 993
},
"last_month": {
"JEDI_CLIN_1": 319,
"JEDI_CLIN_2": 619
},
"total": {
"JEDI_CLIN_1": 40585,
"JEDI_CLIN_2": 28872
}
}
}
]
},
{
"name": "Crown",
"environments": [
{
"name": "CR Portal Dev",
"spending": {
"this_month": {
"JEDI_CLIN_1": 711,
"JEDI_CLIN_2": 413
},
"last_month": {
"JEDI_CLIN_1": 908,
"JEDI_CLIN_2": 632
},
"total": {
"JEDI_CLIN_1": 18753,
"JEDI_CLIN_2": 4004
}
}
},
{
"name": "CR Staging",
"spending": {
"this_month": {
"JEDI_CLIN_1": 440,
"JEDI_CLIN_2": 918
},
"last_month": {
"JEDI_CLIN_1": 370,
"JEDI_CLIN_2": 472
},
"total": {
"JEDI_CLIN_1": 40602,
"JEDI_CLIN_2": 6834
}
}
},
{
"name": "CR Portal Test 1",
"spending": {
"this_month": {
"JEDI_CLIN_1": 928,
"JEDI_CLIN_2": 796
},
"last_month": {
"JEDI_CLIN_1": 680,
"JEDI_CLIN_2": 312
},
"total": {
"JEDI_CLIN_1": 36058,
"JEDI_CLIN_2": 42375
}
}
},
{
"name": "Jewels Prod",
"spending": {
"this_month": {
"JEDI_CLIN_1": 304,
"JEDI_CLIN_2": 428
},
"last_month": {
"JEDI_CLIN_1": 898,
"JEDI_CLIN_2": 729
},
"total": {
"JEDI_CLIN_1": 3162,
"JEDI_CLIN_2": 49836
}
}
},
{
"name": "Jewels Dev",
"spending": {
"this_month": {
"JEDI_CLIN_1": 498,
"JEDI_CLIN_2": 890
},
"last_month": {
"JEDI_CLIN_1": 506,
"JEDI_CLIN_2": 659
},
"total": {
"JEDI_CLIN_1": 6248,
"JEDI_CLIN_2": 3866
}
}
}
]
}
]
},
"B-Wing": {
"applications": [
{
"name": "NP02",
"environments": [
{
"name": "Integ",
"spending": {
"this_month": {
"JEDI_CLIN_1": 455,
"JEDI_CLIN_2": 746
},
"last_month": {
"JEDI_CLIN_1": 973,
"JEDI_CLIN_2": 504
},
"total": {
"JEDI_CLIN_1": 11493,
"JEDI_CLIN_2": 17751
}
}
},
{
"name": "PreProd",
"spending": {
"this_month": {
"JEDI_CLIN_1": 582,
"JEDI_CLIN_2": 339
},
"last_month": {
"JEDI_CLIN_1": 392,
"JEDI_CLIN_2": 885
},
"total": {
"JEDI_CLIN_1": 41856,
"JEDI_CLIN_2": 46399
}
}
},
{
"name": "Prod",
"spending": {
"this_month": {
"JEDI_CLIN_1": 446,
"JEDI_CLIN_2": 670
},
"last_month": {
"JEDI_CLIN_1": 368,
"JEDI_CLIN_2": 963
},
"total": {
"JEDI_CLIN_1": 10030,
"JEDI_CLIN_2": 29253
}
}
}
]
},
{
"name": "FM",
"environments": [
{
"name": "Integ",
"spending": {
"this_month": {
"JEDI_CLIN_1": 994,
"JEDI_CLIN_2": 573
},
"last_month": {
"JEDI_CLIN_1": 699,
"JEDI_CLIN_2": 418
},
"total": {
"JEDI_CLIN_1": 27881,
"JEDI_CLIN_2": 37092
}
}
},
{
"name": "Prod",
"spending": {
"this_month": {
"JEDI_CLIN_1": 838,
"JEDI_CLIN_2": 839
},
"last_month": {
"JEDI_CLIN_1": 775,
"JEDI_CLIN_2": 946
},
"total": {
"JEDI_CLIN_1": 45007,
"JEDI_CLIN_2": 16197
}
}
}
]
}
]
}
}

View File

@ -1,344 +1,124 @@
from itertools import groupby from collections import defaultdict
import pendulum import json
import os
from decimal import Decimal from decimal import Decimal
from collections import OrderedDict
class ReportingInterface: def load_fixture_data():
def monthly_totals_for_environment(environment): with open(
"""Return the monthly totals for the specified environment. os.path.join(os.path.dirname(__file__), "fixture_spend_data.json"), "r"
) as json_file:
return json.load(json_file)
Data should be in the format of a dictionary with the month as the key
and the spend in that month as the value. For example:
{ "01/2018": 79.85, "02/2018": 86.54 } class MockReportingProvider:
FIXTURE_SPEND_DATA = load_fixture_data()
@classmethod
def get_portfolio_monthly_spending(cls, portfolio):
""" """
raise NotImplementedError() returns an array of application and environment spending for the
portfolio. Applications and their nested environments are sorted in
alphabetical order by name.
class MockEnvironment: [
def __init__(self, id_, env_name):
self.id = id_
self.name = env_name
class MockApplication:
def __init__(self, application_name, envs):
def make_env(name):
return MockEnvironment("{}_{}".format(application_name, name), name)
self.name = application_name
self.environments = [make_env(env_name) for env_name in envs]
def generate_sample_dates(_max=8):
current = pendulum.now()
sample_dates = []
for _i in range(_max):
sample_dates.append(current.strftime("%m/%Y"))
current = current.subtract(months=1)
reversed(sample_dates)
return sample_dates
class MockReportingProvider(ReportingInterface):
MOCK_PERCENT_EXPENDED_FUNDS = 0.75
FIXTURE_MONTHS = generate_sample_dates()
MONTHLY_SPEND_BY_ENVIRONMENT = {
"LC04_Integ": {
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": {
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": {
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": {
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": {
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": {
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": {
FIXTURE_MONTHS[4]: 28699,
FIXTURE_MONTHS[3]: 26766,
FIXTURE_MONTHS[2]: 22619,
FIXTURE_MONTHS[1]: 24090,
FIXTURE_MONTHS[0]: 16719,
},
"BD04_Integ": {},
"BD04_PreProd": {
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": {FIXTURE_MONTHS[1]: 9797},
"Crown_CR Portal Dev": {
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": {
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_Jewels Dev": {
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": {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},
}
REPORT_FIXTURE_MAP = {
"A-Wing": {
"applications": [
MockApplication("LC04", ["Integ", "PreProd", "Prod"]),
MockApplication("SF18", ["Integ", "PreProd", "Prod"]),
MockApplication("Canton", ["Prod"]),
MockApplication("BD04", ["Integ", "PreProd"]),
MockApplication("SCV18", ["Dev"]),
MockApplication(
"Crown",
[
"CR Portal Dev",
"CR Staging",
"CR Portal Test 1",
"Jewels Prod",
"Jewels Dev",
],
),
],
"budget": 500_000,
},
"B-Wing": {
"applications": [
MockApplication("NP02", ["Integ", "PreProd", "Prod"]),
MockApplication("FM", ["Integ", "Prod"]),
],
"budget": 70000,
},
}
def _rollup_application_totals(self, data):
application_totals = {}
for application, environments in data.items():
application_spend = [
(month, spend)
for env in environments.values()
if env
for month, spend in env.items()
]
application_totals[application] = {
month: sum([spend[1] for spend in spends])
for month, spends in groupby(sorted(application_spend), lambda x: x[0])
}
return application_totals
def _rollup_portfolio_totals(self, application_totals):
monthly_spend = [
(month, spend)
for application in application_totals.values()
for month, spend in application.items()
]
portfolio_totals = {}
for month, spends in groupby(sorted(monthly_spend), lambda m: m[0]):
portfolio_totals[month] = sum([spend[1] for spend in spends])
return portfolio_totals
def monthly_totals_for_environment(self, environment_id):
"""Return the monthly totals for the specified environment.
Data should be in the format of a dictionary with the month as the key
and the spend in that month as the value. For example:
{ "01/2018": 79.85, "02/2018": 86.54 }
"""
environment_monthly_totals = self.MONTHLY_SPEND_BY_ENVIRONMENT.get(
environment_id, {}
).copy()
environment_monthly_totals["total_spend_to_date"] = sum(
monthly_total for monthly_total in environment_monthly_totals.values()
)
return environment_monthly_totals
def monthly_totals(self, portfolio):
"""Return month totals rolled up by environment, application, and portfolio.
Data should returned with three top level keys, "portfolio", "applications",
and "environments".
The "applications" key will have budget data per month for each application,
The "environments" key will have budget data for each environment.
The "portfolio" key will be total monthly spending for the portfolio.
For example:
{ {
"environments": { "X-Wing": { "Prod": { "01/2018": 75.42 } } }, name
"applications": { "X-Wing": { "01/2018": 75.42 } }, this_month
"portfolio": { "01/2018": 75.42 }, last_month
total
environments [
{
name
this_month
last_month
total
}
]
} }
]
""" """
applications = portfolio.applications if portfolio.name in cls.FIXTURE_SPEND_DATA:
if portfolio.name in self.REPORT_FIXTURE_MAP: applications = cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]
applications = self.REPORT_FIXTURE_MAP[portfolio.name]["applications"] return sorted(
environments = { [
application.name: { cls._get_application_monthly_totals(application)
env.name: self.monthly_totals_for_environment(env.id) for application in applications
for env in application.environments ],
} key=lambda app: app["name"],
for application in applications
}
application_totals = self._rollup_application_totals(environments)
portfolio_totals = self._rollup_portfolio_totals(application_totals)
return {
"environments": environments,
"applications": application_totals,
"portfolio": portfolio_totals,
}
def get_obligated_funds_by_JEDI_clin(self, portfolio):
"""
Returns a dictionary of obligated funds and spending per JEDI CLIN
{
JEDI_CLIN: {
obligated_funds,
expended_funds
}
}
"""
if portfolio.name in self.REPORT_FIXTURE_MAP:
return_dict = {}
for jedi_clin, clins in groupby(
portfolio.active_clins, lambda clin: clin.jedi_clin_type
):
obligated_funds = sum(clin.obligated_amount for clin in clins)
return_dict[jedi_clin.value] = {
"obligated_funds": obligated_funds,
"expended_funds": (
obligated_funds * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS)
),
}
return OrderedDict(
# 0 index for dict item, -1 for last digit of 4 digit CLIN, e.g. 0001
sorted(return_dict.items(), key=lambda clin: clin[0][-1])
) )
return {} return []
def get_expired_task_orders(self, portfolio): @classmethod
def sorted_task_orders(to_list): def _get_environment_monthly_totals(cls, environment):
return sorted(to_list, key=lambda to: to["number"]) """
returns a dictionary that represents spending totals for an environment e.g.
{
name
this_month
last_month
total
}
"""
return {
"name": environment["name"],
"this_month": sum(environment["spending"]["this_month"].values()),
"last_month": sum(environment["spending"]["last_month"].values()),
"total": sum(environment["spending"]["total"].values()),
}
def sorted_clins(clin_list): @classmethod
return sorted(clin_list, key=lambda clin: clin["number"]) def _get_application_monthly_totals(cls, application):
"""
def serialize_clin(clin): returns a dictionary that represents spending totals for an application
return { and its environments e.g.
"number": clin.number, {
"jedi_clin_type": clin.jedi_clin_type, name
"period_of_performance": { this_month
"start_date": clin.start_date, last_month
"end_date": clin.end_date, total
}, environments: [
"total_value": clin.total_amount, {
"total_obligated_funds": clin.obligated_amount, name
"expended_funds": ( this_month
clin.obligated_amount * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS) last_month
), total
}
]
} }
"""
return sorted_task_orders( environments = sorted(
[ [
{ cls._get_environment_monthly_totals(env)
"id": task_order.id, for env in application["environments"]
"number": task_order.number, ],
"clins": sorted_clins( key=lambda env: env["name"],
[serialize_clin(clin) for clin in task_order.clins]
),
}
for task_order in portfolio.task_orders
if task_order.is_expired
]
) )
return {
"name": application["name"],
"this_month": sum(env["this_month"] for env in environments),
"last_month": sum(env["last_month"] for env in environments),
"total": sum(env["total"] for env in environments),
"environments": environments,
}
@classmethod
def get_spending_by_JEDI_clin(cls, portfolio):
"""
returns an dictionary of spending per JEDI CLIN for a portfolio
{
jedi_clin: {
invoiced
estimated
},
}
"""
if portfolio.name in cls.FIXTURE_SPEND_DATA:
CLIN_spend_dict = defaultdict(lambda: defaultdict(Decimal))
for application in cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]:
for environment in application["environments"]:
for clin, spend in environment["spending"]["this_month"].items():
CLIN_spend_dict[clin]["estimated"] += Decimal(spend)
for clin, spend in environment["spending"]["total"].items():
CLIN_spend_dict[clin]["invoiced"] += Decimal(spend)
return CLIN_spend_dict
return {}

View File

@ -1,15 +1,36 @@
from flask import current_app from flask import current_app
from itertools import groupby
class Reports: class Reports:
@classmethod @classmethod
def monthly_totals(cls, portfolio): def monthly_spending(cls, portfolio):
return current_app.csp.reports.monthly_totals(portfolio) return current_app.csp.reports.get_portfolio_monthly_spending(portfolio)
@classmethod @classmethod
def expired_task_orders(cls, portfolio): def expired_task_orders(cls, portfolio):
return current_app.csp.reports.get_expired_task_orders(portfolio) return [
task_order for task_order in portfolio.task_orders if task_order.is_expired
]
@classmethod @classmethod
def obligated_funds_by_JEDI_clin(cls, portfolio): def obligated_funds_by_JEDI_clin(cls, portfolio):
return current_app.csp.reports.get_obligated_funds_by_JEDI_clin(portfolio) clin_spending = current_app.csp.reports.get_spending_by_JEDI_clin(portfolio)
active_clins = portfolio.active_clins
for jedi_clin, clins in groupby(
active_clins, key=lambda clin: clin.jedi_clin_type
):
if not clin_spending.get(jedi_clin.name):
clin_spending[jedi_clin.name] = {}
clin_spending[jedi_clin.name]["obligated"] = sum(
clin.obligated_amount for clin in clins
)
return [
{
"name": clin,
"invoiced": clin_spending[clin].get("invoiced", 0),
"estimated": clin_spending[clin].get("estimated", 0),
"obligated": clin_spending[clin].get("obligated", 0),
}
for clin in sorted(clin_spending.keys())
]

View File

@ -5,6 +5,7 @@ from flask import render_template
from jinja2 import contextfilter from jinja2 import contextfilter
from jinja2.exceptions import TemplateNotFound from jinja2.exceptions import TemplateNotFound
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
from decimal import DivisionByZero as DivisionByZeroException
def iconSvg(name): def iconSvg(name):
@ -38,6 +39,14 @@ def usPhone(number):
return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:]) return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:])
def obligatedFundingGraphWidth(values):
numerator, denominator = values
try:
return (numerator / denominator) * 100
except DivisionByZeroException:
return 0
def formattedDate(value, formatter="%m/%d/%Y"): def formattedDate(value, formatter="%m/%d/%Y"):
if value: if value:
return value.strftime(formatter) return value.strftime(formatter)
@ -76,6 +85,7 @@ def register_filters(app):
app.jinja_env.filters["pageWindow"] = pageWindow app.jinja_env.filters["pageWindow"] = pageWindow
app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent
app.jinja_env.filters["withExtraParams"] = with_extra_params app.jinja_env.filters["withExtraParams"] = with_extra_params
app.jinja_env.filters["obligatedFundingGraphWidth"] = obligatedFundingGraphWidth
@contextfilter @contextfilter
def translateWithoutCache(context, *kwargs): def translateWithoutCache(context, *kwargs):

View File

@ -1,4 +1,4 @@
from datetime import date, datetime, timedelta from datetime import datetime
from flask import redirect, render_template, url_for, request as http_request, g from flask import redirect, render_template, url_for, request as http_request, g
@ -35,9 +35,6 @@ def create_portfolio():
@user_can(Permissions.VIEW_PORTFOLIO_REPORTS, message="view portfolio reports") @user_can(Permissions.VIEW_PORTFOLIO_REPORTS, message="view portfolio reports")
def reports(portfolio_id): def reports(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id) portfolio = Portfolios.get(g.current_user, portfolio_id)
today = date.today()
current_month = date(int(today.year), int(today.month), 15)
prev_month = current_month - timedelta(days=28)
# wrapped in str() because the sum of obligated funds returns a Decimal object # wrapped in str() because the sum of obligated funds returns a Decimal object
total_portfolio_value = str( total_portfolio_value = str(
sum( sum(
@ -51,10 +48,10 @@ def reports(portfolio_id):
total_portfolio_value=total_portfolio_value, total_portfolio_value=total_portfolio_value,
current_obligated_funds=Reports.obligated_funds_by_JEDI_clin(portfolio), current_obligated_funds=Reports.obligated_funds_by_JEDI_clin(portfolio),
expired_task_orders=Reports.expired_task_orders(portfolio), expired_task_orders=Reports.expired_task_orders(portfolio),
monthly_totals=Reports.monthly_totals(portfolio), monthly_spending=Reports.monthly_spending(portfolio),
current_month=current_month, current_month=current_month,
prev_month=prev_month, prev_month=prev_month,
now=datetime.now(), # mocked datetime of reporting data retrival retrieved=datetime.now(), # mocked datetime of reporting data retrival
) )

View File

@ -1,14 +1,12 @@
import { set } from 'vue/dist/vue' import { set } from 'vue/dist/vue'
import { formatDollars } from '../../lib/dollars' import { formatDollars } from '../../lib/dollars'
import { set as _set } from 'lodash'
export default { export default {
name: 'spend-table', name: 'spend-table',
props: { props: {
applications: Object, applications: Array,
environments: Object,
currentMonthIndex: String,
prevMonthIndex: String,
}, },
data: function() { data: function() {
@ -18,20 +16,16 @@ export default {
}, },
created: function() { created: function() {
Object.keys(this.applications).forEach(application => { this.applicationsState.forEach(application => {
set(this.applicationsState[application], 'isVisible', false) application.isVisible = false
}) })
}, },
methods: { methods: {
toggle: function(e, applicationName) { toggle: function(e, applicationIndex) {
this.applicationsState = Object.assign(this.applicationsState, { set(this.applicationsState, applicationIndex, {
[applicationName]: Object.assign( ...this.applicationsState[applicationIndex],
this.applicationsState[applicationName], isVisible: !this.applicationsState[applicationIndex].isVisible,
{
isVisible: !this.applicationsState[applicationName].isVisible,
}
),
}) })
}, },

View File

@ -73,6 +73,10 @@
color: $color-green; color: $color-green;
} }
.text-danger {
color: $color-secondary;
}
.user-permission { .user-permission {
font-weight: $font-normal; font-weight: $font-normal;
} }

View File

@ -16,6 +16,16 @@
} }
.jedi-clin-funding { .jedi-clin-funding {
$insufficient-gradient: repeating-linear-gradient(
45deg,
$color-secondary-dark,
$color-secondary-dark 10px,
$color-secondary-darkest 11px,
$color-secondary-darkest 14px
);
$graph-bar-height: 2rem;
padding-top: $gap * 3; padding-top: $gap * 3;
padding-bottom: $gap * 3; padding-bottom: $gap * 3;
@ -37,14 +47,36 @@
margin: 0; margin: 0;
} }
&__meter { &__graph {
margin: 10px 0;
-moz-transform: scale(-1, 1);
-webkit-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
transform: scale(-1, 1);
width: 100%; width: 100%;
height: $graph-bar-height;
margin-top: $gap * 2;
margin-bottom: $gap * 2;
display: flex;
&-bar {
height: 100%;
display: block;
float: left;
margin-right: $gap / 2;
&:last-child {
margin-right: 0;
}
&--invoiced {
background: $color-green;
}
&--estimated {
background: $color-green-lighter;
}
&--remaining {
background: $color-primary-darkest;
}
&--insufficient {
background: $insufficient-gradient;
}
}
&-values { &-values {
display: flex; display: flex;
@ -52,13 +84,32 @@
} }
&__meta { &__meta {
&--remaining { margin-right: $gap * 5;
margin-left: auto;
text-align: right;
}
&-header { &-header {
@include small-copy; @include small-copy;
margin-bottom: 0; margin-bottom: 0;
display: flex;
align-items: center;
}
&-key {
height: $graph-bar-height;
width: $graph-bar-height;
margin-right: $gap / 2;
&--invoiced {
background: $color-green;
}
&--estimated {
background: $color-green-lighter;
}
&--remaining {
background: $color-primary-darkest;
}
&--insufficient {
background: $insufficient-gradient;
}
} }
&-value { &-value {
margin-bottom: 0; margin-bottom: 0;

View File

@ -24,12 +24,7 @@
) }} ) }}
{% else %} {% else %}
<spend-table <spend-table v-bind:applications='{{ monthly_spending | tojson }}' inline-template>
v-bind:applications='{{ monthly_totals['applications'] | tojson }}'
v-bind:environments='{{ monthly_totals['environments'] | tojson }}'
current-month-index='{{ current_month_index }}'
prev-month-index='{{ prev_month_index }}'
inline-template>
<div class="responsive-table-wrapper"> <div class="responsive-table-wrapper">
<table class="atat-table"> <table class="atat-table">
<thead> <thead>
@ -41,41 +36,41 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for='(application, name) in applicationsState'> <template v-for='(application, applicationIndex) in applicationsState'>
<tr> <tr>
<td> <td>
<button v-on:click='toggle($event, name)' class='icon-link icon-link--large'> <button v-on:click='toggle($event, applicationIndex)' class='icon-link icon-link--large'>
<span v-html='name'></span> <span v-html='application.name'></span>
<template v-if='application.isVisible'>{{ Icon('caret_down') }}</template> <template v-if='application.isVisible'>{{ Icon('caret_down') }}</template>
<template v-else>{{ Icon('caret_up') }}</template> <template v-else>{{ Icon('caret_up') }}</template>
</button> </button>
</td> </td>
<td class="table-cell--align-right"> <td class="table-cell--align-right">
<span v-html='formatDollars(application[currentMonthIndex] || 0)'></span> <span v-html='formatDollars(application.this_month || 0)'></span>
</td> </td>
<td class="table-cell--align-right"> <td class="table-cell--align-right">
<span v-html='formatDollars(application[prevMonthIndex] || 0)'></span> <span v-html='formatDollars(application.last_month || 0)'></span>
</td> </td>
<td class="table-cell--align-right"> <td class="table-cell--align-right">
<span v-html='formatDollars(application["total_spend_to_date"])'></span> <span v-html='formatDollars(application.total)'></span>
</td> </td>
</tr> </tr>
<tr <tr
v-for='(environment, envName, index) in environments[name]'
v-show='application.isVisible' v-show='application.isVisible'
v-bind:class="[ index == Object.keys(environments[name]).length -1 ? 'reporting-spend-table__env-row--last' : '']" v-for='(environment, index) in application.environments'
v-bind:class="[ index == application.environments.length -1 ? 'reporting-spend-table__env-row--last' : '']"
> >
<td> <td>
<span class="reporting-spend-table__env-row-label" v-html='envName'></span> <span class="reporting-spend-table__env-row-label" v-html='environment.name'></span>
</td> </td>
<td class="table-cell--align-right"> <td class="table-cell--align-right">
<span v-html='formatDollars(environment[currentMonthIndex] || 0)'></span> <span v-html='formatDollars(environment.this_month || 0)'></span>
</td> </td>
<td class="table-cell--align-right"> <td class="table-cell--align-right">
<span v-html='formatDollars(environment[prevMonthIndex] || 0)'></span> <span v-html='formatDollars(environment.last_month || 0)'></span>
</td> </td>
<td class="table-cell--align-right"> <td class="table-cell--align-right">
<span v-html='formatDollars(environment["total_spend_to_date"])'></span> <span v-html='formatDollars(environment.total)'></span>
</td> </td>
</tr> </tr>
</template> </template>

View File

@ -35,13 +35,13 @@
<div>{{ ("{}".format(clin.jedi_clin_type) | translate)[15:] }}</div> <div>{{ ("{}".format(clin.jedi_clin_type) | translate)[15:] }}</div>
</td> </td>
<td> <td>
{{ clin.period_of_performance.start_date | formattedDate(formatter="%b %d, %Y") }} {{ clin.start_date | formattedDate(formatter="%b %d, %Y") }}
- -
{{ clin.period_of_performance.end_date | formattedDate(formatter="%b %d, %Y") }} {{ clin.end_date | formattedDate(formatter="%b %d, %Y") }}
</td> </td>
<td>{{ clin.total_value | dollars }}</td> <td>{{ clin.total_amount | dollars }}</td>
<td>{{ clin.total_obligated_funds | dollars }}</td> <td>{{ clin.obligated_amount | dollars }}</td>
<td>{{ (clin.total_obligated_funds - clin.expended_funds) | dollars }}</td> <td>{{ 0 | dollars }}</td>
<tr> <tr>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}

View File

@ -3,28 +3,62 @@
<section> <section>
<header class="reporting-section-header"> <header class="reporting-section-header">
<h2 class="reporting-section-header__header">Current Obligated funds</h2> <h2 class="reporting-section-header__header">Current Obligated funds</h2>
<span class="reporting-section-header__subheader">As of {{ now | formattedDate(formatter="%B %d, %Y at %H:%M") }}</span> <span class="reporting-section-header__subheader">As of {{ retrieved | formattedDate(formatter="%B %d, %Y at %H:%M") }}</span>
</header> </header>
<div class='panel'> <div class='panel'>
<div class='panel__content jedi-clin-funding'> <div class='panel__content jedi-clin-funding'>
{% for JEDI_clin, funds in current_obligated_funds.items() %} {% for JEDI_clin in current_obligated_funds %}
{% set remaining_funds = (funds["obligated_funds"] - funds["expended_funds"]) %} {% set remaining_funds = JEDI_clin.obligated - (JEDI_clin.invoiced + JEDI_clin.estimated) %}
<div class="jedi-clin-funding__clin-wrapper"> <div class="jedi-clin-funding__clin-wrapper">
<h3 class="h5 jedi-clin-funding__header"> <h3 class="h5 jedi-clin-funding__header">
{{ "JEDICLINType.{}".format(JEDI_clin) | translate }} {{ "JEDICLINType.{}".format(JEDI_clin.name) | translate }}
</h3> </h3>
<p class="jedi-clin-funding__subheader">Total obligated amount: {{ funds["obligated_funds"] | dollars }}</p> <p class="jedi-clin-funding__subheader">Total obligated amount: {{ JEDI_clin.obligated | dollars }}</p>
<meter class="jedi-clin-funding__meter" value='{{remaining_funds}}' min='0' max='{{ funds["obligated_funds"] }}' title='{{ JEDI_clin }}'> <div class="jedi-clin-funding__graph">
<div class='jedi-clin-funding__meter-fallback' style='width:{{ (funds["expended_funds"] / funds["obligated_funds"]) * 100 }}%;'></div> {% if remaining_funds < 0 %}
</meter> <span style="width:100%" class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--insufficient"></span>
<div class="jedi-clin-funding__meter-values"> {% else %}
{% set invoiced_width = (JEDI_clin.invoiced, JEDI_clin.obligated) | obligatedFundingGraphWidth %}
{% if invoiced_width %}
<span style="width:{{ invoiced_width }}%"
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--invoiced">
</span>
{% endif %}
{% set estimated_width = (JEDI_clin.estimated, JEDI_clin.obligated) | obligatedFundingGraphWidth %}
{% if estimated_width %}
<span style="width:{{ (JEDI_clin.estimated, JEDI_clin.obligated) | obligatedFundingGraphWidth }}%"
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--estimated">
</span>
{% endif %}
<span style="width:{{ (remaining_funds, JEDI_clin.obligated) | obligatedFundingGraphWidth }}%"
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--remaining">
</span>
{% endif %}
</div>
<div class="jedi-clin-funding__graph-values">
<div class="jedi-clin-funding__meta"> <div class="jedi-clin-funding__meta">
<p class="jedi-clin-funding__meta-header">Funds expended:</p> <p class="jedi-clin-funding__meta-header">
<p class="h3 jedi-clin-funding__meta-value">{{ funds["expended_funds"] | dollars }}</p> <span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--invoiced"></span>
Invoiced expended funds:
</p>
<p class="h3 jedi-clin-funding__meta-value">{{ JEDI_clin.invoiced | dollars }}</p>
</div> </div>
<div class="jedi-clin-funding__meta jedi-clin-funding__meta--remaining"> <div class="jedi-clin-funding__meta">
<p class="jedi-clin-funding__meta-header">Remaining funds:</p> <p class="jedi-clin-funding__meta-header">
<p class="h3 jedi-clin-funding__meta-value">{{ remaining_funds | dollars }}</p> <span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--estimated"></span>
Estimated expended funds:
</p>
<p class="h3 jedi-clin-funding__meta-value">{{ JEDI_clin.estimated | dollars }}</p>
</div>
<div class="jedi-clin-funding__meta">
<p class="jedi-clin-funding__meta-header">
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--{{"remaining" if remaining_funds > 0 else "insufficient"}}"></span>
Remaining funds:
</p>
<p class="h3 jedi-clin-funding__meta-value {% if remaining_funds < 0 %}text-danger{% endif %}">{{ remaining_funds | dollars }}</p>
</div> </div>
</div> </div>
</div> </div>