diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..21f00cff --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,16 @@ +image: + illa/for_splinter:v1 + +before_script: + - pip install celery + - pip install -r requirements.txt + - python setup.py develop + - pip install Django==1.8 + +tests: + script: + - python manage.py test + +lint: + script: + - flake8 diff --git a/report_builder/api/serializers.py b/report_builder/api/serializers.py index 61064f5d..2d790ca5 100644 --- a/report_builder/api/serializers.py +++ b/report_builder/api/serializers.py @@ -60,7 +60,9 @@ class Meta: fields = ( 'id', 'name', 'description', 'modified', 'root_model', 'root_model_name', 'displayfield_set', 'distinct', 'user_created', 'user_modified', - 'filterfield_set', 'report_file', 'report_file_creation') + 'filterfield_set', 'report_file', 'report_file_creation', + 'chart_categories', 'chart_series', 'chart_values', 'chart_type', 'chart_style', + 'chart_stacked', 'chart_labels', 'chart_total') read_only_fields = ('report_file', 'report_file_creation') def validate(self, data): @@ -79,6 +81,14 @@ def update(self, instance, validated_data): with transaction.atomic(): instance.name = validated_data.get('name', instance.name) + instance.chart_categories = validated_data.get('chart_categories', instance.chart_categories) + instance.chart_series = validated_data.get('chart_series', instance.chart_series) + instance.chart_values = validated_data.get('chart_values', instance.chart_values) + instance.chart_type = validated_data.get('chart_type', instance.chart_type) + instance.chart_style = validated_data.get('chart_style', instance.chart_style) + instance.chart_stacked = validated_data.get('chart_stacked', instance.chart_stacked) + instance.chart_labels = validated_data.get('chart_labels', instance.chart_labels) + instance.chart_total = validated_data.get('chart_total', instance.chart_total) instance.description = validated_data.get('description', instance.description) instance.distinct = validated_data.get( 'distinct', instance.distinct) diff --git a/report_builder/migrations/0004_auto_20160906_1149.py b/report_builder/migrations/0004_auto_20160906_1149.py new file mode 100644 index 00000000..c83ab58e --- /dev/null +++ b/report_builder/migrations/0004_auto_20160906_1149.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('report_builder', '0003_auto_20150720_1549'), + ] + + operations = [ + migrations.AddField( + model_name='report', + name='chart_categories', + field=models.IntegerField(null=True, blank=True), + ), + migrations.AddField( + model_name='report', + name='chart_series', + field=models.IntegerField(null=True, blank=True), + ), + migrations.AddField( + model_name='report', + name='chart_values', + field=models.IntegerField(null=True, blank=True), + ), + ] diff --git a/report_builder/migrations/0005_report_chart_type.py b/report_builder/migrations/0005_report_chart_type.py new file mode 100644 index 00000000..aacc112e --- /dev/null +++ b/report_builder/migrations/0005_report_chart_type.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('report_builder', '0004_auto_20160906_1149'), + ] + + operations = [ + migrations.AddField( + model_name='report', + name='chart_type', + field=models.IntegerField(null=True, blank=True), + ), + ] diff --git a/report_builder/migrations/0006_report_chart_style.py b/report_builder/migrations/0006_report_chart_style.py new file mode 100644 index 00000000..b70ebf75 --- /dev/null +++ b/report_builder/migrations/0006_report_chart_style.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('report_builder', '0005_report_chart_type'), + ] + + operations = [ + migrations.AddField( + model_name='report', + name='chart_style', + field=models.CharField(max_length=16, null=True, blank=True), + ), + ] diff --git a/report_builder/migrations/0007_auto_20160909_0916.py b/report_builder/migrations/0007_auto_20160909_0916.py new file mode 100644 index 00000000..2da3a83c --- /dev/null +++ b/report_builder/migrations/0007_auto_20160909_0916.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('report_builder', '0006_report_chart_style'), + ] + + operations = [ + migrations.AlterField( + model_name='report', + name='chart_values', + field=models.CommaSeparatedIntegerField(max_length=64, null=True, blank=True), + ), + ] diff --git a/report_builder/migrations/0008_auto_20160909_1231.py b/report_builder/migrations/0008_auto_20160909_1231.py new file mode 100644 index 00000000..7820018f --- /dev/null +++ b/report_builder/migrations/0008_auto_20160909_1231.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('report_builder', '0007_auto_20160909_0916'), + ] + + operations = [ + migrations.AddField( + model_name='report', + name='chart_labels', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='report', + name='chart_stacked', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='report', + name='chart_total', + field=models.BooleanField(default=False), + ), + ] diff --git a/report_builder/migrations/0009_auto_20160910_0937.py b/report_builder/migrations/0009_auto_20160910_0937.py new file mode 100644 index 00000000..6813ffbd --- /dev/null +++ b/report_builder/migrations/0009_auto_20160910_0937.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('report_builder', '0008_auto_20160909_1231'), + ] + + operations = [ + migrations.AlterField( + model_name='report', + name='chart_categories', + field=models.CommaSeparatedIntegerField(max_length=64, null=True, blank=True), + ), + ] diff --git a/report_builder/migrations/0010_auto_20160910_1037.py b/report_builder/migrations/0010_auto_20160910_1037.py new file mode 100644 index 00000000..bd9baace --- /dev/null +++ b/report_builder/migrations/0010_auto_20160910_1037.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('report_builder', '0009_auto_20160910_0937'), + ] + + operations = [ + migrations.AlterField( + model_name='report', + name='chart_series', + field=models.CommaSeparatedIntegerField(max_length=64, null=True, blank=True), + ), + ] diff --git a/report_builder/models.py b/report_builder/models.py index 7083a97b..3fdd3af9 100644 --- a/report_builder/models.py +++ b/report_builder/models.py @@ -86,6 +86,14 @@ def allowed_models(): AUTH_USER_MODEL, blank=True, help_text="These users have starred this report for easy reference.", related_name="report_starred_set") + chart_style = models.CharField(max_length=16, null=True, blank=True) + chart_type = models.IntegerField(null=True, blank=True) + chart_categories = models.CommaSeparatedIntegerField(max_length=64, null=True, blank=True) + chart_series = models.CommaSeparatedIntegerField(max_length=64, null=True, blank=True) + chart_values = models.CommaSeparatedIntegerField(max_length=64, null=True, blank=True) + chart_stacked = models.BooleanField(default=False) + chart_labels = models.BooleanField(default=False) + chart_total = models.BooleanField(default=False) def save(self, *args, **kwargs): if not self.id: diff --git a/report_builder/static/report_builder/controllers.js b/report_builder/static/report_builder/controllers.js index aeb0a310..1774c707 100644 --- a/report_builder/static/report_builder/controllers.js +++ b/report_builder/static/report_builder/controllers.js @@ -8,7 +8,7 @@ reportBuilderApp.controller('addCtrl', function($scope, $location, reportService $location.path('/report/' + result.id, true); }); } - } + }; }); reportBuilderApp.controller('homeCtrl', function($scope, $routeParams, $location, $mdSidenav, reportService) { @@ -38,7 +38,7 @@ reportBuilderApp.controller('homeCtrl', function($scope, $routeParams, $location return true; } return false; - } + }; $scope.requestFullscreen = function() { var @@ -56,19 +56,20 @@ reportBuilderApp.controller('homeCtrl', function($scope, $routeParams, $location $scope.fields_header = report.root_model_name; $scope.report = report; $scope.report.lastSaved = null; + $scope.report.create_chart_lists = true; root_related_field = { verbose_name: report.root_model_name, field_name: '', path: '', model_id: report.root_model - } + }; data = { "model": report.root_model, "path": "", "path_verbose": "", "field": "" - } - $scope.related_fields = [root_related_field] + }; + $scope.related_fields = [root_related_field]; reportService.getRelatedFields(data).then(function(result) { root_related_field.related_fields = result; var help_text = 'This model is included in report builder.'; @@ -172,7 +173,7 @@ reportBuilderApp.controller('LeftCtrl', function($scope, $routeParams, $mdSidena if (!$routeParams.reportId) { $mdSidenav('left').open(); } -}) +}); reportBuilderApp.controller('FieldsCtrl', function($scope, $mdSidenav, reportService) { $scope.load_fields = function(field) { @@ -181,7 +182,7 @@ reportBuilderApp.controller('FieldsCtrl', function($scope, $mdSidenav, reportSer "path": field.path, "path_verbose": field.path_verbose, "field": field.field_name - } + }; $scope.help_text = field.help_text; $scope.fields_header = field.verbose_name; reportService.getFields(data).then(function(result) { @@ -195,7 +196,7 @@ reportBuilderApp.controller('FieldsCtrl', function($scope, $mdSidenav, reportSer } $scope.help_text = help_text; }); - } + }; $scope.toggle_related_fields = function(node) { field = node.$nodeScope.$modelValue; @@ -204,7 +205,7 @@ reportBuilderApp.controller('FieldsCtrl', function($scope, $mdSidenav, reportSer "model": field.model_id, "path": parent_field.path, "field": field.field_name - } + }; reportService.getRelatedFields(data).then(function(result) { field.related_fields = result; }); @@ -246,7 +247,7 @@ reportBuilderApp.controller('ReportOptionsCtrl', function($scope, $location, $wi // $location.path('/report/' + list[0].id, true); }); }); - } + }; }); reportBuilderApp.controller('ReportFilterCtrl', function($scope) { @@ -255,8 +256,135 @@ reportBuilderApp.controller('ReportFilterCtrl', function($scope) { }; }); +reportBuilderApp.controller('ChartOptionsCtrl', function($scope, $window, $http, $timeout, $mdToast, reportService) { + $scope.chart_styles = ['area', 'bar', 'column', 'line', 'pie']; + + $scope.$watch('report.displayfield_set', function(newValue, oldValue) { + if (newValue === undefined || $scope.report === undefined) return; + $scope.report_fields_indexes = newValue.map(function(el, idx) { return idx; }); + $scope.report_fields_names = newValue.map(function(el, idx) { return el.name; }); + }, true); + + function isNotEmpty(string) { + return string !== ''; + } + + function parseInt1(el) { + return parseInt(el); + } + + function toListOfInts(element_name, list_name) { + if ($scope.report[element_name] === null) { + $scope.report[list_name] = []; + } else { + $scope.report[list_name] = $scope.report[element_name].split(',').filter(isNotEmpty).map(parseInt1); + } + } + + $scope.$watch('report.create_chart_lists', function(newValue, oldValue) { + if (newValue) { + toListOfInts('chart_categories', 'chart_categories_list'); + toListOfInts('chart_series', 'chart_series_list'); + toListOfInts('chart_values', 'chart_values_list'); + $scope.report.create_chart_lists = false; + } + }); + + $scope.remove_from_list = function(index, list_name) { + var name = 'chart_' + list_name + '_list'; + $scope.report[name].splice(index, 1); + }; + $scope.add_to_list = function(list_name) { + var name = 'chart_' + list_name + '_list'; + $scope.report[name].push(null); + }; +}); + reportBuilderApp.controller('ReportShowCtrl', function($scope, $window, $http, $timeout, $mdToast, reportService) { + + function chart_series_from_columns(data, x, y, titles) { + var categories = []; + var unique_categories = []; + data.forEach(function(row, idx) { + var row_category = ""; + for (var i = 0; i < x.length; i++) { + if (x[i] == null) continue; + row_category += row[x[i]]; + } + categories.push(row_category); + if (unique_categories.indexOf(row_category) < 0) { + unique_categories.push(row_category); + } + }); + var series_data = []; + for (var i = 0; i < y.length; i++) { + if (y[i] == null) continue; + series_data.push({ + name: titles[y[i]], + data: data.map(function(row, idx) { + return [ categories[idx], row[y[i]]]; + }), + }); + } + return { + categories: unique_categories, + series: series_data, + }; + } + + function chart_series_from_rows(data, x1, x2, y) { + var categories = []; + var unique_categories = []; + data.forEach(function(row, idx) { + var row_category = ""; + for (var i = 0; i < x1.length; i++) { + if (x1[i] == null) continue; + row_category += row[x1[i]]; + } + categories.push(row_category); + if (unique_categories.indexOf(row_category) < 0) { + unique_categories.push(row_category); + } + }); + var series_data_dict = {}; + for (i = 0; i < data.length; i++) { + var row_series = ""; + for (var j = 0; j < x2.length; j++) { + if (x2[j] == null) continue; + row_series += data[i][x2[j]]; + } + if (! (row_series in series_data_dict)) { + series_data_dict[row_series] = []; + } + series_data_dict[row_series].push([categories[i], data[i][y]]); + } + var series_data = []; + for (var key in series_data_dict) { + series_data.push({ + name: key, + data: series_data_dict[key] + }); + } + return { + categories: unique_categories, + series: series_data, + }; + } + + function prepare_for_chart(data, report) { + var totals = false; + report.displayfield_set.forEach(function(field) { + if (field.total) { + totals = true; + } + }); + if (totals) { + data.splice(data.length - 2, 2); + } + } + $scope.getPreview = function() { + $scope.reportData.chart = false; $scope.reportData.statusMessage = null; $scope.reportData.refresh = true; reportService.getPreview($scope.report.id).then(function(data) { @@ -275,6 +403,69 @@ reportBuilderApp.controller('ReportShowCtrl', function($scope, $window, $http, $ }); }; + $scope.createChart = function() { + $scope.reportData.chart = true; + $scope.reportData.statusMessage = null; + $scope.reportData.refresh = true; + reportService.getPreview($scope.report.id).then(function(data) { + prepare_for_chart(data, $scope.report); + var chart_data = {}; + if ($scope.report.chart_type == 2) { + chart_data = chart_series_from_columns(data, $scope.report.chart_categories_list, + $scope.report.chart_values_list, data.meta.titles); + } + else if ($scope.report.chart_type == 3) { + chart_data = chart_series_from_rows(data, $scope.report.chart_categories_list, $scope.report.chart_series_list, $scope.report.chart_values_list[0]); + } else { + return; + } + var chart_dict = { + chart: { + type: $scope.report.chart_style, + height: 450, + }, + xAxis: { + categories: chart_data.categories, + }, + title: { + text: $scope.report.name, + }, + yAxis: { + stackLabels: { + enabled: $scope.report.chart_total, + formatter: function() {return 'total: ' + this.total;}, + } + }, + series: chart_data.series, + }; + chart_dict['plotOptions'] = {}; + chart_dict['plotOptions'][$scope.report.chart_style] = { + dataLabels: { + enabled: $scope.report.chart_labels, + }, + stacking: $scope.report.chart_stacked ? 'normal' : false, + }; + var chart = Highcharts.chart('highchart_container', chart_dict); + if ($scope.report.chart_style == 'bar' && chart_data.categories.length * chart_data.series.length > 15 ) { + var series_size = 1; + if (chart_data.series.length > 1) { + series_size = chart_data.series.length / 2; + } + var size = chart_data.categories.length * series_size * 20; + chart.setSize(null, Math.max(size + 100, 450)); + } + + $scope.reportData.refresh = false; + }, function(response) { + $scope.reportData.refresh = false; + $scope.reportData.statusMessage = "Error with status code " + response.status; + }); + }; + + function isNotNull(value) { + return value !== undefined && value !== null; + } + $scope.save = function() { angular.forEach($scope.report.displayfield_set, function(value, index) { value.position = index; @@ -285,6 +476,9 @@ reportBuilderApp.controller('ReportShowCtrl', function($scope, $window, $http, $ angular.forEach($scope.report.filterfield_set, function(value, index) { value.position = index; }); + $scope.report.chart_categories = $scope.report.chart_categories_list.filter(isNotNull).join(','); + $scope.report.chart_values = $scope.report.chart_values_list.filter(isNotNull).join(','); + $scope.report.chart_series = $scope.report.chart_series_list.filter(isNotNull).join(','); $scope.report.save().then(function(result) { $scope.report.lastSaved = new Date(); $scope.reportData.reportErrors = null; @@ -302,7 +496,7 @@ reportBuilderApp.controller('ReportShowCtrl', function($scope, $window, $http, $ }; $scope.downloadReport = function(filetype) { - base_url = BASE_URL + 'report/' + $scope.report.id + base_url = BASE_URL + 'report/' + $scope.report.id; url = base_url + '/download_file/' + filetype + '/'; $scope.workerStatus = 'Requesting report'; if (ASYNC_REPORT === "True") { @@ -329,12 +523,12 @@ reportBuilderApp.controller('ReportShowCtrl', function($scope, $window, $http, $ $timeout(checkPoller, 1000 + (500 * attempts)); } } - }) + }); }; $timeout(checkPoller, 100); }); } else { $window.location.href = url; } - } + }; }); diff --git a/report_builder/static/report_builder/partials/chart_options.html b/report_builder/static/report_builder/partials/chart_options.html new file mode 100644 index 00000000..266b87da --- /dev/null +++ b/report_builder/static/report_builder/partials/chart_options.html @@ -0,0 +1,70 @@ + + + + +

Type:

+ + Series from columns + + + + Series from rows + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Style:

+ + {{ style }} + +
+
+ + + +

Others options:

+ Stacked + Labels + Total +
+
+ + +
+
+ diff --git a/report_builder/static/report_builder/partials/home.html b/report_builder/static/report_builder/partials/home.html index 3beb6c73..1cf5c926 100644 --- a/report_builder/static/report_builder/partials/home.html +++ b/report_builder/static/report_builder/partials/home.html @@ -123,6 +123,12 @@

+ + Chart options + +
+
+
Report diff --git a/report_builder/static/report_builder/partials/show_report.html b/report_builder/static/report_builder/partials/show_report.html index 30f58823..7bc5b249 100644 --- a/report_builder/static/report_builder/partials/show_report.html +++ b/report_builder/static/report_builder/partials/show_report.html @@ -4,6 +4,7 @@ Preview xlsx csv + chart Changes have not been saved @@ -54,7 +55,10 @@

Filter Field Errors

-
+
+
+
+
+ + +