2 * Report template builder
5 angular.module('egReporter',
6 ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egReportMod', 'treeControl', 'ngToast'])
8 .config(['ngToastProvider', function(ngToastProvider) {
9 ngToastProvider.configure({
10 verticalPosition: 'bottom',
15 .config(function($routeProvider, $locationProvider, $compileProvider) {
16 $locationProvider.html5Mode(true);
17 $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
19 var resolver = {delay : function(egStartup) {return egStartup.go()}};
21 $routeProvider.when('/reporter/template/clone/:folder/:id', {
22 templateUrl: './reporter/t_edit_template',
23 controller: 'ReporterTemplateEdit',
27 $routeProvider.when('/reporter/legacy/template/clone/:folder/:id', {
28 templateUrl: './reporter/t_legacy',
29 controller: 'ReporterTemplateLegacy',
33 $routeProvider.when('/reporter/template/new/:folder', {
34 templateUrl: './reporter/t_edit_template',
35 controller: 'ReporterTemplateEdit',
39 $routeProvider.when('/reporter/legacy/main', {
40 templateUrl: './reporter/t_legacy',
41 controller: 'ReporterTemplateLegacy',
46 $routeProvider.otherwise({redirectTo : '/reporter/legacy/main'});
50 * controller for legacy template stuff
52 .controller('ReporterTemplateLegacy',
53 ['$scope','$routeParams','$location','egCore',
54 function($scope , $routeParams , $location , egCore) {
56 var template_id = $routeParams.id;
57 var folder_id = $routeParams.folder;
59 $scope.rurl = '/reports/oils_rpt.xhtml?ses=' + egCore.auth.token();
62 $scope.rurl = '/reports/oils_rpt_builder.xhtml?ses=' +
63 egCore.auth.token() + '&folder=' + folder_id;
65 if (template_id) $scope.rurl += '&ct=' + template_id;
71 * Uber-controller for template editing
73 .controller('ReporterTemplateEdit',
74 ['$scope','$q','$routeParams','$location','$timeout','$window','egCore','$uibModal','egPromptDialog',
75 'egGridDataProvider','egReportTemplateSvc','$uibModal','egConfirmDialog','egSelectDialog','ngToast',
76 function($scope , $q , $routeParams , $location , $timeout , $window, egCore , $uibModal , egPromptDialog ,
77 egGridDataProvider , egReportTemplateSvc , $uibModal , egConfirmDialog , egSelectDialog , ngToast) {
79 function values(o) { return Object.keys(o).map(function(k){return o[k]}) };
81 var template_id = $routeParams.id;
82 var folder_id = $routeParams.folder;
84 $scope.grid_display_fields_provider = egGridDataProvider.instance({
85 get : function (offset, count) {
86 return this.arrayNotifier(egReportTemplateSvc.display_fields, offset, count);
89 $scope.grid_filter_fields_provider = egGridDataProvider.instance({
90 get : function (offset, count) {
91 return this.arrayNotifier(egReportTemplateSvc.filter_fields, offset, count);
95 var dgrid = $scope.display_grid_controls = {};
96 var fgrid = $scope.filter_grid_controls = {};
98 var default_filter_obj = {
100 label : egReportTemplateSvc.Filters['='].label
103 var default_transform_obj = {
105 label : egReportTemplateSvc.Transforms.Bare.label,
109 function mergePaths (items) {
112 items.forEach(function (item) {
117 item.path.forEach(function (p, i, a) {
118 var alias; // unpredictable hashes are fine for intermediate tables
120 if (i) { // not at the top of the tree
121 if (i == 1) join_path = join_path.split('-')[0];
123 // SQLBuilder relies on the first dash-separated component
124 // of the join key to specify the column of left-hand relation
125 // to join on; for has_many and might_have link types, we have to grab the
126 // primary key of the left-hand table; otherwise, we can
127 // just use the field/column name found in p.uplink.name.
128 var uplink = (p.uplink.reltype == 'has_many' || p.uplink.reltype == 'might_have') ?
129 egCore.idl.classes[p.from.split('.').slice(-1)[0]].pkey + '-' + p.uplink.name :
131 join_path += '-' + uplink;
132 alias = hex_md5(join_path);
134 var uplink_alias = uplink + '-' + alias;
136 if (!t.join) t.join = {};
137 if (!t.join[uplink_alias]) t.join[uplink_alias] = {};
139 t = t.join[uplink_alias];
141 var djtype = 'inner';
142 // we use LEFT JOINs for might_have and has_many, AND
143 // also if our previous JOIN was a LEFT JOIN
145 // The last piece prevents later joins from limiting those
146 // closer to the core table
147 if (p.uplink.reltype != 'has_a' || last_jtype == 'left') djtype = 'left';
149 t.type = p.jtype || djtype;
151 t.key = p.uplink.key;
153 join_path = p.classname + '-' + p.classname;
154 alias = hex_md5(join_path);
157 if (!t.alias) t.alias = alias;
160 t.table = p.struct.source ? p.struct.source : p.table;
161 t.idlclass = p.classname;
163 if (a.length == i + 1) { // end of the path array, need a predictable hash
164 t.label = item.path_label;
165 t.alias = hex_md5(item.path_label);
173 // expose for testing
174 $scope._mergePaths = mergePaths;
176 $scope.constructTemplate = function () {
177 var param_counter = 0;
180 doc_url : $scope.templateDocURL,
181 core_class : egCore.idl.classTree.top.classname,
182 select : dgrid.allItems().map(function (i) {
185 path : i.path[i.path.length - 1].classname + '-' + i.name,
186 field_doc : i.doc_text,
187 relation : hex_md5(i.path_label),
190 transform : i.transform ? i.transform.transform : '',
191 transform_label : i.transform ? i.transform.label : '',
192 aggregate : !!i.transform.aggregate
196 from : mergePaths( dgrid.allItems().concat(fgrid.allItems()) ),
197 where : fgrid.allItems().filter(function(i) {
198 return !i.transform.aggregate;
199 }).map(function (i) {
202 i.operator.op == 'is' ||
203 i.operator.op == 'is not' ||
204 i.operator.op == 'is blank' ||
205 i.operator.op == 'is not blank'
207 cond[i.operator.op] = null;
209 if (i.value === undefined) {
210 cond[i.operator.op] = '::P' + param_counter++;
212 cond[i.operator.op] = i.value;
217 path : i.path[i.path.length - 1].classname + '-' + i.name,
218 field_doc : i.doc_text,
219 relation : hex_md5(i.path_label),
222 transform : i.transform.transform,
223 transform_label : i.transform.label,
226 condition : cond // constructed above
229 having : fgrid.allItems().filter(function(i) {
230 return !!i.transform.aggregate;
231 }).map(function (i) {
233 if (i.value === undefined) {
234 cond[i.operator.op] = '::P' + param_counter++;
236 cond[i.operator.op] = i.value;
240 path : i.path[i.path.length - 1].classname + '-' + i.name,
241 field_doc : i.doc_text,
242 relation : hex_md5(i.path_label),
245 transform : i.transform.transform,
246 transform_label : i.transform.label,
249 condition : cond // constructed above
252 display_cols: angular.copy( dgrid.allItems() ).map(strip_item),
253 filter_cols : angular.copy( fgrid.allItems() ).map(strip_item)
256 function strip_item (i) {
258 i.path.forEach(function(p){
262 delete p.struct.permacrud;
263 delete p.struct.field_map;
264 delete p.struct.fields;
271 $scope.upgradeTemplate = function(template) {
272 template.name(template.name() + ' (converted from XUL)');
273 template.data.version = 5;
277 for (var key in template.data.rel_cache) {
278 if (key == 'order_by') {
279 order_by = template.data.rel_cache[key];
281 rels.push(template.data.rel_cache[key]);
285 // preserve the old select order for the display cols
287 template.data.select.map(function(val, idx) {
288 // set key to unique value easily derived from relcache
289 sel_order[val.relation + val.column.colname] = idx;
292 template.data['display_cols'] = [];
293 template.data['filter_cols'] = [];
295 function _convertPath(orig, rel) {
298 var table_path = rel.path.split(/\./);
299 if (table_path.length > 1 || rel.path.indexOf('-') > -1) table_path.push( rel.idlclass );
303 table_path.forEach(function(link) {
304 var cls = link.split(/-/)[0];
305 var fld = link.split(/-/)[1];
307 label : egCore.idl.classes[cls].label
309 if (prev_link != '') {
310 var link_parts = prev_link.split(/-/);
311 args['from'] = link_parts[0];
312 var join_parts = link_parts[1].split(/>/);
313 var prev_col = join_parts[0];
314 egCore.idl.classes[prev_link.split(/-/)[0]].fields.forEach(function(f) {
315 if (prev_col == f.name) {
319 args['jtype'] = join_parts[1]; // frequently undefined
321 newPath.push(egCore.idl.classTree.buildNode(cls, args));
328 function _buildCols(rel, tab_type, col_idx) {
329 if (tab_type == 'dis_tab') {
330 col_type = 'display_cols';
332 col_type = 'filter_cols';
335 for (var col in rel.fields[tab_type]) {
336 var orig = rel.fields[tab_type][col];
339 path : _convertPath(orig, rel),
341 datatype : orig.datatype,
342 doc_text : orig.field_doc,
344 label : orig.transform_label,
345 transform : orig.transform,
346 aggregate : (orig.aggregate == "undefined") ? undefined : orig.aggregate // old structure sometimes has undefined as a quoted string
348 path_label : rel.label.replace('::', '->')
350 if (col_type == 'filter_cols') {
353 label : orig.op_label
355 col['index'] = col_idx++;
356 if ('value' in orig.op_value) {
357 col['value'] = orig.op_value.value;
360 col['index'] = sel_order[rel.alias + orig.colname];
363 template.data[col_type].push(col);
367 rels.map(function(rel) {
368 _buildCols(rel, 'dis_tab');
369 _buildCols(rel, 'filter_tab', template.data.filter_cols.length);
370 _buildCols(rel, 'aggfilter_tab', template.data.filter_cols.length);
373 template.data['display_cols'].sort(function(a, b){return a.index - b.index});
376 function loadTemplate () {
377 if (!template_id) return;
378 egCore.pcrud.retrieve( 'rt', template_id)
379 .then( function(template) {
380 template.data = angular.fromJson(template.data());
381 if (template.data.version < 5) {
382 $scope.upgradeTemplate(template);
385 $scope.templateName = template.name() + ' (clone)';
386 $scope.templateDescription = template.description();
387 $scope.templateDocURL = template.data.doc_url;
389 $scope.changeCoreSource( template.data.core_class );
391 egReportTemplateSvc.display_fields = template.data.display_cols;
392 egReportTemplateSvc.filter_fields = template.data.filter_cols;
402 $scope.saveTemplate = function () {
403 var tmpl = new egCore.idl.rt();
404 tmpl.name( $scope.templateName );
405 tmpl.description( $scope.templateDescription );
406 tmpl.owner(egCore.auth.user().id());
407 tmpl.folder(folder_id);
408 tmpl.data(angular.toJson($scope.constructTemplate()));
410 egConfirmDialog.open(tmpl.name(), egCore.strings.TEMPLATE_CONF_CONFIRM_SAVE,
412 return egCore.pcrud.create( tmpl )
415 ngToast.create(egCore.strings.TEMPLATE_CONF_SUCCESS_SAVE);
418 $window.location.href = egCore.env.basePath + 'reporter/legacy/main';
424 ngToast.warning(egCore.strings.TEMPLATE_CONF_FAIL_SAVE);
431 $scope.addDisplayFields = function () {
432 var t = $scope.selected_transform;
433 if (!t) t = default_transform_obj;
435 egReportTemplateSvc.addFields(
437 $scope.selected_source_field_list,
439 $scope.currentPathLabel,
442 $scope.selected_transform = null;
446 $scope.addFilterFields = function () {
447 var t = $scope.selected_transform;
448 if (!t) t = default_transform_obj;
449 f = default_filter_obj;
451 egReportTemplateSvc.addFields(
453 $scope.selected_source_field_list,
455 $scope.currentPathLabel,
459 $scope.selected_transform = null;
463 $scope.moveDisplayFieldUp = function (items) {
464 items.reverse().forEach(function(item) {
465 egReportTemplateSvc.moveFieldUp('display_fields', item);
470 $scope.moveDisplayFieldDown = function (items) {
471 items.forEach(function(item) {
472 egReportTemplateSvc.moveFieldDown('display_fields', item);
477 $scope.removeDisplayField = function (items) {
478 items.forEach(function(item) {egReportTemplateSvc.removeField('display_fields', item)});
482 $scope.changeDisplayLabel = function (items) {
483 items.forEach(function(item) {
484 egPromptDialog.open(egCore.strings.TEMPLATE_CONF_PROMPT_CHANGE, item.label || '',
485 {ok : function(value) {
486 if (value) egReportTemplateSvc.display_fields[item.index].label = value;
493 $scope.changeDisplayFieldDoc = function (items) {
494 items.forEach(function(item) {
495 egPromptDialog.open(egCore.strings.TEMPLATE_FIELD_DOC_PROMPT_CHANGE, item.doc_text || '',
496 {ok : function(value) {
497 if (value) egReportTemplateSvc.display_fields[item.index].doc_text = value;
504 $scope.changeFilterFieldDoc = function (items) {
505 items.forEach(function(item) {
506 egPromptDialog.open(egCore.strings.TEMPLATE_FIELD_DOC_PROMPT_CHANGE, item.doc_text || '',
507 {ok : function(value) {
508 if (value) egReportTemplateSvc.filter_fields[item.index].doc_text = value;
515 $scope.changeFilterValue = function (items) {
516 items.forEach(function(item) {
518 if (item.datatype == "bool") {
519 var displayVal = typeof item.value === "undefined" ? egCore.strings.TEMPLATE_CONF_UNSET :
520 item.value === 't' ? egCore.strings.TEMPLATE_CONF_TRUE :
521 item.value === 'f' ? egCore.strings.TEMPLATE_CONF_FALSE :
522 item.value.toString();
523 egConfirmDialog.open(egCore.strings.TEMPLATE_CONF_DEFAULT, displayVal,
525 egReportTemplateSvc.filter_fields[item.index].value = 't';
527 cancel : function() {
528 egReportTemplateSvc.filter_fields[item.index].value = 'f';
529 }}, egCore.strings.TEMPLATE_CONF_TRUE, egCore.strings.TEMPLATE_CONF_FALSE
532 egPromptDialog.open(egCore.strings.TEMPLATE_CONF_DEFAULT, item.value || '',
533 {ok : function(value) {
534 if (value) egReportTemplateSvc.updateFilterValue(item, value);
542 $scope.changeTransform = function (items) {
547 angular.forEach(egReportTemplateSvc.Transforms, function (o,n) {
548 if ( o.datatype.indexOf(f.datatype) > -1) {
549 if (tlist.indexOf(o.label) == -1) tlist.push( o.label );
553 items.forEach(function(item) {
555 egCore.strings.SELECT_TFORM, tlist, item.transform.label,
556 {ok : function(value) {
558 var t = egReportTemplateSvc.getTransformByLabel(value);
562 aggregate : egReportTemplateSvc.Transforms[t].aggregate ? true : false
572 $scope.changeOperator = function (items) {
575 Object.keys(egReportTemplateSvc.Filters).forEach(function(k){
576 var v = egReportTemplateSvc.Filters[k];
577 if (flist.indexOf(v.label) == -1) flist.push(v.label);
578 if (v.labels && v.labels.length > 0) {
579 v.labels.forEach(function(l) {
580 if (flist.indexOf(l) == -1) flist.push(l);
585 items.forEach(function(item) {
586 var l = item.operator ? item.operator.label : '';
588 egCore.strings.SELECT_OP, flist, l,
589 {ok : function(value) {
591 var t = egReportTemplateSvc.getFilterByLabel(value);
592 item.operator = { label: value, op : t };
594 //Update the filter value based on the new operator, because
595 // different operators treat the value differently
596 egReportTemplateSvc.updateFilterValue(item, egReportTemplateSvc.filter_fields[item.index].value);
605 $scope.removeFilterValue = function (items) {
606 items.forEach(function(item) {delete egReportTemplateSvc.filter_fields[item.index].value});
610 $scope.removeFilterField = function (items) {
611 items.forEach(function(item) {egReportTemplateSvc.removeField('filter_fields', item)});
615 $scope.allSources = values(egCore.idl.classes).sort( function(a,b) {
616 if (a.core && !b.core) return -1;
617 if (b.core && !a.core) return 1;
618 aname = a.label ? a.label : a.name;
619 bname = b.label ? b.label : b.name;
620 if (aname > bname) return 1;
624 $scope.class_tree = [];
625 $scope.selected_source = null;
626 $scope.selected_source_fields = [];
627 $scope.selected_source_field_list = [];
628 $scope.available_field_transforms = [];
629 $scope.coreSource = null;
630 $scope.coreSourceChosen = false;
631 $scope.currentPathLabel = '';
633 $scope.treeExpand = function (node, expanding) {
634 if (expanding) node.children.map(egCore.idl.classTree.fleshNode);
637 $scope.filterFields = function (n) {
638 return n.virtual ? false : true;
639 // should we hide links?
640 return n.datatype && n.datatype != 'link'
643 $scope.field_tree_opts = {
644 multiSelection: true,
645 equality : function(node1, node2) {
646 return node1.name == node2.name;
650 $scope.field_transforms_tree_opts = {
651 equality : function(node1, node2) {
652 if (!node2) return false;
653 return node1.transform == node2.transform;
657 $scope.selectFields = function () {
658 while ($scope.available_field_transforms.length) {
659 $scope.available_field_transforms.pop();
662 angular.forEach( $scope.selected_source_field_list, function (f) {
663 angular.forEach(egReportTemplateSvc.Transforms, function (o,n) {
664 if ( o.datatype.indexOf(f.datatype) > -1) {
667 angular.forEach($scope.available_field_transforms, function (t) {
668 if (t.transform == n) include = false;
671 if (include) $scope.available_field_transforms.push({
674 aggregate : o.aggregate ? true : false
682 $scope.selectSource = function (node, selected, $path) {
684 while ($scope.selected_source_field_list.length) {
685 $scope.selected_source_field_list.pop();
687 while ($scope.selected_source_fields.length) {
688 $scope.selected_source_fields.pop();
692 $scope.currentPath = angular.copy( $path().reverse() );
693 $scope.selected_source = node;
694 $scope.currentPathLabel = $scope.currentPath.map(function(n,i){
696 if (i && n.jtype) l += ' (' + n.jtype + ')';
699 angular.forEach( node.fields, function (f) {
700 $scope.selected_source_fields.push( f );
703 $scope.currentPathLabel = '';
706 // console.log($scope.selected_source);
709 $scope.changeCoreSource = function (new_core) {
710 console.log('changeCoreSource: '+new_core);
711 function change_core () {
712 if (new_core) $scope.coreSource = new_core;
713 $scope.coreSourceChosen = true;
715 $scope.class_tree.pop();
716 $scope.class_tree.push(
717 egCore.idl.classTree.setTop($scope.coreSource)
720 while ($scope.selected_source_fields.length) {
721 $scope.selected_source_fields.pop();
724 while ($scope.available_field_transforms.length) {
725 $scope.available_field_transforms.pop();
728 $scope.currentPathLabel = '';
731 if ($scope.coreSourceChosen) {
732 egConfirmDialog.open(
733 egCore.strings.FOLDERS_TEMPLATE,
734 egCore.strings.SOURCE_SETUP_CONFIRM_EXIT,