diff --git a/assets/js/components/ChartSourceController/components/BarChart/index.jsx b/assets/js/components/ChartSourceController/components/BarChart/index.jsx index d7f13d973..4ac6c81a9 100644 --- a/assets/js/components/ChartSourceController/components/BarChart/index.jsx +++ b/assets/js/components/ChartSourceController/components/BarChart/index.jsx @@ -44,7 +44,11 @@ class BarChart extends React.Component { const calcHeight = (scale) => { const config = createChartJsConfig({items, color, rotated, viewportWidth, barTypes}); - return (config.data.datasets[0].data.length * (25 * scale)) + 55; + if (config.data.datasets.length > 0) { + return (config.data.datasets[0].data.length * (25 * scale)) + 55; + } else { + return 0; + } }; @@ -71,10 +75,11 @@ class BarChart extends React.Component { const viewportWidth = window.innerWidth; const config = createChartJsConfig({items, color, rotated, viewportWidth, barTypes}); - config.data.datasets.forEach(({data}, index) => { - chartInstance.data.datasets[index].data = data; - }); - + if (chartInstance.data.datasets.length > 0) { + config.data.datasets.forEach(({data}, index) => { + chartInstance.data.datasets[index].data = data; + }); + } return chartInstance.update(); } diff --git a/assets/js/components/ChartSourceController/components/BarChart/services/createChartJsConfig/index.js b/assets/js/components/ChartSourceController/components/BarChart/services/createChartJsConfig/index.js index 72db05362..f92498c45 100644 --- a/assets/js/components/ChartSourceController/components/BarChart/services/createChartJsConfig/index.js +++ b/assets/js/components/ChartSourceController/components/BarChart/services/createChartJsConfig/index.js @@ -1,7 +1,7 @@ /* eslint no-param-reassign: 0 */ -import { zip } from 'lodash'; +import {zip} from 'lodash'; import colorString from 'color-string'; import Chart from 'chart.js'; @@ -10,214 +10,248 @@ import isObjectLiteral from './services/isObjectLiteral/index.js'; const recursiveHeadingOverrides = (result, obj) => { - Object.keys(obj).forEach((key) => { - if (isObjectLiteral(obj[key])) { - result.labels.push(`heading: ${key}`); - result.values.push(0); - - return recursiveHeadingOverrides(result, obj[key]); - } - - result.labels.push(key); - result.values.push(obj[key]); - return null; - }); + Object.keys(obj).forEach((key) => { + if (isObjectLiteral(obj[key])) { + result.labels.push(`heading: ${key}`); + result.values.push(0); + + return recursiveHeadingOverrides(result, obj[key]); + } + + result.labels.push(key); + result.values.push(obj[key]); + return null; + }); }; const flattenNesting = (obj) => { - const result = { labels: [], values: [] }; - recursiveHeadingOverrides(result, obj); - return result; + const result = {labels: [], values: []}; + recursiveHeadingOverrides(result, obj); + return result; }; const calcLabelPosition = (height, x, y, maxWidth) => { - if (x > maxWidth / 2) { + if (x > maxWidth / 2) { + return { + textX: x - (height / 3), + textY: y - 6, + align: 'right', + color: 'black', + space: x - (((height / 3) * 2) + 20), + }; + } + return { - textX: x - (height / 3), - textY: y - 6, - align: 'right', - color: 'black', - space: x - (((height / 3) * 2) + 20), + textX: x + (height / 3), + textY: y - 6, + align: 'left', + color: 'black', + space: maxWidth - (x + (((height / 3) * 2) + 20)), }; - } - - return { - textX: x + (height / 3), - textY: y - 6, - align: 'left', - color: 'black', - space: maxWidth - (x + (((height / 3) * 2) + 20)), - }; }; const calcLabelTruncate = (target, space, label) => { - let truncatedLabel = label; + let truncatedLabel = label; - if (target.measureText(truncatedLabel).width < space) { - return truncatedLabel; - } - - for (let characters = label.length; characters >= 0; characters--) { - truncatedLabel = truncatedLabel.substring(0, characters); if (target.measureText(truncatedLabel).width < space) { - break; + return truncatedLabel; + } + + for (let characters = label.length; characters >= 0; characters--) { + truncatedLabel = truncatedLabel.substring(0, characters); + if (target.measureText(truncatedLabel).width < space) { + break; + } } - } - return `${truncatedLabel}...`; + return `${truncatedLabel}...`; }; -const createModifyLabel = (target, fontString) => ({ label, height, x, y, maxWidth }) => { - const { textX, textY, align, color, space } = calcLabelPosition(height, x, y, maxWidth); - const fontFallbacks = '\'Source Sans\', sans-serif'; +const createModifyLabel = (target, fontString) => ({label, height, x, y, maxWidth}) => { + const {textX, textY, align, color, space} = calcLabelPosition(height, x, y, maxWidth); + const fontFallbacks = '\'Source Sans\', sans-serif'; - const regexArray = label.match(/(^heading:\s)(.+)/im); - const isHeading = /(^heading:\s)(.+)/im.test(label); - const labelAfterHeadingCheck = isHeading ? regexArray[2] : label; - const fontStyle = isHeading ? fontString(14, 'bold', fontFallbacks) : fontString(11, 'normal', fontFallbacks); + const regexArray = label.match(/(^heading:\s)(.+)/im); + const isHeading = /(^heading:\s)(.+)/im.test(label); + const labelAfterHeadingCheck = isHeading ? regexArray[2] : label; + const fontStyle = isHeading ? fontString(14, 'bold', fontFallbacks) : fontString(11, 'normal', fontFallbacks); - const truncatedLabel = calcLabelTruncate(target, space, labelAfterHeadingCheck); + const truncatedLabel = calcLabelTruncate(target, space, labelAfterHeadingCheck); - target.font = fontStyle; - target.textBaseline = 'top'; - target.fillStyle = isHeading ? 'grey' : color; - target.textAlign = align; - target.fillText(truncatedLabel, textX, isHeading ? textY + 3 : textY); + target.font = fontStyle; + target.textBaseline = 'top'; + target.fillStyle = isHeading ? 'grey' : color; + target.textAlign = align; + target.fillText(truncatedLabel, textX, isHeading ? textY + 3 : textY); }; -const dynamicLabelPlugin = ({ chart }) => { - const barInfo = chart.getDatasetMeta(0).data; - const modifyLabel = createModifyLabel(chart.ctx, Chart.helpers.fontString); +const dynamicLabelPlugin = ({chart}) => { + const barInfo = chart.getDatasetMeta(0).data; + const modifyLabel = createModifyLabel(chart.ctx, Chart.helpers.fontString); - barInfo.forEach((bar) => { - const { _xScale, _model } = bar; + barInfo.forEach((bar) => { + const {_xScale, _model} = bar; - const { maxWidth } = _xScale; - const { x, y, label, height } = _model; - modifyLabel({ label, height, x, y, maxWidth }); - }); + const {maxWidth} = _xScale; + const {x, y, label, height} = _model; + modifyLabel({label, height, x, y, maxWidth}); + }); }; +const formatDataset = ({color, barTypes}) => (data, index) => { + const [r, g, b] = colorString.get.rgb(color); + const backgroundColor = `rgba(${r}, ${g}, ${b}, 0.${(index + 1) * 20})`; -const formatDataset = ({ color, barTypes }) => (data, index) => { - const [r, g, b] = colorString.get.rgb(color); - const backgroundColor = `rgba(${r}, ${g}, ${b}, 0.${(index + 1) * 20})`; - - return { - label: barTypes[index], - data, - backgroundColor, - }; + return { + label: barTypes[index], + data, + 'backgroundColor': barTypes[index] === 'Actual Expenditure' ? '#ee9f31' : backgroundColor, + stack: barTypes[index], + borderColor: '#fff', + borderWidth: { + top: barTypes[index] === 'Actual Expenditure' ? 1 : 0, + left: 0, + right: 0 + } + }; }; const buildDatasets = (barTypes, values, color) => { - if (!barTypes) { - return [ - { - data: values, - backgroundColor: color, - }, - ]; - } - - const pivotedValues = zip(...values); - const result = pivotedValues.map(formatDataset({ color, barTypes }), []); - return result; + if (!barTypes) { + return [ + { + data: values, + backgroundColor: color + }, + ]; + } + + const pivotedValues = zip(...values); + const result = pivotedValues.map(formatDataset({color, barTypes}), []); + return result; }; -const createChartJsConfig = ({ items, rotated, color, viewportWidth, barTypes }) => { - const { labels, values } = flattenNesting(items); - const rotateLabels = viewportWidth && viewportWidth < 600 && rotated; - const datasets = buildDatasets(barTypes, values, color); - - return { - type: rotated ? 'bar' : 'horizontalBar', - data: { - labels, - datasets, - }, - options: { - barThickness: 10, - maintainAspectRatio: false, - tooltips: { - intersect: rotated, - custom: (tooltip) => { - if (!tooltip || /(^heading:\s)(.+)/im.test(tooltip.title)) { - tooltip.opacity = 0; - return; - } - tooltip.displayColors = false; - }, - callbacks: { - label: (item, dataObject) => { - const { index, datasetIndex } = item; - const { data, label } = dataObject.datasets[datasetIndex]; - const prefix = barTypes ? `${label}: ` : ''; - - return `${prefix}R${trimValues(data[index])}`; - }, +const createChartJsConfig = ({items, rotated, color, viewportWidth, barTypes}) => { + let {labels, values} = flattenNesting(items); + const rotateLabels = viewportWidth && viewportWidth < 600 && rotated; + let datasets = buildDatasets(barTypes, values, color); + + return { + type: rotated ? 'bar' : 'horizontalBar', + data: { + labels, + datasets, }, - }, - animation: { - duration: 0, - }, - layout: { - padding: { - top: 15, - bottom: 15, + options: { + barThickness: 10, + maintainAspectRatio: false, + tooltips: { + intersect: rotated, + custom: (tooltip) => { + if (!tooltip || /(^heading:\s)(.+)/im.test(tooltip.title)) { + tooltip.opacity = 0; + return; + } + tooltip.displayColors = false; + }, + callbacks: { + label: (item, dataObject) => { + const {index, datasetIndex} = item; + const {data, label} = dataObject.datasets[datasetIndex]; + const prefix = barTypes ? `${label}: ` : ''; + + return `${prefix}R${trimValues(data[index])}`; + }, + title: (data, a, b) => { + if (data.length <= 0) { + return; + } + if (data[0].datasetIndex <= 3) { + return `${data[0].label}` + } else { + return `${data[0].label} Q${data[0].datasetIndex - 3}` + } + } + }, + }, + animation: { + duration: 0, + }, + layout: { + padding: { + top: 15, + bottom: 15, + }, + }, + legend: { + display: false, + }, + scales: { + yAxes: [{ + barPercentage: 0.8, + categoryPercentage: 1.0, + display: true, + gridLines: { + color: 'transparent', + display: true, + drawBorder: false, + zeroLineColor: '#ccc', + zeroLineWidth: 1, + }, + ticks: { + display: rotated, + beginAtZero: true, + callback: value => (rotated ? `R${trimValues(value)}` : value), + }, + }], + xAxes: [{ + barPercentage: 1, + stacked: true, + categoryPercentage: 0.6, + ticks: { + beginAtZero: true, + maxRotation: rotateLabels ? 90 : 0, + minRotation: rotateLabels ? 90 : 0, + callback: value => (rotated ? value : `R${trimValues(value)}`), + }, + gridLines: { + color: 'transparent', + display: true, + drawBorder: false, + zeroLineColor: '#ccc', + zeroLineWidth: 1, + }, + }], + }, }, - }, - legend: { - display: false, - }, - scales: { - yAxes: [{ - barPercentage: 0.8, - categoryPercentage: 1.0, - display: true, - gridLines: { - color: 'transparent', - display: true, - drawBorder: false, - zeroLineColor: '#ccc', - zeroLineWidth: 1, - }, - ticks: { - display: rotated, - beginAtZero: true, - callback: value => (rotated ? `R${trimValues(value)}` : value), - }, - }], - xAxes: [{ - barPercentage: 1, - categoryPercentage: 0.6, - ticks: { - beginAtZero: true, - maxRotation: rotateLabels ? 90 : 0, - minRotation: rotateLabels ? 90 : 0, - callback: value => (rotated ? value : `R${trimValues(value)}`), - }, - gridLines: { - color: 'transparent', - display: true, - drawBorder: false, - zeroLineColor: '#ccc', - zeroLineWidth: 1, - }, - }], - }, - }, - plugins: [ - { - afterDatasetsDraw: rotated || dynamicLabelPlugin, - }, - ], - }; + plugins: [ + { + afterDatasetsDraw: rotated ? function (chart, options) { + let ctx = chart.chart.ctx; + + ctx.textAlign = 'center'; + ctx.fillStyle = '#fff'; + ctx.font = "10px \"Helvetica Neue\", Helvetica, Arial, sans-serif"; + + datasets.forEach(function (dataset, i) { + if (dataset.stack !== 'Actual Expenditure') { + return + } + let meta = chart.controller.getDatasetMeta(i); + meta.data.forEach(function (bar, index) { + const y = bar._model.y + ((bar._model.base - bar._model.y) / 2) + 5; + ctx.fillText(`Q${i - 3}`, bar._model.x, y); + }); + }); + } : dynamicLabelPlugin + }, + ], + }; }; diff --git a/assets/js/components/ChartSourceController/index.jsx b/assets/js/components/ChartSourceController/index.jsx index 0f6ca17cc..0930a1b43 100644 --- a/assets/js/components/ChartSourceController/index.jsx +++ b/assets/js/components/ChartSourceController/index.jsx @@ -1,81 +1,165 @@ import React from 'react'; import uuid from 'uuid/v4'; import BarChart from './components/BarChart/index.jsx'; +import fetchWrapper from '../../utilities/js/helpers/fetchWrapper'; -const buildToggle = ({ toggle, changeSource, source }) => { - const id = uuid(); +const buildToggle = ({toggle, changeSource, source}) => { + const id = uuid(); - const toggleItems = Object.keys(toggle).map((key) => { - const { title } = toggle[key]; - const htmlId = uuid(); + const toggleItems = Object.keys(toggle).map((key) => { + const {title} = toggle[key]; + const htmlId = uuid(); + + return ( + + ); + }); + + const {description} = toggle[source]; return ( - +
+
+ {toggleItems} +
+

{description}

+
); - }); - - const { description } = toggle[source]; - - return ( -
-
- {toggleItems} -
-

{description}

-
- ); }; -const Markup = ({ items, toggle, styling, changeSource, source, downloadText, barTypes }) => { - const { scale, color, rotated } = styling; - return ( -
- - {toggle && buildToggle({ source, toggle, changeSource })} -
- ); +const Markup = ({items, toggle, styling, changeSource, source, downloadText, barTypes}) => { + const {scale, color, rotated} = styling; + return ( +
+ + {toggle && buildToggle({source, toggle, changeSource})} +
+ ); }; class ChartSourceController extends React.Component { - constructor(...props) { - super(...props); - - const { initial, items } = this.props; - - this.state = { - source: initial || Object.keys(items)[0], - }; - - this.events = { - changeSource: this.changeSource.bind(this), - }; - } - - changeSource(source) { - this.setState({ source }); - } - - render() { - const { items: rawItems, toggle, styling, downloadText, barTypes } = this.props; - const { source } = this.state; - const { changeSource } = this.events; - const items = rawItems[source]; - - return ; - } + constructor(...props) { + super(...props); + + const {initial, items, type, inYearEnabled, departmentName} = this.props; + const source = initial || Object.keys(items)[0]; + + const barItems = this.getBarItems(this.props.items, source, type); + this.state = { + source: source, + barItems: barItems, + barTypes: this.props.barTypes + }; + const sphere = document.getElementById('sphere-slug').value; + console.log(type, inYearEnabled, sphere, departmentName); + + if (type == "expenditurePhase" && inYearEnabled && sphere == "national") + this.fetchActualExpenditureUrls(departmentName); + + this.events = { + changeSource: this.changeSource.bind(this), + }; + } + + getBarItems(barItems, source, type) { + if (type !== 'expenditurePhase') { + return barItems[source]; + } + + Object.keys(barItems).forEach((sourceType) => { + let tempItems = barItems[sourceType]; + Object.keys(tempItems).forEach((key) => { + for (let i = 0; i < 4; i++) { + // each quarter is null initially + tempItems[key].push(null); + } + }) + }) + + return barItems[source]; + } + + fetchActualExpenditureUrls(departmentName) { + console.log(departmentName); + let url = `../../actual-expenditure/?department_name=${encodeURI(departmentName)}`; + fetchWrapper(url) + .then((response) => { + for (const year in response) { + this.fetchAndSetActualExpenditure(year, response[year]); + } + }) + .catch((err) => console.warn(err)); + } + + fetchAndSetActualExpenditure(year, obj) { + let url = obj.url; + const multiplier = 1000; + fetchWrapper(url) + .then((response) => { + let barItems = this.state.barItems; + //one for each quarter + barItems[year][4] = response.summary['q1.sum'] > 0 ? response.summary['q1.sum'] * multiplier : null; //q1 + barItems[year][5] = response.summary['q2.sum'] > 0 ? response.summary['q2.sum'] * multiplier : null; //q2 + barItems[year][6] = response.summary['q3.sum'] > 0 ? response.summary['q3.sum'] * multiplier : null; //q3 + barItems[year][7] = response.summary['q4.sum'] > 0 ? response.summary['q4.sum'] * multiplier : null; //q4 + + this.setState({ + ...this.state, + barItems: barItems + }) + }) + .catch((err) => console.warn(err)); + } + + changeSource(source) { + this.setState({ + source, + barItems: this.props.items[source] + }); + } + + getToggle(toggle, type) { + if (type !== 'expenditurePhase') { + return toggle; + } + const extraText = ' Actual expenditure is not currently available on the inflation-adjusted view'; + if (toggle['real']['description'].indexOf(extraText) >= 0) { + return toggle; + } + toggle['real']['description'] += extraText + + return toggle; + } + + render() { + const {styling, downloadText} = this.props; + const {source} = this.state; + const {changeSource} = this.events; + let toggle = this.getToggle(this.props.toggle, this.props.type); + + return ; + } } diff --git a/assets/js/scenes/department/ExpenditurePhaseSection/index.html b/assets/js/scenes/department/ExpenditurePhaseSection/index.html index 73f323ba4..e035af1ac 100644 --- a/assets/js/scenes/department/ExpenditurePhaseSection/index.html +++ b/assets/js/scenes/department/ExpenditurePhaseSection/index.html @@ -71,8 +71,16 @@

Budgeted and actual expenditure comparison

{% include 'components/ChartLabel/index.html' with index="3" label="Audited outcome" %}
+ + {% if in_year_spending_enabled and sphere.slug == "national" %} +
+ {% include 'components/ChartLabel/index.html' with index="4" label="Actual expenditure" %} +
+ {% endif %} + +
Budgeted and actual expenditure comparison data-title="{{ title | truncatechars:45 }}" data-subtitle="{{ subtitle }}" data-description="{{ description }}" - data-barTypes="["Main Appropriation", "Adjusted Appropriation", "Final Appropriation", "Audited Outcome"]" + data-barTypes="["Main Appropriation", "Adjusted Appropriation", "Final Appropriation", "Audited Outcome", "Actual Expenditure", "Actual Expenditure", "Actual Expenditure", "Actual Expenditure"]" data-rotated >
diff --git a/assets/js/services/chartAdaptor/scripts.js b/assets/js/services/chartAdaptor/scripts.js index 211dbcd21..b749597aa 100644 --- a/assets/js/services/chartAdaptor/scripts.js +++ b/assets/js/services/chartAdaptor/scripts.js @@ -26,28 +26,33 @@ const normaliseData = ({ type, rawItems }) => { const ChartAdaptor = (props) => { - const { scale, type, items: rawItems, title, subtitle, description, rotated, barTypes } = props; + const { + scale, type, items: rawItems, title, subtitle, description, rotated, barTypes, + } = props; const expenditure = type === 'expenditure' - || type === 'expenditureMultiples' - || type === 'expenditurePhase'; + || type === 'expenditureMultiples' + || type === 'expenditurePhase'; const needToggle = type === 'expenditurePhase' || type === 'expenditure'; const items = normaliseData({ type, rawItems, rotated }); const color = expenditure ? '#ad3c64' : '#73b23e'; - const toggleValues = { - "nominal": { - "title": "Not adjusted for inflation" - }, - "real": { - "title": "Adjusted for inflation", - "description": `The Rand values in this chart are adjusted for CPI inflation and are the effective \ + let toggle; + if (needToggle) { + toggle = { + "nominal": { + "title": "Not adjusted for inflation" + }, + "real": { + "title": "Adjusted for inflation", + "description": `The Rand values in this chart are adjusted for CPI inflation and are the effective \ value in ${rawItems.base_financial_year.slice(0, 4)} Rands. CPI is used as the deflator, with the ${rawItems.base_financial_year} \ financial year as the base.` + } } + } else { + toggle = null; } - - const toggle = needToggle ? toggleValues : null; const downloadText = { title, @@ -56,7 +61,11 @@ const ChartAdaptor = (props) => { }; const styling = { scale, color, rotated }; - return React.createElement(ChartSourceController, { items, toggle, barTypes, styling, downloadText }); + return React.createElement(ChartSourceController, { + items, toggle, barTypes, styling, downloadText, type, + inYearEnabled: rawItems.in_year_spending_enabled, + departmentName: rawItems.department_name + }); }; diff --git a/assets/js/services/chartAdaptor/services/normaliseExpenditurePhase/index.js b/assets/js/services/chartAdaptor/services/normaliseExpenditurePhase/index.js index 9fa5a1886..8829bc3ec 100644 --- a/assets/js/services/chartAdaptor/services/normaliseExpenditurePhase/index.js +++ b/assets/js/services/chartAdaptor/services/normaliseExpenditurePhase/index.js @@ -53,7 +53,10 @@ const normalise = (itemsArray) => { const normaliseExpenditurePhase = (data) => { - const { nominal: nominalRaw, real: realRaw } = data; + const { + nominal: nominalRaw, + real: realRaw, + } = data; const nominal = normalise(nominalRaw); const real = normalise(realRaw); diff --git a/assets/scss/components/ChartLabel/styles.scss b/assets/scss/components/ChartLabel/styles.scss index e166c538f..bb6c6bb0c 100644 --- a/assets/scss/components/ChartLabel/styles.scss +++ b/assets/scss/components/ChartLabel/styles.scss @@ -27,4 +27,8 @@ &--indexOf3::after { background: #bd6383; } + + &--indexOf4::after { + background: #ee9f31; + } } diff --git a/budgetportal/datasets.py b/budgetportal/datasets.py index 074df1bba..73250e479 100644 --- a/budgetportal/datasets.py +++ b/budgetportal/datasets.py @@ -10,6 +10,7 @@ AdjustedEstimatesOfExpenditure, EstimatesOfExpenditure, ExpenditureTimeSeries, + InYearExpenditure, ) from ckanapi import NotFound from django.conf import settings @@ -199,6 +200,7 @@ def get_openspending_api(self): "budgeted-and-actual-national-expenditure": ExpenditureTimeSeries, "budgeted-and-actual-provincial-expenditure": ExpenditureTimeSeries, "consolidated-expenditure-budget": ExpenditureTimeSeries, + "in-year-spending": InYearExpenditure, } api_class = api_class_mapping[self.category.slug] self._openspending_api = api_class(api_resource["url"]) diff --git a/budgetportal/models/government.py b/budgetportal/models/government.py index b5bfd3e0b..130ac8785 100644 --- a/budgetportal/models/government.py +++ b/budgetportal/models/government.py @@ -11,6 +11,8 @@ import logging from django.urls import reverse import requests +from constance import config + logger = logging.getLogger(__name__) ckan = settings.CKAN @@ -1221,7 +1223,10 @@ def get_expenditure_time_series_summary(self): for y in range(financial_year_start_int - 3, financial_year_start_int + 1) ] - expenditure = {"nominal": [], "real": []} + expenditure = { + "nominal": [], + "real": [], + } dataset = get_expenditure_time_series_dataset(self.government.sphere.slug) if not dataset: @@ -1323,9 +1328,13 @@ def get_expenditure_time_series_summary(self): else: missing_phases_count[fiscal_year] += 1 - expenditure["base_financial_year"] = FinancialYear.slug_from_year_start( - str(base_year) - ) + expenditure.update({ + "base_financial_year": FinancialYear.slug_from_year_start( + str(base_year) + ), + "in_year_spending_enabled": config.IN_YEAR_SPENDING_ENABLED, + "department_name": self.name, + }) # Generate notices if applicable no_data_for_years = [] diff --git a/budgetportal/openspending.py b/budgetportal/openspending.py index c248c57ea..4afe9c944 100644 --- a/budgetportal/openspending.py +++ b/budgetportal/openspending.py @@ -230,6 +230,20 @@ def get_adjustment_kind_ref(self): return self.get_ref(self.get_adjustment_kind_dimension(), "label") +class InYearExpenditure(BabbageFiscalDataset): + def get_financial_year_ref(self): + return self.get_ref(self.get_financial_year_dimension(), "label") + + def get_financial_year_dimension(self): + return self.get_dimension("date") + + def get_department_name_ref(self): + return self.get_ref(self.get_department_dimension(), "label") + + def get_department_dimension(self): + return self.get_dimension("administrative_classification") + + class ExpenditureTimeSeries(AdjustedEstimatesOfExpenditure): pass diff --git a/budgetportal/settings.py b/budgetportal/settings.py index 5af5e455f..9569a8952 100644 --- a/budgetportal/settings.py +++ b/budgetportal/settings.py @@ -116,9 +116,14 @@ CONSTANCE_CONFIG = { "EQPRS_DATA_ENABLED": ( False, - "enabling / disabling summary on department page", + "enabling / disabling performance data summary on department page", bool, - ) + ), + "IN_YEAR_SPENDING_ENABLED": ( + False, + "enabling / disabling presenting in-year spending on department page", + bool, + ), } if DEBUG_TOOLBAR: diff --git a/budgetportal/templates/base.html b/budgetportal/templates/base.html index 9155e1224..b023b5c60 100644 --- a/budgetportal/templates/base.html +++ b/budgetportal/templates/base.html @@ -29,6 +29,7 @@ https://api.appzi.io/api/ https://clouderrorreporting.googleapis.com/v1beta1/projects/appzi-prod/ https://openspending.vulekamali.gov.za + https://openspending-dedicated.vulekamali.gov.za https://openspending.org https://*.google-analytics.com https://*.analytics.google.com diff --git a/budgetportal/templates/department.html b/budgetportal/templates/department.html index 871d9e024..75f8f048d 100644 --- a/budgetportal/templates/department.html +++ b/budgetportal/templates/department.html @@ -261,7 +261,7 @@

Department infrastructure projects

{% endif %} - {% if EQPRS_DATA_ENABLED %} + {% if eqprs_data_enabled %}