From c65cff0a81bfd19dbfe199747b51614da129bd9b Mon Sep 17 00:00:00 2001 From: Shiv K Sah Date: Fri, 23 Jun 2017 21:40:09 +0530 Subject: [PATCH 1/3] Common UI chart component for all reports --- cloud/endagaweb/static/css/dashboard.css | 19 + .../js/dashboard/report-chart-components.js | 849 ++++++++++++++++++ 2 files changed, 868 insertions(+) create mode 100644 cloud/endagaweb/static/js/dashboard/report-chart-components.js diff --git a/cloud/endagaweb/static/css/dashboard.css b/cloud/endagaweb/static/css/dashboard.css index b79e59bc..bd99a031 100644 --- a/cloud/endagaweb/static/css/dashboard.css +++ b/cloud/endagaweb/static/css/dashboard.css @@ -504,3 +504,22 @@ div.message { opacity: 1; } } + +/* Reports chart */ +.nv-label, .nv-legend-text { + text-transform: capitalize; +} +.nvtooltip { + text-transform: capitalize; +} +.display td { + text-align: center; +} +.display td:nth-child(1) { + background-color: #eee; + font-weight: bold; +} +.display td:nth-child(2) { + background-color: #eee; + font-weight: bold; +} \ No newline at end of file diff --git a/cloud/endagaweb/static/js/dashboard/report-chart-components.js b/cloud/endagaweb/static/js/dashboard/report-chart-components.js new file mode 100644 index 00000000..390c6efc --- /dev/null +++ b/cloud/endagaweb/static/js/dashboard/report-chart-components.js @@ -0,0 +1,849 @@ +/* + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// React chart components. + + +var TimeseriesChartWithButtonsAndDatePickers = React.createClass({ + getInitialState: function() { + // We expect many of these values to be overridden before the chart is + // first rendered -- see componentDidMount. + return { + startTimeEpoch: 0, + endTimeEpoch: -1, + isLoading: true, + chartData: {}, + activeButtonText: '', + xAxisFormatter: '%x', + yAxisFormatter: '', + activeView:'' + } + }, + getDefaultProps: function() { + // Load the current time with the user's clock if nothing is specified. We + // also specify the user's computer's timezone offset and use that to + // adjust the graph data. + var currentTime = Math.round(new Date().getTime() / 1000); + return { + title: 'title (set me!)', + chartID: 'one', + buttons: ['hour', 'day', 'week', 'month', 'year'], + icons: ['graph', 'list'], + defaultView: 'graph', + defaultButtonText: 'week', + endpoint: '/api/v1/stats/', + statTypes: 'sms', + level: 'network', + levelID: 0, + aggregation: 'count', + yAxisLabel: 'an axis label (set me!)', + currentTimeEpoch: currentTime, + timezoneOffset: 0, + tooltipUnits: '', + frontTooltip: '', + chartType: 'line-chart', + reportView: 'list' + } + }, + + // On-mount, build the charts with the default data. + // Note that we have to load the current time here. + componentDidMount: function() { + this.setState({ + activeButtonText: this.props.defaultButtonText, + activeView: this.props.defaultView, + startTimeEpoch: this.props.currentTimeEpoch - secondsMap[this.props.defaultButtonText], + endTimeEpoch: this.props.currentTimeEpoch, + // When the request params in the state have been set, go get more data. + }, function() { + this.updateChartData(); + }); + }, + + componentDidUpdate(prevProps, prevState) { + // Update if we toggled a load + if (!prevState.isLoading && this.state.isLoading) { + this.updateChartData(); + } + }, + + // This handler takes the text of the date range buttons + // and ouputs figures out the corresponding number of seconds. + handleButtonClick: function(text) { + // Update only if the startTime has actually changed. + var newStartTimeEpoch = this.props.currentTimeEpoch - secondsMap[text]; + if (this.state.startTimeEpoch != newStartTimeEpoch) { + this.setState({ + startTimeEpoch: newStartTimeEpoch, + endTimeEpoch: this.props.currentTimeEpoch, + isLoading: true, + activeButtonText: text, + }); + } + }, + + // This handler takes the text of the view mode buttons + // and ouputs figures out the corresponding number of seconds. + handleViewClick: function(text) { + // Update only if the startTime has actually changed. + if (this.state.activeView != text) { + + this.setState({ + startTimeEpoch: this.state.startTimeEpoch, + endTimeEpoch: this.props.currentTimeEpoch, + isLoading: true, + activeView: text, + }); + var interval = this.props.defaultButtonText; + setTimeout(function(){ + //this.handleButtonClick(interval); + this.forceUpdate() + }.bind(this), 1000); + + } + }, + + handleDownloadClick: function(text) { + var queryParams = { + 'start-time-epoch': this.state.startTimeEpoch, + 'end-time-epoch': this.state.endTimeEpoch, + 'stat-types': this.props.statTypes, + 'level':this.props.level, + 'level_id': this.props.levelID, + 'report-type':this.props.title, + + }; + $.get('/report/downloadcsv', queryParams, function(data,response) { + + var todayTime = new Date(); var month = (todayTime .getMonth() + 1); var day = (todayTime .getDate()); var year = (todayTime .getFullYear()); + var convertdate = year+ "-" + month + "-" +day; + this.setState({ + isLoading: false, + data: data, + title:this.props.title + }); + var filename = this.state.title + var csvData = new Blob([data], {type: 'text/csv;charset=utf-8;'}); + var csvURL = null; + if (navigator.msSaveBlob) { + csvURL = navigator.msSaveBlob(csvData, filename+"-"+convertdate+'.csv'); + } else { + csvURL = window.URL.createObjectURL(csvData); + } + var tempLink = document.createElement('a'); + document.body.appendChild(tempLink); + tempLink.href = csvURL; + tempLink.setAttribute('download', filename+"-"+convertdate+'.csv'); + tempLink.target="_self" + tempLink.click(); + }.bind(this)); + }, + + // Datepicker handlers, one each for changing the start and end times. + startTimeChange: function(newTime) { + if (newTime < this.state.endTimeEpoch && !this.state.isLoading) { + this.setState({ + startTimeEpoch: newTime, + isLoading: true, + }); + } + }, + endTimeChange: function(newTime) { + var now = moment().unix(); + if (newTime > this.state.startTimeEpoch && newTime < now + && !this.state.isLoading) { + this.setState({ + endTimeEpoch: newTime, + isLoading: true, + }); + } + }, + + // Gets new chart data from the backend. + // Recall that this must be called explicitly..it's a bit different + // than the normal react component connectivity. + // First figure out how to set the interval (or, more aptly, the bin width) + // and the x-axis formatter (see the d3 wiki on time formatting). + updateChartData: function() { + var interval, newXAxisFormatter, newYAxisFormatter; + var delta = this.state.endTimeEpoch - this.state.startTimeEpoch; + if (delta <= 60) { + interval = 'seconds'; + newXAxisFormatter = '%-H:%M:%S'; + } else if (delta <= (60 * 60)) { + interval = 'minutes'; + newXAxisFormatter = '%-H:%M'; + } else if (delta <= (24 * 60 * 60)) { + interval = 'hours'; + newXAxisFormatter = '%-H:%M'; + } else if (delta <= (7 * 24 * 60 * 60)) { + interval = 'hours'; + newXAxisFormatter = '%b %d, %-H:%M'; + } else if (delta <= (30 * 24 * 60 * 60)) { + interval = 'days'; + newXAxisFormatter = '%b %d'; + } else if (delta <= (365 * 24 * 60 * 60)) { + interval = 'days'; + newXAxisFormatter = '%b %d'; + } else { + interval = 'months'; + newXAxisFormatter = '%x'; + } + if (this.props.statTypes == 'total_data,uploaded_data,downloaded_data') { + newYAxisFormatter = '.1f'; + } else { + newYAxisFormatter = ''; + } + var queryParams = { + 'start-time-epoch': this.state.startTimeEpoch, + 'end-time-epoch': this.state.endTimeEpoch, + 'interval': interval, + 'stat-types': this.props.statTypes, + 'level-id': this.props.levelID, + 'aggregation': this.props.aggregation, + 'extras': this.props.extras, + 'dynamic-stat': this.props.dynamicStat, + 'topup-percent': this.props.topupPercent, + 'report-view': this.props.reportView + }; + var endpoint = this.props.endpoint + this.props.level; + $.get(endpoint, queryParams, function(data) { + this.setState({ + isLoading: false, + chartData: data, + xAxisFormatter: newXAxisFormatter, + yAxisFormatter: newYAxisFormatter, + }); + }.bind(this)); + }, + + render: function() { + var fromDatepickerID = 'from-datepicker-' + this.props.chartID; + var toDatepickerID = 'to-datepicker-' + this.props.chartID; + return ( +
+

{this.props.title}

+ past    + {this.props.buttons.map(function(buttonText, index) { + return ( + + ); + }, this)} + + + + +     + {this.props.icons.map(function(buttonText, index) { + return ( + + ); + }, this)} + + + + + +
+ ); + }, +}); + + +var secondsMap = { + 'hour': 60 * 60, + 'day': 24 * 60 * 60, + 'week': 7 * 24 * 60 * 60, + 'month': 30 * 24 * 60 * 60, + 'year': 365 * 24 * 60 * 60, +}; + +var add = function (a, b){ + return a + b ; +} + +// Builds the target chart from scratch. NVD3 surprisingly handles this well. +// domTarget is the SVG element's parent and data is the info that will be graphed. +var updateChart = function(domTarget, data, xAxisFormatter, yAxisFormatter, yAxisLabel, timezoneOffset, tooltipUnits, frontTooltip, chartType, domTargetId) { + // We pass in the timezone offset and calculate a locale offset. The former + // is based on the UserProfile's specified timezone and the latter is the user's + // computer's timezone offset. We manually shift the data to work around + // d3's conversion into the user's computer's timezone. Note that for my laptop + // in PST, the locale offset is positive (7hrs) while the UserProfile offset + // is negative (-7hrs). + var localeOffset = 60 * (new Date()).getTimezoneOffset(); + var shiftedData = []; + var changeAmount = []; + var tableData = []; + + for (var index in data) { + var newSeries = { 'key': data[index]['key'] }; + var newValues = []; + if( typeof(data[index]['values']) === 'object'){ + for (var series_index in data[index]['values']) { + var newValue = [ + // Shift out of the locale offset to 'convert' to UTC and then shift + // back into the operator's tz by adding the tz offset from the server. + data[index]['values'][series_index][0] + 1e3 * localeOffset + 1e3 * timezoneOffset, + data[index]['values'][series_index][1] + ]; + newValues.push(newValue); + changeAmount.push(newValue[1]) + } + // Get sum of the total charges + var sumAmount = changeAmount.reduce(add, 0); + changeAmount = [] + // sum can be of all negative values + if ( sumAmount < 0 ){ + newSeries['total'] = (sumAmount * -1); + } + else{ + newSeries['total'] = (sumAmount); + } + newSeries['values'] = newValues; + } else { + newSeries['total'] = data[index]['values']; + } + tableData.push([newSeries['key'], newSeries['total']]); + shiftedData.push(newSeries); + } + + $('.'+domTargetId).DataTable({ + data: tableData, + paging: false, + ordering: false, + info: false, + searching: false, + autoWidth: true, + scrollY: 320, + destroy: true, + columns: [ + { title: "Title" }, + { title: "Value" } + ] + }); + + + nv.addGraph(function() { + if(chartType == 'pie-chart') { + var chart = nv.models.pieChart() + .x(function(d) { return d.key.replace('_'," "); }) + .y(function(d) { return d.total; }) + .showLabels(true) + .labelType("percent"); + + chart.tooltipContent(function(key, x, y) { + return '

'+ key + '

' + '
'+ '' + '

' + frontTooltip + x + '

'+ '' + '

' + }); + + d3.select(domTarget) + .datum(shiftedData) + .transition().duration(1200) + .call(chart); + } else if(chartType == 'bar-chart'){ + var chart = nv.models.multiBarChart() + .x(function(d) { return d[0] }) + .y(function(d) { return d[1] }) + //.staggerLabels(true) //Too many bars and not enough room? Try staggering labels. + // .tooltips(true) + // .showValues(true) //...instead, show the bar value right on top of each bar. + .transitionDuration(350) + .stacked(false).showControls(false); + + chart.xAxis.tickFormat(function(d) { + return d3.time.format(xAxisFormatter)(new Date(d)); + }); + // Fixes x-axis time alignment. + + var xScale =d3.time.scale.utc(); + + chart.yAxis.scale(xScale) + .axisLabel(yAxisLabel) + .axisLabelDistance(25) + .tickFormat(d3.format(yAxisFormatter)); + // Fixes the axis-labels being rendered out of the SVG element. + chart.margin({right: 80}); + chart.tooltipContent(function(key, x, y) { + return '

' + frontTooltip + y + tooltipUnits + ' ' + key + '

' + '

' + x + '

'; + }); + + + d3.select(domTarget) + .datum(shiftedData) + .transition().duration(350) + .call(chart); + } else { + var chart = nv.models.lineChart() + .x(function(d) { return d[0] }) + .y(function(d) { return d[1] }) + .color(d3.scale.category10().range()) + .interpolate('monotone') + .showYAxis(true) + ; + chart.xAxis + .tickFormat(function(d) { + return d3.time.format(xAxisFormatter)(new Date(d)); + }); + // Fixes x-axis time alignment. + chart.xScale(d3.time.scale.utc()); + chart.yAxis + .axisLabel(yAxisLabel) + .axisLabelDistance(25) + .tickFormat(d3.format(yAxisFormatter)); + // Fixes the axis-labels being rendered out of the SVG element. + chart.margin({right: 80}); + chart.tooltipContent(function(key, x, y) { + return '

' + frontTooltip + y + tooltipUnits + ' ' + key + '

' + '

' + x + '

'; + }); + + d3.select(domTarget) + .datum(shiftedData) + .transition().duration(350) + .call(chart); + } + // Resize the chart on window resize. + nv.utils.windowResize(chart.update); + return chart; + }); +}; + + +var TimeseriesChart = React.createClass({ + + getDefaultProps: function() { + return { + chartID: 'some-chart-id', + chartHeight: 380, + data: {}, + xAxisFormatter: '%x', + yAxisFormatter: '', + yAxisLabel: 'the y axis!', + timezoneOffset: 0, + frontTooltip: '', + tooltipUnits: '', + chartType:'', + activeView:'' + } + }, + + chartIsFlat(results) { + return results.every(function(series) { + if(typeof(series['values']) === 'object'){ + return series['values'].every(function(pair) { + return pair[1] === 0; + }); + } else { + return series['values'] === 0; + } + }); + }, + + render: function() { + + var results = this.props.data['results']; + var isFlatChart = !results || this.chartIsFlat(results); + var className = ['time-series-chart-container']; + var flatLineOverlay = null; + if (isFlatChart) { + flatLineOverlay = ( +
+
+ No data available for this range. +
+
+ ); + className.push('flat'); + } + if(this.props.activeView == 'list') { + $('#'+this.props.chartID + "-download").hide(); + return ( +
+ {flatLineOverlay} + + + ); + } + else { + $('#'+this.props.chartID + "-download").show(); + return ( +
+ {flatLineOverlay} + +
+ ); + } + } +}); + + +var TimeSeriesChartElement = React.createClass({ + // When the request params have changed, get new data and rebuild the graph. + // We circumvent react's typical re-render cycle for this component by returning false. + shouldComponentUpdate: function(nextProps) { + this.props.activeView = nextProps.activeView; + var nextData = JSON.stringify(nextProps.data); + var prevData = JSON.stringify(this.props.data); + if (nextData !== prevData) { + updateChart( + '#' + this.props.chartID, + nextProps.data['results'], + nextProps.xAxisFormatter, + nextProps.yAxisFormatter, + nextProps.yAxisLabel, + this.props.timezoneOffset, + this.props.tooltipUnits, + this.props.frontTooltip, + this.props.chartType, + this.props.chartID + ); + } + return false; + }, + + render: function() { + var inlineStyles = { + height: this.props.chartHeight + }; + return ( + + + ); + } +}); + + +var RangeButton = React.createClass({ + propTypes: { + buttonText: React.PropTypes.string.isRequired + }, + + getDefaultProps: function() { + return { + buttonText: 'day', + activeButtonText: '', + onButtonClick: null, + } + }, + + render: function() { + // Determine whether this particular button is active by checking + // this button's text vs the owner's knowledge of the active button. + // Then change styles accordingly. + var inlineStyles = { + marginRight: 20 + }; + if (this.props.buttonText == this.props.activeButtonText) { + inlineStyles.cursor = 'inherit'; + inlineStyles.color = 'black'; + inlineStyles.textDecoration = 'none'; + } else { + inlineStyles.cursor = 'pointer'; + } + return ( + + {this.props.buttonText} + + ); + }, + + onThisClick: function(text) { + this.props.onButtonClick(text); + }, +}); + + +var LoadingText = React.createClass({ + getDefaultProps: function() { + return { + visible: false, + } + }, + + render: function() { + var inlineStyles = { + display: this.props.visible ? 'inline' : 'none', + marginRight: 20, + }; + + return ( + + (loading..) + + ); + }, +}); + + +var DatePicker = React.createClass({ + getDefaultProps: function() { + return { + label: 'date', + pickerID: 'some-datetimepicker-id', + epochTime: 0, + onDatePickerChange: null, + datePickerOptions : { + icons: { + time: 'fa fa-clock-o', + date: 'fa fa-calendar', + up: 'fa fa-arrow-up', + down: 'fa fa-arrow-down', + previous: 'fa fa-arrow-left', + next: 'fa fa-arrow-right', + today: 'fa fa-circle-o', + }, + showTodayButton: true, + format: 'YYYY-MM-DD [at] h:mmA', + }, + dateFormat: 'YYYY-MM-DD [at] h:mmA', + } + }, + + componentDidMount: function() { + var formattedDate = moment.unix(this.props.epochTime).format(this.props.dateFormat); + var domTarget = '#' + this.props.pickerID; + $(domTarget).keydown(function(e){ + e.preventDefault(); + return false; + }); + $(domTarget) + .datetimepicker(this.props.datePickerOptions) + .data('DateTimePicker') + .date(formattedDate); + var dateFormat = this.props.dateFormat; + var handler = this.props.onDatePickerChange; + $(domTarget).on('dp.change', function(event) { + var newEpochTime = moment(event.target.value, dateFormat).unix(); + handler(newEpochTime); + }); + }, + + shouldComponentUpdate: function(nextProps) { + var formattedDate = moment.unix(nextProps.epochTime).format(nextProps.dateFormat); + var domTarget = '#' + nextProps.pickerID; + $(domTarget).data('DateTimePicker').date(formattedDate); + return false + }, + + render: function() { + return ( + + + + + ); + }, +}); + +var DownloadButton = React.createClass({ + getDefaultProps: function() { + return { + visible: false, + startimeepoch:'', + endTimeEpoch:'', + defaultButtonText: 'week', + statsType:'', + onButtonClick: null, + activeView:'graph' + } + }, + componentWillMount() { + this.id = this.props.chartID + "-download"; + }, + componentDidMount: function() { + var domTargetId = this.props.chartID; + var btn = document.getElementById(this.id); + var svg = document.getElementById(domTargetId); + var canvas = document.querySelector('canvas'); + + btn.addEventListener('click', function () { + + var width = $("#"+domTargetId).width(); + var height = $("#"+domTargetId).height(); + + var canvas = document.getElementById('canvas'); + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = "#FFF"; + ctx.fillRect(0, 0, width, height); + + var data = (new XMLSerializer()).serializeToString(svg); + var DOMURL = window.URL || window.webkitURL; + + var img = new Image(); + var svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'}); + var url = DOMURL.createObjectURL(svgBlob); + + img.onload = function() { + ctx.drawImage(img, 0, 0); + DOMURL.revokeObjectURL(url); + + var imgURI = canvas + .toDataURL('image/png') + .replace('image/png', 'image/octet-stream'); + + var evt = new MouseEvent('click', { + view: window, + bubbles: false, + cancelable: true + }); + + var a = document.createElement('a'); + a.setAttribute('download', 'report.png'); + a.setAttribute('href', imgURI); + a.setAttribute('target', '_blank'); + a.dispatchEvent(evt); + }; + img.src = url; + //img.setAttribute("src", "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(data))) ); + }); + }, + render: function() { + return ( + + + +    + + + + + ); + }, + onThisClick: function(text) { + this.props.onButtonClick(); + } +}); + +var ViewButton = React.createClass({ + propTypes: { + buttonText: React.PropTypes.string.isRequired + }, + + getDefaultProps: function() { + return { + buttonText: 'graph', + activeView: '', + onButtonClick: null, + } + }, + + render: function() { + // Determine whether this particular button is active by checking + // this button's text vs the owner's knowledge of the active button. + // Then change styles accordingly. + var inlineStyles = { + marginRight: 20 + }; + if (this.props.buttonText == this.props.activeView) { + inlineStyles.cursor = 'inherit'; + inlineStyles.color = 'black'; + inlineStyles.textDecoration = 'none'; + } else { + inlineStyles.cursor = 'pointer'; + } + if(this.props.buttonText == 'graph') { + return ( + + + + ); + } else { + return ( + + + + ); + } + }, + + onThisClick: function(text) { + this.props.onButtonClick(text); + }, +}); + +var Table = React.createClass({ + shouldComponentUpdate: function(nextProps) { + this.props.activeView = nextProps.activeView; + + var nextData = JSON.stringify(nextProps.data); + var prevData = JSON.stringify(this.props.data); + if (nextData !== prevData) { + updateChart( + '#' + this.props.chartID, + nextProps.data['results'], + nextProps.xAxisFormatter, + nextProps.yAxisFormatter, + nextProps.yAxisLabel, + this.props.timezoneOffset, + this.props.tooltipUnits, + this.props.frontTooltip, + this.props.chartType, + this.props.chartID + ); + } + return false; + }, + + render: function() { + var inlineStyles = { + 'min-height': '360px', + 'margin-top': '20px' + }; + return ( +
+
+
+ ); + } +}); From a2eb5b56fe0c768834bc94bf9d6ad049960a015e Mon Sep 17 00:00:00 2001 From: Shiv K Sah Date: Thu, 3 Aug 2017 19:50:26 +0530 Subject: [PATCH 2/3] base files for all report --- .../js/dashboard/report-chart-components.js | 1704 +++++++++-------- cloud/endagaweb/stats_app/stats_client.py | 174 +- cloud/endagaweb/stats_app/views.py | 74 +- .../endagaweb/templates/dashboard/layout.html | 35 +- .../templates/dashboard/report/filter.html | 75 + .../templates/dashboard/report/header.html | 19 + .../report/health-filter-action.html | 32 + .../templates/dashboard/report/nav.html | 46 + cloud/endagaweb/urls.py | 17 +- cloud/endagaweb/views/__init__.py | 1 + cloud/endagaweb/views/reports.py | 372 ++++ 11 files changed, 1724 insertions(+), 825 deletions(-) create mode 100644 cloud/endagaweb/templates/dashboard/report/filter.html create mode 100644 cloud/endagaweb/templates/dashboard/report/header.html create mode 100644 cloud/endagaweb/templates/dashboard/report/health-filter-action.html create mode 100644 cloud/endagaweb/templates/dashboard/report/nav.html create mode 100644 cloud/endagaweb/views/reports.py diff --git a/cloud/endagaweb/static/js/dashboard/report-chart-components.js b/cloud/endagaweb/static/js/dashboard/report-chart-components.js index 390c6efc..c074f49f 100644 --- a/cloud/endagaweb/static/js/dashboard/report-chart-components.js +++ b/cloud/endagaweb/static/js/dashboard/report-chart-components.js @@ -11,297 +11,333 @@ var TimeseriesChartWithButtonsAndDatePickers = React.createClass({ - getInitialState: function() { - // We expect many of these values to be overridden before the chart is - // first rendered -- see componentDidMount. - return { - startTimeEpoch: 0, - endTimeEpoch: -1, - isLoading: true, - chartData: {}, - activeButtonText: '', - xAxisFormatter: '%x', - yAxisFormatter: '', - activeView:'' - } - }, - getDefaultProps: function() { - // Load the current time with the user's clock if nothing is specified. We - // also specify the user's computer's timezone offset and use that to - // adjust the graph data. - var currentTime = Math.round(new Date().getTime() / 1000); - return { - title: 'title (set me!)', - chartID: 'one', - buttons: ['hour', 'day', 'week', 'month', 'year'], - icons: ['graph', 'list'], - defaultView: 'graph', - defaultButtonText: 'week', - endpoint: '/api/v1/stats/', - statTypes: 'sms', - level: 'network', - levelID: 0, - aggregation: 'count', - yAxisLabel: 'an axis label (set me!)', - currentTimeEpoch: currentTime, - timezoneOffset: 0, - tooltipUnits: '', - frontTooltip: '', - chartType: 'line-chart', - reportView: 'list' - } - }, - - // On-mount, build the charts with the default data. - // Note that we have to load the current time here. - componentDidMount: function() { - this.setState({ - activeButtonText: this.props.defaultButtonText, - activeView: this.props.defaultView, - startTimeEpoch: this.props.currentTimeEpoch - secondsMap[this.props.defaultButtonText], - endTimeEpoch: this.props.currentTimeEpoch, - // When the request params in the state have been set, go get more data. - }, function() { - this.updateChartData(); - }); - }, - - componentDidUpdate(prevProps, prevState) { - // Update if we toggled a load - if (!prevState.isLoading && this.state.isLoading) { - this.updateChartData(); - } - }, - - // This handler takes the text of the date range buttons - // and ouputs figures out the corresponding number of seconds. - handleButtonClick: function(text) { - // Update only if the startTime has actually changed. - var newStartTimeEpoch = this.props.currentTimeEpoch - secondsMap[text]; - if (this.state.startTimeEpoch != newStartTimeEpoch) { - this.setState({ - startTimeEpoch: newStartTimeEpoch, - endTimeEpoch: this.props.currentTimeEpoch, - isLoading: true, - activeButtonText: text, - }); - } - }, - - // This handler takes the text of the view mode buttons - // and ouputs figures out the corresponding number of seconds. - handleViewClick: function(text) { - // Update only if the startTime has actually changed. - if (this.state.activeView != text) { - - this.setState({ - startTimeEpoch: this.state.startTimeEpoch, - endTimeEpoch: this.props.currentTimeEpoch, - isLoading: true, - activeView: text, - }); - var interval = this.props.defaultButtonText; - setTimeout(function(){ - //this.handleButtonClick(interval); - this.forceUpdate() - }.bind(this), 1000); - } - }, - - handleDownloadClick: function(text) { - var queryParams = { - 'start-time-epoch': this.state.startTimeEpoch, - 'end-time-epoch': this.state.endTimeEpoch, - 'stat-types': this.props.statTypes, - 'level':this.props.level, - 'level_id': this.props.levelID, - 'report-type':this.props.title, - - }; - $.get('/report/downloadcsv', queryParams, function(data,response) { - - var todayTime = new Date(); var month = (todayTime .getMonth() + 1); var day = (todayTime .getDate()); var year = (todayTime .getFullYear()); - var convertdate = year+ "-" + month + "-" +day; - this.setState({ - isLoading: false, - data: data, - title:this.props.title - }); - var filename = this.state.title - var csvData = new Blob([data], {type: 'text/csv;charset=utf-8;'}); - var csvURL = null; - if (navigator.msSaveBlob) { - csvURL = navigator.msSaveBlob(csvData, filename+"-"+convertdate+'.csv'); - } else { - csvURL = window.URL.createObjectURL(csvData); - } - var tempLink = document.createElement('a'); - document.body.appendChild(tempLink); - tempLink.href = csvURL; - tempLink.setAttribute('download', filename+"-"+convertdate+'.csv'); - tempLink.target="_self" - tempLink.click(); - }.bind(this)); - }, - - // Datepicker handlers, one each for changing the start and end times. - startTimeChange: function(newTime) { - if (newTime < this.state.endTimeEpoch && !this.state.isLoading) { - this.setState({ - startTimeEpoch: newTime, - isLoading: true, - }); - } - }, - endTimeChange: function(newTime) { - var now = moment().unix(); - if (newTime > this.state.startTimeEpoch && newTime < now - && !this.state.isLoading) { - this.setState({ - endTimeEpoch: newTime, - isLoading: true, - }); - } - }, - - // Gets new chart data from the backend. - // Recall that this must be called explicitly..it's a bit different - // than the normal react component connectivity. - // First figure out how to set the interval (or, more aptly, the bin width) - // and the x-axis formatter (see the d3 wiki on time formatting). - updateChartData: function() { - var interval, newXAxisFormatter, newYAxisFormatter; - var delta = this.state.endTimeEpoch - this.state.startTimeEpoch; - if (delta <= 60) { - interval = 'seconds'; - newXAxisFormatter = '%-H:%M:%S'; - } else if (delta <= (60 * 60)) { - interval = 'minutes'; - newXAxisFormatter = '%-H:%M'; - } else if (delta <= (24 * 60 * 60)) { - interval = 'hours'; - newXAxisFormatter = '%-H:%M'; - } else if (delta <= (7 * 24 * 60 * 60)) { - interval = 'hours'; - newXAxisFormatter = '%b %d, %-H:%M'; - } else if (delta <= (30 * 24 * 60 * 60)) { - interval = 'days'; - newXAxisFormatter = '%b %d'; - } else if (delta <= (365 * 24 * 60 * 60)) { - interval = 'days'; - newXAxisFormatter = '%b %d'; - } else { - interval = 'months'; - newXAxisFormatter = '%x'; - } - if (this.props.statTypes == 'total_data,uploaded_data,downloaded_data') { - newYAxisFormatter = '.1f'; - } else { - newYAxisFormatter = ''; - } - var queryParams = { - 'start-time-epoch': this.state.startTimeEpoch, - 'end-time-epoch': this.state.endTimeEpoch, - 'interval': interval, - 'stat-types': this.props.statTypes, - 'level-id': this.props.levelID, - 'aggregation': this.props.aggregation, - 'extras': this.props.extras, - 'dynamic-stat': this.props.dynamicStat, - 'topup-percent': this.props.topupPercent, - 'report-view': this.props.reportView - }; - var endpoint = this.props.endpoint + this.props.level; - $.get(endpoint, queryParams, function(data) { - this.setState({ - isLoading: false, - chartData: data, - xAxisFormatter: newXAxisFormatter, - yAxisFormatter: newYAxisFormatter, - }); - }.bind(this)); - }, - - render: function() { - var fromDatepickerID = 'from-datepicker-' + this.props.chartID; - var toDatepickerID = 'to-datepicker-' + this.props.chartID; - return ( -
-

{this.props.title}

- past    - {this.props.buttons.map(function(buttonText, index) { - return ( - - ); - }, this)} - - - - -     - {this.props.icons.map(function(buttonText, index) { - return ( - - ); - }, this)} - - - - - -
- ); - }, + getInitialState: function() { + // We expect many of these values to be overridden before the chart is + // first rendered -- see componentDidMount. + return { + startTimeEpoch: 0, + endTimeEpoch: -1, + isLoading: true, + chartData: {}, + activeButtonText: '', + xAxisFormatter: '%x', + yAxisFormatter: '', + activeView:'', + tablesColumnValueName:'' + } + }, + getDefaultProps: function() { + // Load the current time with the user's clock if nothing is specified. We + // also specify the user's computer's timezone offset and use that to + // adjust the graph data. + var currentTime = Math.round(new Date().getTime() / 1000); + return { + title: 'title (set me!)', + chartID: 'one', + buttons: ['hour', 'day', 'week', 'month', 'year'], + icons: ['graph', 'list'], + defaultView: 'graph', + defaultButtonText: 'week', + endpoint: '/api/v1/stats/', + statTypes: 'sms', + level: 'network', + levelID: 0, + aggregation: 'count', + yAxisLabel: 'an axis label (set me!)', + currentTimeEpoch: currentTime, + timezoneOffset: 0, + tooltipUnits: '', + frontTooltip: '', + chartType: 'line-chart', + reportView: 'list', + info: 'info of report type' + } + }, + + // On-mount, build the charts with the default data. + // Note that we have to load the current time here. + componentDidMount: function() { + this.setState({ + activeButtonText: this.props.defaultButtonText, + activeView: this.props.defaultView, + startTimeEpoch: this.props.currentTimeEpoch - secondsMap[this.props.defaultButtonText], + endTimeEpoch: this.props.currentTimeEpoch, + // When the request params in the state have been set, go get more data. + }, function() { + this.updateChartData(); + }); + }, + componentDidUpdate(prevProps, prevState) { + // Update if we toggled a load + if (!prevState.isLoading && this.state.isLoading) { + this.updateChartData(); + } + }, + + // This handler takes the text of the date range buttons + // and ouputs figures out the corresponding number of seconds. + handleButtonClick: function(text) { + // Update only if the startTime has actually changed. + var newStartTimeEpoch = this.props.currentTimeEpoch - secondsMap[text]; + if (this.state.startTimeEpoch != newStartTimeEpoch) { + this.setState({ + startTimeEpoch: newStartTimeEpoch, + endTimeEpoch: this.props.currentTimeEpoch, + isLoading: true, + activeButtonText: text, + }); + } + }, + + // This handler takes the text of the view mode buttons + handleViewClick: function(text) { + // Update only if the startTime has actually changed. + if (this.state.activeView != text) { + this.setState({ + startTimeEpoch: this.state.startTimeEpoch, + endTimeEpoch: this.props.currentTimeEpoch, + isLoading: true, + activeView: text, + }); + } + }, + + handleDownloadClick: function(text) { + var queryParams = { + 'start-time-epoch': this.state.startTimeEpoch, + 'end-time-epoch': this.state.endTimeEpoch, + 'stat-types': this.props.statTypes, + 'level':this.props.level, + 'level_id': this.props.levelID, + 'report-type':this.props.title, + }; + $.get('/report/downloadcsv', queryParams, function(data,response) { + var todayTime = new Date(); var month = (todayTime .getMonth() + 1); var day = (todayTime .getDate()); var year = (todayTime .getFullYear()); + var convertdate = year+ "-" + month + "-" +day; + this.setState({ + isLoading: false, + data: data, + title:this.props.title + }); + var filename = this.state.title + var csvData = new Blob([data], {type: 'text/csv;charset=utf-8;'}); + var csvURL = null; + if (navigator.msSaveBlob) { + csvURL = navigator.msSaveBlob(csvData, filename+"-"+convertdate+'.csv'); + } else { + csvURL = window.URL.createObjectURL(csvData); + } + var tempLink = document.createElement('a'); + document.body.appendChild(tempLink); + tempLink.href = csvURL; + tempLink.setAttribute('download', filename+"-"+convertdate+'.csv'); + tempLink.target="_self" + tempLink.click(); + }.bind(this)); + }, + + // Datepicker handlers, one each for changing the start and end times. + startTimeChange: function(newTime) { + if (newTime < this.state.endTimeEpoch && !this.state.isLoading) { + this.setState({ + startTimeEpoch: newTime, + isLoading: true, + }); + } + }, + endTimeChange: function(newTime) { + var now = moment().unix(); + if (newTime > this.state.startTimeEpoch && newTime < now && !this.state.isLoading) { + this.setState({ + endTimeEpoch: newTime, + isLoading: true, + }); + } + }, + + // Gets new chart data from the backend. + // Recall that this must be called explicitly..it's a bit different + // than the normal react component connectivity. + // First figure out how to set the interval (or, more aptly, the bin width) + // and the x-axis formatter (see the d3 wiki on time formatting). + updateChartData: function() { + var interval, newXAxisFormatter, newYAxisFormatter, tablesColumnValueName; + var delta = this.state.endTimeEpoch - this.state.startTimeEpoch; + if (delta <= 60) { + interval = 'seconds'; + newXAxisFormatter = '%-H:%M:%S'; + } else if (delta <= (60 * 60)) { + interval = 'minutes'; + newXAxisFormatter = '%-H:%M'; + } else if (delta <= (24 * 60 * 60)) { + interval = 'hours'; + newXAxisFormatter = '%-H:%M'; + } else if (delta <= (7 * 24 * 60 * 60)) { + interval = 'hours'; + newXAxisFormatter = '%b %d, %-H:%M'; + } else if (delta <= (30 * 24 * 60 * 60)) { + interval = 'days'; + newXAxisFormatter = '%b %d'; + } else if (delta <= (365 * 24 * 60 * 60)) { + interval = 'days'; + newXAxisFormatter = '%b %d'; + } else { + interval = 'months'; + newXAxisFormatter = '%x'; + } + if (this.props.statTypes == 'total_data,uploaded_data,downloaded_data') { + newYAxisFormatter = '.1f'; + } else { + newYAxisFormatter = ''; + } + if (this.props.chartID == 'data-chart') { + newYAxisFormatter = '.2f'; + tablesColumnValueName = [{ + title: "Type" + }, { + title: "Minutes" + }] + + } else if (this.props.chartID == 'call-chart' | this.props.chartID == 'sms-chart') { + tablesColumnValueName = [{ + title: "Type" + }, { + title: "Count" + }] + } else if (this.props.chartID == 'topupSubscriber-chart') { + newYAxisFormatter = '.2f'; + tablesColumnValueName = [{ + title: "Denomination Bracket" + }, { + title: "Amount in" + }] + + } else if (this.props.chartID == 'topup-chart') { + newYAxisFormatter = '.2f'; + tablesColumnValueName = [{ + title: "Denomination Bracket" + }, { + title: "Count" + }] + + } else if (this.props.chartID == 'load-transfer-chart') { + newYAxisFormatter = '.2f'; + } + else { + tablesColumnValueName = [{ + title: "Type" + }, { + title: "Count" + }] + } + var queryParams = { + 'start-time-epoch': this.state.startTimeEpoch, + 'end-time-epoch': this.state.endTimeEpoch, + 'interval': interval, + 'stat-types': this.props.statTypes, + 'level-id': this.props.levelID, + 'aggregation': this.props.aggregation, + 'extras': this.props.extras, + 'dynamic-stat': this.props.dynamicStat, + 'topup-percent': this.props.topupPercent, + 'report-view': this.props.reportView + }; + var endpoint = this.props.endpoint + this.props.level; + $.get(endpoint, queryParams, function(data) { + this.setState({ + isLoading: false, + chartData: data, + xAxisFormatter: newXAxisFormatter, + yAxisFormatter: newYAxisFormatter, + tablesColumnValueName: tablesColumnValueName, + }); + }.bind(this)); + }, + + render: function() { + var fromDatepickerID = 'from-datepicker-' + this.props.chartID; + var toDatepickerID = 'to-datepicker-' + this.props.chartID; + return ( +
+

+ {this.props.title} +   +

+ past    + {this.props.buttons.map(function(buttonText, index) { + return ( + + ); + }, this)} + + + + +     + {this.props.icons.map(function(buttonText, index) { + return ( + + ); + }, this)} + + + + + +
+ ); + }, }); var secondsMap = { - 'hour': 60 * 60, - 'day': 24 * 60 * 60, - 'week': 7 * 24 * 60 * 60, - 'month': 30 * 24 * 60 * 60, - 'year': 365 * 24 * 60 * 60, + 'hour': 60 * 60, + 'day': 24 * 60 * 60, + 'week': 7 * 24 * 60 * 60, + 'month': 30 * 24 * 60 * 60, + 'year': 365 * 24 * 60 * 60, }; var add = function (a, b){ @@ -310,540 +346,634 @@ var add = function (a, b){ // Builds the target chart from scratch. NVD3 surprisingly handles this well. // domTarget is the SVG element's parent and data is the info that will be graphed. -var updateChart = function(domTarget, data, xAxisFormatter, yAxisFormatter, yAxisLabel, timezoneOffset, tooltipUnits, frontTooltip, chartType, domTargetId) { - // We pass in the timezone offset and calculate a locale offset. The former - // is based on the UserProfile's specified timezone and the latter is the user's - // computer's timezone offset. We manually shift the data to work around - // d3's conversion into the user's computer's timezone. Note that for my laptop - // in PST, the locale offset is positive (7hrs) while the UserProfile offset - // is negative (-7hrs). - var localeOffset = 60 * (new Date()).getTimezoneOffset(); - var shiftedData = []; - var changeAmount = []; - var tableData = []; - - for (var index in data) { - var newSeries = { 'key': data[index]['key'] }; - var newValues = []; - if( typeof(data[index]['values']) === 'object'){ - for (var series_index in data[index]['values']) { - var newValue = [ - // Shift out of the locale offset to 'convert' to UTC and then shift - // back into the operator's tz by adding the tz offset from the server. - data[index]['values'][series_index][0] + 1e3 * localeOffset + 1e3 * timezoneOffset, - data[index]['values'][series_index][1] - ]; - newValues.push(newValue); - changeAmount.push(newValue[1]) - } - // Get sum of the total charges - var sumAmount = changeAmount.reduce(add, 0); - changeAmount = [] - // sum can be of all negative values - if ( sumAmount < 0 ){ - newSeries['total'] = (sumAmount * -1); - } - else{ - newSeries['total'] = (sumAmount); - } - newSeries['values'] = newValues; - } else { - newSeries['total'] = data[index]['values']; +var updateChart = function(domTarget, data, xAxisFormatter, yAxisFormatter, yAxisLabel, timezoneOffset, tooltipUnits, frontTooltip, chartType, domTargetId, tablesColumnValueName) { + // We pass in the timezone offset and calculate a locale offset. The former + // is based on the UserProfile's specified timezone and the latter is the user's + // computer's timezone offset. We manually shift the data to work around + // d3's conversion into the user's computer's timezone. Note that for my laptop + // in PST, the locale offset is positive (7hrs) while the UserProfile offset + // is negative (-7hrs). + var localeOffset = 60 * (new Date()).getTimezoneOffset(); + var shiftedData = []; + var changeAmount = []; + var tableData = []; + + + for (var index in data) { + var newSeries = { + 'key': data[index]['key'] + }; + var retailer = data[index]['retailer_table_data'] + + var newValues = []; + if (typeof(data[index]['values']) === 'object') { + for (var series_index in data[index]['values']) { + + var newValue = [ + moment(data[index]['values'][series_index][0] ), + data[index]['values'][series_index][1] + ]; + newValues.push(newValue); + changeAmount.push(newValue[1]) + } + // Get sum of the total charges + var sumAmount = changeAmount.reduce(add, 0); + changeAmount = [] + // sum can be of all negative values + if (sumAmount < 0) { + newSeries['total'] = (sumAmount * -1); + } else { + newSeries['total'] = (sumAmount); + } + newSeries['values'] = newValues; + } else { + newSeries['total'] = data[index]['values']; + } + // Get sum of the total charges + + + if ( domTargetId == 'call-sms-chart' || domTargetId == 'sms-chart' || domTargetId == 'call-chart' ) { + if (newSeries['total'] != undefined) { + newSeries['total'] = newSeries['total'] + tableData.push([newSeries['key'], newSeries['total']]); + }} + else if (domTargetId == 'data-chart' || domTargetId == 'topupSubscriber-chart' || domTargetId == 'call-billing-chart' || domTargetId == 'sms-billing-chart' || domTargetId == 'call-sms-billing-chart'){ + + if (newSeries['total'] != undefined) { + newSeries['total'] = newSeries['total'].toFixed(2); + tableData.push([newSeries['key'], newSeries['total']]); + } + } + else if (domTargetId == 'load-transfer-chart' || domTargetId == 'add-money-chart') { + if (typeof(retailer !== 'undefined') && Object.keys(retailer).length >= 1) { + if (Object.keys(retailer).length >= 1) { + for (var series_index in data[index]['retailer_table_data']) { + var elements = data[index]['retailer_table_data'][series_index]; + tableData.push([series_index, elements]) + } + } + } else { + tableData.push(['No data', 'No data']) + } + } else { + newSeries['total'] = newSeries['total'] + tableData.push([newSeries['key'], newSeries['total']]); + } + + shiftedData.push(newSeries); + if (frontTooltip != "" && domTargetId == 'topupSubscriber-chart') { + tablesColumnValueName = [{ + title: "Denomination Bracket" + }, { + title: "Amount in (" + frontTooltip + ")" + }] + } else if (domTargetId == 'call-billing-chart' || domTargetId == 'sms-billing-chart' || domTargetId == 'call-sms-billing-chart') { + tablesColumnValueName = [{ + title: "Type" + }, { + title: "Amount in (" + frontTooltip + ")" + }] + } + else if (frontTooltip != "" && domTargetId == 'add-money-chart' || domTargetId == 'load-transfer-chart') { + tablesColumnValueName = [{ + title: "IMSI" + }, { + title: "Amount in (" + frontTooltip + ")" + }] + } else { + tablesColumnValueName = tablesColumnValueName + } } - tableData.push([newSeries['key'], newSeries['total']]); - shiftedData.push(newSeries); - } - - $('.'+domTargetId).DataTable({ - data: tableData, - paging: false, - ordering: false, - info: false, - searching: false, - autoWidth: true, - scrollY: 320, - destroy: true, - columns: [ - { title: "Title" }, - { title: "Value" } - ] - }); - - - nv.addGraph(function() { - if(chartType == 'pie-chart') { - var chart = nv.models.pieChart() - .x(function(d) { return d.key.replace('_'," "); }) - .y(function(d) { return d.total; }) - .showLabels(true) - .labelType("percent"); - - chart.tooltipContent(function(key, x, y) { - return '

'+ key + '

' + '
'+ '' + '

' + frontTooltip + x + '

'+ '' + '

' - }); - - d3.select(domTarget) - .datum(shiftedData) - .transition().duration(1200) - .call(chart); - } else if(chartType == 'bar-chart'){ - var chart = nv.models.multiBarChart() - .x(function(d) { return d[0] }) - .y(function(d) { return d[1] }) - //.staggerLabels(true) //Too many bars and not enough room? Try staggering labels. - // .tooltips(true) - // .showValues(true) //...instead, show the bar value right on top of each bar. - .transitionDuration(350) - .stacked(false).showControls(false); - - chart.xAxis.tickFormat(function(d) { - return d3.time.format(xAxisFormatter)(new Date(d)); - }); - // Fixes x-axis time alignment. - - var xScale =d3.time.scale.utc(); - - chart.yAxis.scale(xScale) - .axisLabel(yAxisLabel) - .axisLabelDistance(25) - .tickFormat(d3.format(yAxisFormatter)); - // Fixes the axis-labels being rendered out of the SVG element. - chart.margin({right: 80}); - chart.tooltipContent(function(key, x, y) { - return '

' + frontTooltip + y + tooltipUnits + ' ' + key + '

' + '

' + x + '

'; - }); - - d3.select(domTarget) - .datum(shiftedData) - .transition().duration(350) - .call(chart); - } else { - var chart = nv.models.lineChart() - .x(function(d) { return d[0] }) - .y(function(d) { return d[1] }) - .color(d3.scale.category10().range()) - .interpolate('monotone') - .showYAxis(true) - ; - chart.xAxis - .tickFormat(function(d) { - return d3.time.format(xAxisFormatter)(new Date(d)); - }); - // Fixes x-axis time alignment. - chart.xScale(d3.time.scale.utc()); - chart.yAxis - .axisLabel(yAxisLabel) - .axisLabelDistance(25) - .tickFormat(d3.format(yAxisFormatter)); - // Fixes the axis-labels being rendered out of the SVG element. - chart.margin({right: 80}); - chart.tooltipContent(function(key, x, y) { - return '

' + frontTooltip + y + tooltipUnits + ' ' + key + '

' + '

' + x + '

'; - }); + $('.' + domTargetId).DataTable({ + data: tableData, + paging: false, + ordering: false, + info: false, + searching: false, + autoWidth: true, + scrollY: 320, + destroy: true, + columns: tablesColumnValueName + }); - d3.select(domTarget) - .datum(shiftedData) - .transition().duration(350) - .call(chart); - } - // Resize the chart on window resize. - nv.utils.windowResize(chart.update); - return chart; - }); + nv.addGraph(function() { + if (chartType == 'pie-chart') { + var chart = nv.models.pieChart() + .x(function(d) { + return d.key.replace(/[_]/g, ' '); + }) + .y(function(d) { + return d.total; + }) + .showLabels(true) + .labelType("percent"); + + chart.tooltipContent(function(key, x, y) { + chart.valueFormat(d3.format(yAxisFormatter)); + return '

' + key + '

' + '
' + '' + '

' + frontTooltip + x + '

' + '' + '

' + }); + + d3.select(domTarget) + .datum(shiftedData) + .transition().duration(350) + .call(chart); + } else if (chartType == 'bar-chart') { + + var chart = nv.models.multiBarChart() + .x(function(d) { + return d[0] + }) + .y(function(d) { + return d[1] + }) + .color(d3.scale.category10().range()) + .options({ + interpolate: 'monotone', + grid:true //<== this line here + }) + .showYAxis(true) + .stacked(false).showControls(false) + .reduceXTicks(true) + ; + chart.xAxis + .scale( d3.time.scale.utc()) + .showMaxMin(true); + chart.xAxis + .tickFormat(function(d){return d3.time.format(xAxisFormatter)(new Date(d));}) + chart.yAxis + .axisLabel(yAxisLabel) + .axisLabelDistance(25) + .tickFormat(d3.format(yAxisFormatter)); + // Fixes the axis-labels being rendered out of the SVG element. + chart.margin({ + right: 90 + }); + chart.tooltipContent(function(key, x, y) { + if (key == 'add_money') { + return '

' + frontTooltip + y + tooltipUnits + ' ' + '

' + '

' + x + '

'; + } else { + return '

' + frontTooltip + y + tooltipUnits + ' ' + key + '

' + '

' + x + '

'; + } + }); + d3.select(domTarget) + .datum(shiftedData) + .transition().duration(350) + .call(chart); + + } else { + var chart = nv.models.lineChart() + .x(function(d) { + return d[0] + }) + .y(function(d) { + if (d[1] > 0){ + return 1 + } else { + return 0 + } + + }) + .color(d3.scale.category10().range()) + .interpolate('monotone') + .showYAxis(true) + ; + chart.xAxis + .tickFormat(function(d) { + return d3.time.format(xAxisFormatter)(new Date(d)); + }); + // Fixes x-axis time alignment. + chart.xScale(d3.time.scale.utc()); + // For Health Graph + if(domTarget == '#call-chart') { + chart.yAxis + .axisLabel(yAxisLabel) + .axisLabelDistance(25) + .tickFormat(function(d) { + if (d == 1){ + return "UP"; + } else if (d > 0) { + return ; + } else { + return "DOWN"; + } + }); + } else { + chart.yAxis + .axisLabel(yAxisLabel) + .axisLabelDistance(25) + .tickFormat(d3.format(yAxisFormatter)); + } + + // Fixes the axis-labels being rendered out of the SVG element. + chart.margin({ + right: 80 + }); + chart.tooltipContent(function(key, x, y) { + if (key == 'health_state') { + key = '' + if (y =='UP'){ + y = 'UP' + } else { + y = 'DOWN' + } + } + return '

' + frontTooltip + y + tooltipUnits + ' ' + key + '

' + '

' + x + '

'; + }); + + d3.selectAll(".nv-axis path").style({ 'fill':'none', 'stroke': '#000' }); + d3.selectAll(".nv-chart path").style({ 'fill':'none'}); + d3.selectAll(".nv-line").style({ 'fill':'none'}); + + d3.select(domTarget) + .datum(shiftedData) + .transition().duration(350) + .call(chart); + } + // Resize the chart on window resize. + nv.utils.windowResize(chart.update); + return chart; + }); }; var TimeseriesChart = React.createClass({ - getDefaultProps: function() { - return { - chartID: 'some-chart-id', - chartHeight: 380, - data: {}, - xAxisFormatter: '%x', - yAxisFormatter: '', - yAxisLabel: 'the y axis!', - timezoneOffset: 0, - frontTooltip: '', - tooltipUnits: '', - chartType:'', - activeView:'' - } - }, - - chartIsFlat(results) { - return results.every(function(series) { - if(typeof(series['values']) === 'object'){ - return series['values'].every(function(pair) { - return pair[1] === 0; + getDefaultProps: function() { + return { + chartID: 'some-chart-id', + chartHeight: 380, + data: {}, + xAxisFormatter: '%x', + yAxisFormatter: '', + yAxisLabel: 'the y axis!', + timezoneOffset: 0, + frontTooltip: '', + tooltipUnits: '', + chartType:'', + activeView:'', + tablesColumnValueName:'' + } + }, + + chartIsFlat(results) { + return results.every(function(series) { + if(typeof(series['values']) === 'object'){ + return series['values'].every(function(pair) { + return pair[1] === 0; + }); + } else { + return series['values'] === 0; + } }); - } else { - return series['values'] === 0; - } - }); - }, - - render: function() { - - var results = this.props.data['results']; - var isFlatChart = !results || this.chartIsFlat(results); - var className = ['time-series-chart-container']; - var flatLineOverlay = null; - if (isFlatChart) { - flatLineOverlay = ( -
-
- No data available for this range. -
-
- ); - className.push('flat'); - } - if(this.props.activeView == 'list') { - $('#'+this.props.chartID + "-download").hide(); - return ( -
- {flatLineOverlay} - - - ); - } - else { - $('#'+this.props.chartID + "-download").show(); - return ( -
- {flatLineOverlay} - -
- ); + }, + + render: function() { + var results = this.props.data['results']; + var isFlatChart = !results || this.chartIsFlat(results); + var className = ['time-series-chart-container']; + var flatLineOverlay = null; + if (isFlatChart) { + flatLineOverlay = ( +
+
+ No data available for this range. +
+
+ ); + className.push('flat'); + } + if(this.props.activeView == 'list') { + $('#'+this.props.chartID + "-download").hide(); + return ( +
+ {flatLineOverlay} +
+ + ); + } else { + $('#'+this.props.chartID + "-download").show(); + return ( +
+ {flatLineOverlay} + +
+ ); + } } - } }); var TimeSeriesChartElement = React.createClass({ - // When the request params have changed, get new data and rebuild the graph. - // We circumvent react's typical re-render cycle for this component by returning false. - shouldComponentUpdate: function(nextProps) { - this.props.activeView = nextProps.activeView; - var nextData = JSON.stringify(nextProps.data); - var prevData = JSON.stringify(this.props.data); - if (nextData !== prevData) { - updateChart( - '#' + this.props.chartID, - nextProps.data['results'], - nextProps.xAxisFormatter, - nextProps.yAxisFormatter, - nextProps.yAxisLabel, - this.props.timezoneOffset, - this.props.tooltipUnits, - this.props.frontTooltip, - this.props.chartType, - this.props.chartID - ); + // When the request params have changed, get new data and rebuild the graph. + // We circumvent react's typical re-render cycle for this component by returning false. + shouldComponentUpdate: function(nextProps) { + this.props.activeView = nextProps.activeView; + var nextData = JSON.stringify(nextProps.data); + var prevData = JSON.stringify(this.props.data); + this.forceUpdate(); + if (nextData !== prevData) { + updateChart( + '#' + this.props.chartID, + nextProps.data['results'], + nextProps.xAxisFormatter, + nextProps.yAxisFormatter, + nextProps.yAxisLabel, + this.props.timezoneOffset, + this.props.tooltipUnits, + this.props.frontTooltip, + this.props.chartType, + this.props.chartID, + nextProps.tablesColumnValueName + ); + } + return false; + }, + componentDidUpdate(prevProps, prevState) { + var nextProps = prevProps; + // Update if we toggled a load + updateChart( + '#' + this.props.chartID, + nextProps.data['results'], + nextProps.xAxisFormatter, + nextProps.yAxisFormatter, + nextProps.yAxisLabel, + this.props.timezoneOffset, + this.props.tooltipUnits, + this.props.frontTooltip, + this.props.chartType, + this.props.chartID, + nextProps.tablesColumnValueName + ); + }, + render: function() { + var inlineStyles = { + height: this.props.chartHeight + }; + return ( + + + ); } - return false; - }, - - render: function() { - var inlineStyles = { - height: this.props.chartHeight - }; - return ( - - - ); - } }); - var RangeButton = React.createClass({ - propTypes: { - buttonText: React.PropTypes.string.isRequired - }, - - getDefaultProps: function() { - return { - buttonText: 'day', - activeButtonText: '', - onButtonClick: null, - } - }, - - render: function() { - // Determine whether this particular button is active by checking - // this button's text vs the owner's knowledge of the active button. - // Then change styles accordingly. - var inlineStyles = { - marginRight: 20 - }; - if (this.props.buttonText == this.props.activeButtonText) { - inlineStyles.cursor = 'inherit'; - inlineStyles.color = 'black'; - inlineStyles.textDecoration = 'none'; - } else { - inlineStyles.cursor = 'pointer'; - } - return ( - - {this.props.buttonText} - - ); - }, - - onThisClick: function(text) { - this.props.onButtonClick(text); - }, + propTypes: { + buttonText: React.PropTypes.string.isRequired + }, + + getDefaultProps: function() { + return { + buttonText: 'day', + activeButtonText: '', + onButtonClick: null, + } + }, + + render: function() { + // Determine whether this particular button is active by checking + // this button's text vs the owner's knowledge of the active button. + // Then change styles accordingly. + var inlineStyles = { + marginRight: 20 + }; + if (this.props.buttonText == this.props.activeButtonText) { + inlineStyles.cursor = 'inherit'; + inlineStyles.color = 'black'; + inlineStyles.textDecoration = 'none'; + } else { + inlineStyles.cursor = 'pointer'; + } + return ( + + {this.props.buttonText} + + ); + }, + onThisClick: function(text) { + this.props.onButtonClick(text); + }, }); - var LoadingText = React.createClass({ - getDefaultProps: function() { - return { - visible: false, - } - }, - - render: function() { - var inlineStyles = { - display: this.props.visible ? 'inline' : 'none', - marginRight: 20, - }; - - return ( - - (loading..) - - ); - }, + getDefaultProps: function() { + return { + visible: false, + } + }, + render: function() { + var inlineStyles = { + display: this.props.visible ? 'inline' : 'none', marginRight: 20 + }; + return ( + + (loading..) + + ); + }, }); - var DatePicker = React.createClass({ - getDefaultProps: function() { - return { - label: 'date', - pickerID: 'some-datetimepicker-id', - epochTime: 0, - onDatePickerChange: null, - datePickerOptions : { - icons: { - time: 'fa fa-clock-o', - date: 'fa fa-calendar', - up: 'fa fa-arrow-up', - down: 'fa fa-arrow-down', - previous: 'fa fa-arrow-left', - next: 'fa fa-arrow-right', - today: 'fa fa-circle-o', - }, - showTodayButton: true, - format: 'YYYY-MM-DD [at] h:mmA', - }, - dateFormat: 'YYYY-MM-DD [at] h:mmA', - } - }, - - componentDidMount: function() { - var formattedDate = moment.unix(this.props.epochTime).format(this.props.dateFormat); - var domTarget = '#' + this.props.pickerID; - $(domTarget).keydown(function(e){ - e.preventDefault(); - return false; - }); - $(domTarget) - .datetimepicker(this.props.datePickerOptions) - .data('DateTimePicker') - .date(formattedDate); - var dateFormat = this.props.dateFormat; - var handler = this.props.onDatePickerChange; - $(domTarget).on('dp.change', function(event) { - var newEpochTime = moment(event.target.value, dateFormat).unix(); - handler(newEpochTime); - }); - }, - - shouldComponentUpdate: function(nextProps) { - var formattedDate = moment.unix(nextProps.epochTime).format(nextProps.dateFormat); - var domTarget = '#' + nextProps.pickerID; - $(domTarget).data('DateTimePicker').date(formattedDate); - return false - }, - - render: function() { - return ( - - - - - ); - }, + getDefaultProps: function() { + return { + label: 'date', + pickerID: 'some-datetimepicker-id', + epochTime: 0, + onDatePickerChange: null, + datePickerOptions: { + icons: { + time: 'fa fa-clock-o', + date: 'fa fa-calendar', + up: 'fa fa-arrow-up', + down: 'fa fa-arrow-down', + previous: 'fa fa-arrow-left', + next: 'fa fa-arrow-right', + today: 'fa fa-circle-o', + }, + showTodayButton: true, + format: 'YYYY-MM-DD [at] h:mmA', + }, + dateFormat: 'YYYY-MM-DD [at] h:mmA', + } + }, + + componentDidMount: function() { + var formattedDate = moment.unix(this.props.epochTime).format(this.props.dateFormat); + var domTarget = '#' + this.props.pickerID; + $(domTarget).keydown(function(e) { + e.preventDefault(); + return false; + }); + $(domTarget) + .datetimepicker(this.props.datePickerOptions) + .data('DateTimePicker') + .date(formattedDate); + var dateFormat = this.props.dateFormat; + var handler = this.props.onDatePickerChange; + $(domTarget).on('dp.change', function(event) { + var newEpochTime = moment(event.target.value, dateFormat).unix(); + handler(newEpochTime); + }); + }, + + shouldComponentUpdate: function(nextProps) { + var formattedDate = moment.unix(nextProps.epochTime).format(nextProps.dateFormat); + var domTarget = '#' + nextProps.pickerID; + $(domTarget).data('DateTimePicker').date(formattedDate); + return false + }, + + render: function() { + return ( + + + + + ); + }, }); var DownloadButton = React.createClass({ - getDefaultProps: function() { - return { - visible: false, - startimeepoch:'', - endTimeEpoch:'', - defaultButtonText: 'week', - statsType:'', - onButtonClick: null, - activeView:'graph' + getDefaultProps: function() { + return { + visible: false, + startimeepoch:'', + endTimeEpoch:'', + defaultButtonText: 'week', + statsType:'', + onButtonClick: null, + activeView:'graph' + } + }, + componentWillMount() { + this.id = this.props.chartID + "-download"; + }, + componentDidMount: function() { + var domTargetId = this.props.chartID; + var btn = document.getElementById(this.id); + var filename = this.props.reporttype+".png"; + + btn.addEventListener('click', function () { + saveSvgAsPng(document.getElementById(domTargetId), filename, {backgroundColor:"#FFF"}); + }); + }, + render: function() { + return ( + + + +    + + + + + ); + }, + onThisClick: function(text) { + this.props.onButtonClick(); } - }, - componentWillMount() { - this.id = this.props.chartID + "-download"; - }, - componentDidMount: function() { - var domTargetId = this.props.chartID; - var btn = document.getElementById(this.id); - var svg = document.getElementById(domTargetId); - var canvas = document.querySelector('canvas'); - - btn.addEventListener('click', function () { - - var width = $("#"+domTargetId).width(); - var height = $("#"+domTargetId).height(); - - var canvas = document.getElementById('canvas'); - canvas.width = width; - canvas.height = height; - var ctx = canvas.getContext('2d'); - - ctx.fillStyle = "#FFF"; - ctx.fillRect(0, 0, width, height); - - var data = (new XMLSerializer()).serializeToString(svg); - var DOMURL = window.URL || window.webkitURL; - - var img = new Image(); - var svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'}); - var url = DOMURL.createObjectURL(svgBlob); - - img.onload = function() { - ctx.drawImage(img, 0, 0); - DOMURL.revokeObjectURL(url); - - var imgURI = canvas - .toDataURL('image/png') - .replace('image/png', 'image/octet-stream'); - - var evt = new MouseEvent('click', { - view: window, - bubbles: false, - cancelable: true - }); - - var a = document.createElement('a'); - a.setAttribute('download', 'report.png'); - a.setAttribute('href', imgURI); - a.setAttribute('target', '_blank'); - a.dispatchEvent(evt); - }; - img.src = url; - //img.setAttribute("src", "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(data))) ); - }); - }, - render: function() { - return ( - - - -    - - - - - ); - }, - onThisClick: function(text) { - this.props.onButtonClick(); - } }); var ViewButton = React.createClass({ - propTypes: { - buttonText: React.PropTypes.string.isRequired - }, - - getDefaultProps: function() { - return { - buttonText: 'graph', - activeView: '', - onButtonClick: null, - } - }, - - render: function() { - // Determine whether this particular button is active by checking - // this button's text vs the owner's knowledge of the active button. - // Then change styles accordingly. - var inlineStyles = { - marginRight: 20 - }; - if (this.props.buttonText == this.props.activeView) { - inlineStyles.cursor = 'inherit'; - inlineStyles.color = 'black'; - inlineStyles.textDecoration = 'none'; - } else { - inlineStyles.cursor = 'pointer'; - } - if(this.props.buttonText == 'graph') { - return ( - - - - ); - } else { - return ( - - - - ); - } - }, - - onThisClick: function(text) { - this.props.onButtonClick(text); - }, + propTypes: { + buttonText: React.PropTypes.string.isRequired + }, + + getDefaultProps: function() { + return { + buttonText: 'graph', + activeView: '', + onButtonClick: null, + } + }, + + render: function() { + // Determine whether this particular button is active by checking + // this button's text vs the owner's knowledge of the active button. + // Then change styles accordingly. + var inlineStyles = { + marginRight: 20 + }; + if (this.props.buttonText == this.props.activeView) { + inlineStyles.cursor = 'inherit'; + inlineStyles.color = 'black'; + inlineStyles.textDecoration = 'none'; + } else { + inlineStyles.cursor = 'pointer'; + } + if(this.props.buttonText == 'graph') { + return ( + + + + ); + } else { + return ( + + + + ); + } + }, + + onThisClick: function(text) { + this.props.onButtonClick(text); + }, }); var Table = React.createClass({ - shouldComponentUpdate: function(nextProps) { - this.props.activeView = nextProps.activeView; - - var nextData = JSON.stringify(nextProps.data); - var prevData = JSON.stringify(this.props.data); - if (nextData !== prevData) { - updateChart( - '#' + this.props.chartID, - nextProps.data['results'], - nextProps.xAxisFormatter, - nextProps.yAxisFormatter, - nextProps.yAxisLabel, - this.props.timezoneOffset, - this.props.tooltipUnits, - this.props.frontTooltip, - this.props.chartType, - this.props.chartID - ); + shouldComponentUpdate: function(nextProps) { + this.props.activeView = nextProps.activeView; + + var nextData = JSON.stringify(nextProps.data); + var prevData = JSON.stringify(this.props.data); + this.forceUpdate(); + if (nextData !== prevData) { + updateChart( + '#' + this.props.chartID, + nextProps.data['results'], + nextProps.xAxisFormatter, + nextProps.yAxisFormatter, + nextProps.yAxisLabel, + this.props.timezoneOffset, + this.props.tooltipUnits, + this.props.frontTooltip, + this.props.chartType, + this.props.chartID, + nextProps.tablesColumnValueName + ); + } + return false; + }, + componentDidUpdate(prevProps, prevState) { + var nextProps = prevProps; + // Update if we toggled a load + updateChart( + '#' + this.props.chartID, + nextProps.data['results'], + nextProps.xAxisFormatter, + nextProps.yAxisFormatter, + nextProps.yAxisLabel, + this.props.timezoneOffset, + this.props.tooltipUnits, + this.props.frontTooltip, + this.props.chartType, + this.props.chartID, + nextProps.tablesColumnValueName + ); + }, + + render: function() { + var inlineStyles = { + 'min-height': '360px', + 'margin-top': '20px' + }; + return ( +
+
+
+ ); } - return false; - }, - - render: function() { - var inlineStyles = { - 'min-height': '360px', - 'margin-top': '20px' - }; - return ( -
-
-
- ); - } }); diff --git a/cloud/endagaweb/stats_app/stats_client.py b/cloud/endagaweb/stats_app/stats_client.py index 5ebe043f..f88f2040 100644 --- a/cloud/endagaweb/stats_app/stats_client.py +++ b/cloud/endagaweb/stats_app/stats_client.py @@ -8,16 +8,19 @@ of patent rights can be found in the PATENTS file in the same directory. """ -from datetime import datetime import time +from datetime import datetime, timedelta +from operator import itemgetter +import calendar -from django.db.models import aggregates -from django.db.models import Q import pytz import qsstats - +from dateutil.rrule import rrule, MONTHLY +from django.db.models import Q +from django.db.models import aggregates from endagaweb import models - +from decimal import * +from pytz import timezone CALL_KINDS = [ 'local_call', 'local_recv_call', 'outside_call', 'incoming_call', @@ -25,9 +28,21 @@ SMS_KINDS = [ 'local_sms', 'local_recv_sms', 'outside_sms', 'incoming_sms', 'free_sms', 'error_sms'] -USAGE_EVENT_KINDS = CALL_KINDS + SMS_KINDS + ['gprs'] +SUBSCRIBER_KINDS = ['Provisioned', 'deactivate_number'] +ZERO_BALANCE_SUBSCRIBER = ['zero_balance_subscriber'] +INACTIVE_SUBSCRIBER = ['expired', 'first_expired', 'blocked'] +BTS_STATUS = ['health_state'] +TRANSFER_KINDS = ['transfer', 'add_money'] +WATERFALL_KINDS = ['loader', 'reload_rate', 'reload_amount', + 'reload_transaction', 'average_load', 'average_frequency'] +DENOMINATION_KINDS = ['start_amount', 'end_amount'] +BTS_KINDS = ['bts up', 'bts down'] +USAGE_EVENT_KINDS = CALL_KINDS + SMS_KINDS + ['gprs'] + SUBSCRIBER_KINDS + \ + TRANSFER_KINDS + WATERFALL_KINDS TIMESERIES_STAT_KEYS = [ - 'ccch_sdcch4_load', 'tch_f_max', 'tch_f_load', 'sdcch8_max', 'tch_f_pdch_load', 'tch_f_pdch_max', 'tch_h_load', 'tch_h_max', 'sdcch8_load', 'ccch_sdcch4_max', + 'ccch_sdcch4_load', 'tch_f_max', 'tch_f_load', 'sdcch8_max', + 'tch_f_pdch_load', 'tch_f_pdch_max', 'tch_h_load', 'tch_h_max', + 'sdcch8_load', 'ccch_sdcch4_max', 'sdcch_load', 'sdcch_available', 'tchf_load', 'tchf_available', 'pch_active', 'pch_total', 'agch_active', 'agch_pending', 'gprs_current_pdchs', 'gprs_utilization_percentage', 'noise_rssi_db', @@ -99,6 +114,9 @@ def aggregate_timeseries(self, param, **kwargs): end_time_epoch = kwargs.pop('end_time_epoch', -1) interval = kwargs.pop('interval', 'months') aggregation = kwargs.pop('aggregation', 'count') + report_view = kwargs.pop('report_view', 'list') + imsi_dict = {} + imsi_list = [] # Turn the start and end epoch timestamps into datetimes. start = datetime.fromtimestamp(start_time_epoch, pytz.utc) if end_time_epoch != -1: @@ -107,24 +125,44 @@ def aggregate_timeseries(self, param, **kwargs): end = datetime.fromtimestamp(time.time(), pytz.utc) # Build the queryset -- first determine if we're dealing with # UsageEvents or TimeseriesStats. - filters = None if param in USAGE_EVENT_KINDS: objects = models.UsageEvent.objects filters = Q(kind=param) + elif param in ZERO_BALANCE_SUBSCRIBER: + objects = models.UsageEvent.objects + filters = Q(oldamt__gt=0, newamt__lte=0) + elif param in INACTIVE_SUBSCRIBER: + aggregation = 'valid_through' + objects = models.Subscriber.objects + filters = Q(state=param) elif param in TIMESERIES_STAT_KEYS: objects = models.TimeseriesStat.objects filters = Q(key=param) + elif param in BTS_KINDS: + objects = models.SystemEvent.objects + filters = Q(type=param, bts_id=self.level_id) + else: + # For Dynamic Kinds coming from Database currently for Top Up + objects = models.UsageEvent.objects + filters = Q(kind='transfer') # Filter by infrastructure level. if self.level == 'tower': filters = filters & Q(bts__id=self.level_id) - elif self.level == 'network': + elif self.level == 'network' and param not in BTS_KINDS: filters = filters & Q(network__id=self.level_id) elif self.level == 'global': pass - # Create the queryset itself. + if kwargs.has_key('query'): + filters = filters & kwargs.pop('query') + if report_view == 'value': + filters = filters & Q(date__lte=end) & Q(date__gte=start) + result = models.UsageEvent.objects.filter(filters).values_list( + 'subscriber_id', flat=True).distinct() + return list(result) queryset = objects.filter(filters) + # Use qsstats to aggregate the queryset data on an interval. - if aggregation == 'duration': + if aggregation in ['duration', 'duration_minute']: queryset_stats = qsstats.QuerySetStats( queryset, 'date', aggregate=aggregates.Sum('billsec')) elif aggregation == 'up_byte_count': @@ -136,24 +174,124 @@ def aggregate_timeseries(self, param, **kwargs): elif aggregation == 'average_value': queryset_stats = qsstats.QuerySetStats( queryset, 'date', aggregate=aggregates.Avg('value')) + elif aggregation == 'valid_through': + queryset_stats = qsstats.QuerySetStats(queryset, 'valid_through') + elif aggregation == 'reload_transcation_count': + queryset_stats = qsstats.QuerySetStats( + queryset, 'date', aggregate=(aggregates.Count('to_number'))) + elif aggregation == 'reload_transcation_sum': + queryset_stats = qsstats.QuerySetStats( + queryset, 'date', + aggregate=(aggregates.Sum('change') * 0.00001)) + # Sum of change in amounts for SMS/CALL + elif aggregation in ['transaction_sum', 'transcation_count']: + # Change is negative value, set positive for charts + if report_view == 'summary': + adjust = -10 + elif param == 'add_money': + adjust = 0.00001 + elif param == 'transfer': + adjust = -0.00001 + else: + adjust = 1 + queryset_stats = qsstats.QuerySetStats( + queryset, 'date', aggregate=(aggregates.Sum('change') * adjust)) + + if report_view =='table_view': + imsi = {} + for qs in queryset_stats.qs.filter( + date__range=(str(start), str(end))): + if qs.subscriber.imsi in imsi: + imsi[qs.subscriber.imsi] += round(qs.change * adjust, 2) + else: + imsi[qs.subscriber.imsi] = round(qs.change * adjust, 2) + return imsi + + # if percentage is set for top top-up + percentage = kwargs['topup_percent'] + top_numbers = 1 + if percentage is not None: + numbers = {} + percentage = float(percentage) / 100 + # Create subscribers dict + for query in queryset: + numbers[query.to_number] = 0 + for query in queryset_stats.qs: + numbers[query.to_number] += (query.change * -1) + top_numbers = int(len(numbers) * percentage) + top_numbers = top_numbers if top_numbers > 1 else top_numbers + top_subscribers = list(dict( + sorted(numbers.iteritems(), key=itemgetter(1), + reverse=True)[:top_numbers]).keys()) + queryset = queryset_stats.qs.filter( + Q(to_number__in=top_subscribers)) + # Count the numbers + if aggregation == 'transcation_count': + queryset_stats = qsstats.QuerySetStats( + queryset, 'date', aggregate=( + aggregates.Count('to_number'))) + else: + # Sum of change + queryset_stats = qsstats.QuerySetStats( + queryset, 'date', aggregate=( + aggregates.Sum('change'))) + elif aggregation == 'loader': + queryset_stats = qsstats.QuerySetStats( + queryset, 'date', aggregate=aggregates.Count('subscriber_id')) else: queryset_stats = qsstats.QuerySetStats(queryset, 'date') - timeseries = queryset_stats.time_series(start, end, interval=interval) - # The timeseries results is a list of (datetime, value) pairs. We need + + if param =='bts down'or param=='bts up': + timeseries = queryset_stats.time_series(start, end, interval='minutes') + else: + timeseries = queryset_stats.time_series(start, end, + interval=interval) + + # The timeseries results is a list of (datetime, value) pairs. We need # to convert the datetimes to timestamps with millisecond precision and # then zip the pairs back together. + timeseries = queryset_stats.time_series(start, end, + interval=interval) + if param in BTS_KINDS: + timeseries = queryset_stats.time_series(start, end, + date_field='date', + interval='minutes') + for idx, val in enumerate(timeseries): + # remove unwanted objects count + if val[1] == 0: + timeseries.pop(idx) datetimes, values = zip(*timeseries) + if report_view == 'summary': + # Return sum count for pie-chart and table view + if aggregation == 'transaction_sum': + # When kind is change + return sum(values) * 0.000001 + elif aggregation == 'duration_minute': + return (sum(values) / 60.00) or 0 + else: + return sum(values) + timestamps = [ int(time.mktime(dt.timetuple()) * 1e3 + dt.microsecond / 1e3) for dt in datetimes ] # Round the stats values when necessary. rounded_values = [] - for value in values: - if round(value) != round(value, 2): - rounded_values.append(round(value, 2)) - else: - rounded_values.append(value) + if param in BTS_KINDS: + for value in values: + if param == 'bts down': + val = -1 if value == 1 else 0 + elif param == 'bts up': + val = 1 if value == 1 else 0 + rounded_values.append(val) + else: + for value in values: + if value <= 0: + rounded_values.append(value * -0.00001) + elif round(value) != round(value, 2): + rounded_values.append(round(value, 2)) + else: + rounded_values.append(value) return zip(timestamps, rounded_values) diff --git a/cloud/endagaweb/stats_app/views.py b/cloud/endagaweb/stats_app/views.py index df6c88df..ce911ff5 100644 --- a/cloud/endagaweb/stats_app/views.py +++ b/cloud/endagaweb/stats_app/views.py @@ -14,10 +14,17 @@ from rest_framework import response from rest_framework import status from rest_framework import views +import pytz +from rest_framework.authtoken.models import Token +import stripe +from guardian.shortcuts import get_objects_for_user +from ccm.common.currency import parse_credits, humanize_credits, \ + CURRENCIES, Money from endagaweb.stats_app import stats_client - +from endagaweb.models import (UserProfile, Ledger, Subscriber, UsageEvent, + Network, PendingCreditUpdate, Number) # Set the stat types that we can query for. Note that some of these kinds are # 'faux kinds' in that the stats client aggregates the true UsageEvent kinds # for these other categories: sms, call, uploaded_data, downloaded_data, @@ -26,13 +33,25 @@ CALL_KINDS = stats_client.CALL_KINDS + ['call'] GPRS_KINDS = ['total_data', 'uploaded_data', 'downloaded_data'] TIMESERIES_STAT_KEYS = stats_client.TIMESERIES_STAT_KEYS -VALID_STATS = SMS_KINDS + CALL_KINDS + GPRS_KINDS + TIMESERIES_STAT_KEYS +SUBSCRIBER_KINDS = stats_client.SUBSCRIBER_KINDS + \ + stats_client.ZERO_BALANCE_SUBSCRIBER + \ + stats_client.INACTIVE_SUBSCRIBER +BTS_STATUS = stats_client.BTS_STATUS +WATERFALL_KINDS = ['loader', 'reload_rate', 'reload_amount', + 'reload_transaction', 'average_load', 'average_frequency'] +NON_LOADER_KINDS = ['non_loader_base', 'cumulative_base'] +DENOMINATION_KINDS = stats_client.DENOMINATION_KINDS # Set valid intervals. INTERVALS = ['years', 'months', 'weeks', 'days', 'hours', 'minutes'] +TRANSFER_KINDS = stats_client.TRANSFER_KINDS +VALID_STATS = SMS_KINDS + CALL_KINDS + GPRS_KINDS + TIMESERIES_STAT_KEYS + \ + TRANSFER_KINDS + SUBSCRIBER_KINDS + WATERFALL_KINDS + \ + BTS_STATUS + DENOMINATION_KINDS + NON_LOADER_KINDS # Set valid aggregation types. AGGREGATIONS = ['count', 'duration', 'up_byte_count', 'down_byte_count', - 'average_value'] - + 'average_value', 'transaction_sum', 'transcation_count', + 'duration_minute'] +REPORT_VIEWS = ['summary', 'list'] # Any requested start time earlier than this date will be set to this date. # This comes from a bug where UEs were generated at an astounding rate in @@ -52,6 +71,9 @@ def parse_query_params(params): 'stat-types': ['sms'], 'level-id': -1, 'aggregation': 'count', + 'report-view': 'list', + 'extras': [], + 'topup-percent': None, } # Override defaults with any query params that have been set, if the # query params are valid. @@ -74,10 +96,20 @@ def parse_query_params(params): # If nothing validated, we just leave the stat-types as the default. if validated_types: parsed_params['stat-types'] = validated_types + # Check if stat-type is dynamic currently for denominations + if params.has_key('dynamic-stat') and bool(params['dynamic-stat']): + parsed_params['stat-types'] = stat_types + # For filtering top topups as per percentage + if params.has_key('topup-percent'): + parsed_params['topup-percent'] = params['topup-percent'] if 'level-id' in params: parsed_params['level-id'] = int(params['level-id']) if 'aggregation' in params and params['aggregation'] in AGGREGATIONS: parsed_params['aggregation'] = params['aggregation'] + if 'extras' in params: + parsed_params['extras'] = params['extras'].split(',') + if 'report-view' in params and params['report-view'] in REPORT_VIEWS: + parsed_params['report-view'] = params['report-view'] return parsed_params @@ -125,7 +157,7 @@ def get(self, request, infrastructure_level): data = { 'results': [], } - for stat_type in params['stat-types']: + for index, stat_type in enumerate(params['stat-types']): # Setup the appropriate stats client, SMS, call or GPRS. if stat_type in SMS_KINDS: client_type = stats_client.SMSStatsClient @@ -135,6 +167,18 @@ def get(self, request, infrastructure_level): client_type = stats_client.GPRSStatsClient elif stat_type in TIMESERIES_STAT_KEYS: client_type = stats_client.TimeseriesStatsClient + elif stat_type in SUBSCRIBER_KINDS: + client_type = stats_client.SubscriberStatsClient + elif stat_type in TRANSFER_KINDS: + client_type = stats_client.TransferStatsClient + elif stat_type in BTS_STATUS: + client_type = stats_client.BTSStatsClient + elif stat_type in WATERFALL_KINDS: + client_type = stats_client.WaterfallStatsClient + elif stat_type in NON_LOADER_KINDS: + client_type = stats_client.NonLoaderStatsClient + else: + client_type = stats_client.TopUpStatsClient # Instantiate the client at an infrastructure level. if infrastructure_level == 'global': client = client_type('global') @@ -142,6 +186,10 @@ def get(self, request, infrastructure_level): client = client_type('network', params['level-id']) elif infrastructure_level == 'tower': client = client_type('tower', params['level-id']) + try: + extra_param = params['extras'][index] + except IndexError: + extra_param = None # Get timeseries results and append it to data. results = client.timeseries( stat_type, @@ -149,10 +197,24 @@ def get(self, request, infrastructure_level): start_time_epoch=params['start-time-epoch'], end_time_epoch=params['end-time-epoch'], aggregation=params['aggregation'], + report_view=params['report-view'], + extras=extra_param, + topup_percent=params['topup-percent'] ) + if stat_type in TRANSFER_KINDS: + table_view = client.timeseries( + stat_type, + start_time_epoch=params['start-time-epoch'], + end_time_epoch=params['end-time-epoch'], + aggregation=params['aggregation'], + report_view ="table_view" + ) + else: + table_view = {} data['results'].append({ "key": stat_type, - "values": results + "values": results, + "retailer_table_data":table_view }) # Convert params.stat_types back to CSV and echo back the request. diff --git a/cloud/endagaweb/templates/dashboard/layout.html b/cloud/endagaweb/templates/dashboard/layout.html index c4567a7f..5b7ed374 100644 --- a/cloud/endagaweb/templates/dashboard/layout.html +++ b/cloud/endagaweb/templates/dashboard/layout.html @@ -23,23 +23,19 @@ - + {% block pagestyle %} {% endblock %} - {% if user_profile.user.is_staff %} + {% if user_profile.user.is_superuser %}