jsharmony
Version:
Rapid Application Development (RAD) Platform for Node.js Database Application Development
922 lines (849 loc) • 40.7 kB
JavaScript
/*
Copyright 2017 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
var _ = require('lodash');
var fs = require('fs');
var tmp = require('tmp');
var async = require('async');
var HelperFS = require('./lib/HelperFS.js');
var Helper = require('./lib/Helper.js');
var ejs = require('ejs');
var ejsext = require('./lib/ejsext.js');
var moment = require('moment');
var querystring = require('querystring');
var _HEADER_ZOOM = 0.75;
var _BROWSER_RECYCLE_COUNT = 50;
function AppSrvRpt(appsrv) {
this.AppSrv = appsrv;
this.browser = null;
this.browserreqcount = 0;
this.browserqueue = null;
this.InitReportQueue();
process.addListener('exit', function (code) { if (this.browser != null) { this.browser.close(); this.browser = null; } });
}
AppSrvRpt.prototype.InitReportQueue = function () {
var _this = this;
this.browserqueue = async.queue(function (task, done) {
_this.genReport(task.req, task.res, task.modelid, task.params, task.data, done);
}, 1);
};
AppSrvRpt.prototype.queueReport = function (req, res, fullmodelid, Q, P, params, onComplete) {
if(!params) params = {};
var thisapp = this.AppSrv;
var jsh = thisapp.jsh;
var _this = this;
var model = jsh.getModel(req, fullmodelid);
var db = params.db;
var dbcontext = params.dbcontext;
var errorHandler = function(num, txt, stats){ return Helper.GenError(req, res, num, txt, { stats: stats }); };
if(params.errorHandler) errorHandler = params.errorHandler;
if(req){
if (!Helper.hasModelAction(req, model, 'B')) { return errorHandler(-11, jsh._tP('Invalid Model Access for @fullmodelid', { fullmodelid })); }
db = db || jsh.getModelDB(req, fullmodelid);
dbcontext = dbcontext || jsh.getDBContext(req, model, db) || 'report';
}
else if(!db) throw new Error('Either req or db is required.');
//Validate Parameters
var fieldlist = thisapp.getFieldNames(req, model.fields, 'B');
_.map(fieldlist, function (field) { if (!(field in Q)) Q[field] = ''; });
if (!thisapp.ParamCheck('Q', Q, _.map(fieldlist, function (field) { return '&' + field; }).concat(_.map((req?_.keys(req.jshsite.datalock):[]), function(field) { return '|' + field; })))) { return errorHandler(-4, 'Invalid Parameters'); }
if (!thisapp.ParamCheck('P', P, [])) { return errorHandler(-4, 'Invalid Parameters'); }
if(req && !params.fromBatch) jsh.Log.info('REPORT: ' + req.originalUrl + ' ' + (req.user_id || '') + ' ' + (req.user_name || ''));
var sql_ptypes = [];
var sql_params = {};
var verrors = {};
var fields = thisapp.getFieldsByName(model.fields, fieldlist);
//if (fields.length == 0) return onComplete(null, null); //Commented to enable reports with no parameters
_.each(fields, function (field) {
var fname = field.name;
if (fname in Q) {
var dbtype = thisapp.getDBType(field);
sql_ptypes.push(dbtype);
sql_params[fname] = thisapp.DeformatParam(field, Q[fname], verrors);
}
else return errorHandler(-4, 'Missing parameter ' + fname);
});
verrors = _.merge(verrors, model.xvalidate.Validate('B', sql_params));
if (!_.isEmpty(verrors)) { return errorHandler(-2, verrors[''].join('\n')); }
if(model.batch && !params.fromBatch) return _this.batchReport(req, res, db, dbcontext, model, sql_ptypes, sql_params, verrors, errorHandler, Q, params, onComplete);
var dbtasks = {};
try{
this.parseReportSQLData(req, db, dbcontext, model, sql_ptypes, sql_params, verrors, dbtasks, model.reportdata);
}
catch(err){
jsh.Log.error(err);
return errorHandler(-99999, err.toString());
}
db.ExecTasks(dbtasks, function (err, dbdata, stats) {
if (err) {
if(jsh.Config.debug_params.report_debug) jsh.Log.debug(err);
if(err.sql) jsh.Log.error('Error running SQL in report '+fullmodelid+' :: '+err.sql);
return thisapp.AppDBError(req, res, err, stats, errorHandler);
}
if (dbdata == null) dbdata = {};
_this.MergeReportData(dbdata, model.reportdata, null);
if(params.output=='html'){
return onComplete(null,_this.genReportContent(req, res, fullmodelid, sql_params, dbdata));
}
else{
_this.browserqueue.push({ req: req, res: res, modelid: fullmodelid, params: sql_params, data: dbdata }, onComplete);
}
});
};
AppSrvRpt.prototype.batchReport = function (req, res, db, dbcontext, model, sql_ptypes, sql_params, verrors, errorHandler, Q, params, onComplete) {
var thisapp = this.AppSrv;
var jsh = thisapp.jsh;
if(!model.batch || !model.batch.sql) return errorHandler(-6, 'Batch reports require model.batch.sql');
if(model.format != 'pdf') return errorHandler(-6, 'Batch reports requires model.format "pdf"');
//Parameters should already be validated
//Add DataLock parameters to SQL
var datalockqueries = [];
if(req){
thisapp.getDataLockSQL(req, model, model.fields, sql_ptypes, sql_params, verrors, function (datalockquery) { datalockqueries.push(datalockquery); });
}
var sql = db.sql.runReportBatch(jsh, model, datalockqueries);
if(!req && (sql.indexOf('%%%DATALOCKS%%%')>=0)) throw new Error('Cannot use %%%DATALOCKS%%% in automated reports');
var dbtasks = {};
dbtasks['batchqueue'] = function (callback) {
db.Recordset(dbcontext, sql, sql_ptypes, sql_params, function (err, rslt, stats) {
if ((err == null) && (rslt == null)) err = Helper.NewError('Record not found', -1);
if (err != null) { err.model = model; err.sql = sql; }
if (stats) stats.model = model;
callback(err, rslt, stats);
});
};
db.ExecTasks(dbtasks, function (err, rslt, stats) {
if (err) { return thisapp.AppDBError(req, res, err, stats, errorHandler); }
if (rslt == null) rslt = {};
if(params.output=='html'){
//Generate Batch HTML
var rptrslt = [];
async.eachSeries(rslt.batchqueue, function(batchparams, cb){
batchparams = _.extend({}, Q, batchparams);
thisapp.rptsrv.queueReport(req, res, model.id, batchparams, {}, _.extend({}, params, { db: db, dbcontext: dbcontext, errorHandler: errorHandler, fromBatch: true}), function (err, rptcontent) {
if(err) return cb(err);
rptrslt.push(rptcontent);
return cb();
});
}, function(err){
if(err) return errorHandler(-99999, err);
return onComplete(null, rptrslt);
});
}
else {
//Generate Batch PDF
var report_folder = jsh.Config.datadir + 'temp/report/';
HelperFS.createFolderIfNotExists(report_folder, function (err) {
if (err) throw err;
HelperFS.clearFiles(report_folder, jsh.Config.public_temp_expiration, -1, function () {
tmp.file({ dir: report_folder }, function (batchtmperr, batchtmppath, batchtmpfd) {
if (batchtmperr) return errorHandler(-99999, batchtmperr);
var batchtmppdfpath = batchtmppath + '.pdf';
var pdfFiles = [];
var batchdbdata = [];
async.eachSeries(rslt.batchqueue, function(batchparams, cb){
batchparams = _.extend({}, Q, batchparams);
thisapp.rptsrv.queueReport(req, res, model.id, batchparams, {}, _.extend({}, params, { db: db, dbcontext: dbcontext, errorHandler: errorHandler, fromBatch: true}), function (err, tmppath, dispose, dbdata) {
if(err) return cb(err);
batchdbdata.push(dbdata);
/* Report Done */
HelperFS.getFileStats(req, res, tmppath, function (err, stat) {
if (err){ dispose(); return cb('Report file not found'); }
pdfFiles.push(tmppath);
jsh.Extensions.report.getPdfMerge(function(err, pdfMerge){
if(err) return cb(err);
pdfMerge(pdfFiles,{ output: batchtmppdfpath }).then(function(){
pdfFiles = [batchtmppdfpath];
dispose();
return cb(null);
});
});
});
});
}, function(err){
// *** Be sure to call dispose independently of the callback, because dispose may fail, but it should not be treated as a critical failure
var dispose = function(disposedone){
fs.close(batchtmpfd, function () {
fs.unlink(batchtmppath, function (err) {
if(disposedone) disposedone();
});
});
};
if(err) return errorHandler(-99999, err);
return onComplete(null, batchtmppdfpath, dispose, batchdbdata);
});
});
});
});
}
});
};
AppSrvRpt.prototype.parseReportSQLData = function (req, db, dbcontext, model, sql_ptypes, sql_params, verrors, dbtasks, rdata) {
var thisapp = this.AppSrv;
var jsh = thisapp.jsh;
var _this = this;
if(req){
db = db || jsh.getModelDB(req, model.id);
dbcontext = dbcontext || jsh.getDBContext(req, model, db) || 'report';
}
else if(!db) throw new Error('Either req or db is required.');
_.each(rdata, function (dparams, dname) {
if (!('sql' in dparams)) throw new Error(dname + ' missing sql');
var datalockqueries = [];
var skipdatalock = true;
if(req){
//Add DataLock parameters to SQL
thisapp.getDataLockSQL(req, model, model.fields, sql_ptypes, sql_params, verrors, function (datalockquery) { datalockqueries.push(datalockquery); }, dparams.nodatalock);
skipdatalock = false;
if ('nodatalock' in dparams) {
skipdatalock = true;
for (var datalockid in req.jshsite.datalock) {
if (Helper.arrayIndexOf(dparams.nodatalock,datalockid,{caseInsensitive:jsh.Config.system_settings.case_insensitive_datalocks}) < 0) skipdatalock = false;
}
}
}
var sql = db.sql.parseReportSQLData(jsh, dname, dparams, skipdatalock, datalockqueries);
if(!req && (sql.indexOf('%%%DATALOCKS%%%')>=0)) throw new Error('Cannot use %%%DATALOCKS%%% in automated reports');
dbtasks[dname] = function (callback) {
db.Recordset(dbcontext, sql, sql_ptypes, sql_params, function (err, rslt, stats) {
if ((err == null) && (rslt == null)) err = Helper.NewError('Record not found', -1);
if (err != null) { err.model = model; err.sql = sql; }
if (stats) stats.model = model;
callback(err, rslt, stats);
});
};
if ('children' in dparams) _this.parseReportSQLData(req, db, dbcontext, model, sql_ptypes, sql_params, verrors, dbtasks, dparams.children);
});
};
AppSrvRpt.prototype.MergeReportData = function (data, tree, parent) {
var _this = this;
_.each(tree, function (leaf, name) {
if ('children' in leaf) _this.MergeReportData(data, leaf.children, name);
//Post-order traversal
if (parent == null) return;
if (!(parent in data)) throw new Error('No parent result set found for ' + name);
if (!(name in data)) throw new Error('No result set found for ' + name);
var pdata = data[parent];
var cdata = data[name];
var bindings = leaf.bindings;
for (var i = 0; i < pdata.length; i++) {
var prow = pdata[i];
var pskip = false;
_.each(bindings, function (bparent, bchild) {
if (!(bparent in prow)) throw new Error('Parent result set ' + parent + ' missing binding ' + bparent);
if (prow[bparent] === null) pskip = true;
});
prow[name] = [];
if (pskip) continue;
for (var j = 0; j < cdata.length; j++) {
var crow = cdata[j];
var bmatch = true;
_.each(bindings, function (bparent, bchild) {
if (!(bchild in crow)) throw new Error('Child result set ' + name + ' missing binding ' + bchild);
if (crow[bchild] === null) bmatch = false;
else if (prow[bparent] !== crow[bchild]) bmatch = false;
});
if (bmatch) {
prow[name].push(crow);
cdata.splice(j, 1);
j--;
}
}
}
delete data[name];
});
//Parse tree bottom-up
// If leaf has no parent, return
// For each parent row
// Create array of CHILDNAME
// Add all records matching binding into CHILDNAME
// Delete leaf
};
AppSrvRpt.prototype.genReportContent = function(req, res, fullmodelid, params, data){
var rslt = { header: '', body: '', footer: '' };
var _this = this;
var jsh = _this.AppSrv.jsh;
var model = jsh.getModel(req, fullmodelid);
if(!model) throw new Error('Model '+fullmodelid+' not found');
if (model.layout !== 'report') throw new Error('Model '+fullmodelid+' is not a report');
var ejsbody_header = Helper.ParseMultiLine(model.pageheader||'');
var ejsbody_footer = Helper.ParseMultiLine(model.pagefooter||'');
var ejsbody = Helper.ParseMultiLine(model.reportbody||'');
if (!ejsbody && ((typeof model.reportbody == 'undefined') || (model.reportbody === null))){
ejsbody = 'REPORT BODY NOT FOUND';
}
if (jsh.Config.debug_params.report_debug) {
ejsbody = ejsbody.replace(/{{(.*?)}}/g, '<%=ejsext.null_log(jsh.Log,$1,\'$1\')%>');
ejsbody_header = ejsbody_header.replace(/{{(.*?)}}/g, '<%=ejsext.null_log(jsh.Log,$1,\'$1\')%>');
ejsbody_footer = ejsbody_footer.replace(/{{(.*?)}}/g, '<%=ejsext.null_log(jsh.Log,$1,\'$1\')%>');
}
else {
ejsbody = ejsbody.replace(/{{/g, '<%=ejsext.null_blank(');
ejsbody = ejsbody.replace(/}}/g, ')%>');
ejsbody_header = ejsbody_header.replace(/{{/g, '<%=ejsext.null_blank(');
ejsbody_header = ejsbody_header.replace(/}}/g, ')%>');
ejsbody_footer = ejsbody_footer.replace(/{{/g, '<%=ejsext.null_blank(');
ejsbody_footer = ejsbody_footer.replace(/}}/g, ')%>');
}
rslt.body = ejs.render(ejsbody, {
model: model,
moment: moment,
_this: _this,
jsh: jsh,
ejsext: ejsext,
data: data,
params: params,
_: _,
filename: model.path
});
if(ejsbody_header){
rslt.header = _this.RenderEJS(ejsbody_header, {
model: model,
moment: moment,
_this: _this,
jsh: jsh,
ejsext: ejsext,
data: data,
params: params,
_: _,
pageNum: "<span class='pageNumber'></span>",
numPages: "<span class='totalPages'></span>"
});
}
if(ejsbody_footer){
rslt.footer = _this.RenderEJS(ejsbody_footer, {
model: model,
moment: moment,
_this: _this,
jsh: jsh,
ejsext: ejsext,
data: data,
params: params,
_: _,
pageNum: "<span class='pageNumber'></span>",
numPages: "<span class='totalPages'></span>"
});
}
return rslt;
};
AppSrvRpt.prototype.genReportPdf = function (req, res, fullmodelid, params, data, tmppath, callback) {
var _this = this;
var jsh = _this.AppSrv.jsh;
var model = jsh.getModel(req, fullmodelid);
if(!model) throw new Error('Model '+fullmodelid+' not found');
if (model.layout !== 'report') throw new Error('Model '+fullmodelid+' is not a report');
_this.getBrowser(function (err, browser) {
var page = null;
function genReportError(err, source){
var rpterr = Helper.NewError('Error occurred during report generation'+(source ? ' - ' + source: '')+' (' + err.toString() + ')', -99999);
if (page != null){
return page.close()
.then(function(){ return callback(rpterr, null); })
.catch(function (err) { jsh.Log.error(err); return callback(rpterr, null); });
}
else return callback(rpterr, null);
}
if(err) return genReportError(err, 'getBrowser');
try {
browser.newPage().then(function (_page) {
var tmppdfpath = tmppath + '.pdf';
var tmphtmlpath = tmppath + '.html';
page = _page;
var pagesettings = {
format: 'Letter',
landscape: false,
printBrackground: true,
margin: {
top: '1cm',
bottom: '1cm',
left: '1cm',
right: '1cm',
}
};
var rptcontent = { header: '', body: '', footer: '' };
var contentWidth = 0;
var contentHeight = 0;
var font_css = '';
var font_render = [];
var cancelRender = false;
async.waterfall([
//model.onrender
function(rpt_cb){
if(!model.onrender) return rpt_cb();
model.onrender({ page: page, content: rptcontent, data: data, params: params }, function(err, _cancelRender){
cancelRender = _cancelRender;
return rpt_cb(err);
}, require, jsh, fullmodelid);
},
//Initialize report
function(rpt_cb){
if(cancelRender) return rpt_cb();
rptcontent = _this.genReportContent(req, res, fullmodelid, params, data);
if ('pagesettings' in model){
if(model.pagesettings.width && model.pagesettings.height) delete pagesettings.format;
pagesettings = _.extend(pagesettings, model.pagesettings);
}
pagesettings.path = tmppdfpath;
var dpi = 96;
var default_header = '1cm';//Math.floor(0.4 * dpi) + 'px';
var headerheight = 0;
var footerheight = 0;
if(rptcontent.header){
pagesettings.displayHeaderFooter = true;
pagesettings.headerTemplate = rptcontent.header;
headerheight = default_header;
if ('headerheight' in model) headerheight = model.headerheight;
}
if(rptcontent.footer){
pagesettings.displayHeaderFooter = true;
pagesettings.footerTemplate = rptcontent.footer;
footerheight = default_header;
if ('footerheight' in model) footerheight = model.footerheight;
}
if(pagesettings.displayHeaderFooter) pagesettings.footerTemplate = pagesettings.footerTemplate || ' ';
//page.set('viewportSize',{width:700,height:800},function(){
//Calculate page width
var marginLeft = 0;
var marginRight = 0;
var marginTop = 0;
var marginBottom = 0;
var pageWidth = 1; //px
var pageHeight = 1; //px
var headerHeightPx = 0;
var footerHeightPx = 0;
if(pagesettings.margin){
var basemargin = pagesettings.margin;
if(!basemargin) basemargin = 0;
if(_.isString(basemargin)) basemargin = parseUnitsPx(basemargin,dpi);
if(_.isNumber(basemargin)){
marginLeft = basemargin;
marginRight = basemargin;
marginTop = basemargin;
marginBottom = basemargin;
}
else {
if(basemargin.left) marginLeft = parseUnitsPx(basemargin.left,dpi);
if(basemargin.right) marginRight = parseUnitsPx(basemargin.right,dpi);
if(basemargin.top) marginTop = parseUnitsPx(basemargin.top,dpi);
if(basemargin.bottom) marginBottom = parseUnitsPx(basemargin.bottom,dpi);
}
}
if(pagesettings.format){
var fmt = pagesettings.format.toLowerCase();
var w = 1;
var h = 1;
//Width and height in millimeters
if(fmt=='a0'){ w = 841; h=1189; }
if(fmt=='a1'){ w = 594; h=841; }
if(fmt=='a2'){ w = 420; h=594; }
if(fmt=='a3'){ w = 297; h=420; }
else if(fmt=='a4'){ w=210; h=297; }
else if(fmt=='a5'){ w=148; h=210; }
else if(fmt=='a6'){ w=105; h=148; }
else if(fmt=='a7'){ w=74; h=105; }
else if(fmt=='legal'){ w=215.9; h=355.6; }
else if(fmt=='letter'){ w=215.9; h=279.4; }
else if(fmt=='tabloid'){ w=279.4; h=431.8; }
else if(fmt=='ledger'){ w=279.4; h=431.8; }
else return jsh.Log.error('Invalid report format: '+pagesettings.format);
if(pagesettings.landscape){
var _w = w;
w = h;
h = _w;
}
//Width and height in pixels
pageWidth = (w / (25.4)) * dpi;
pageHeight = (h / (25.4)) * dpi;
}
if(pagesettings.width) pageWidth = parseUnitsPx(pagesettings.width,dpi);
if(pagesettings.height) pageHeight = parseUnitsPx(pagesettings.height,dpi);
if(headerheight) headerHeightPx = parseUnitsPx(headerheight,dpi);
if(footerheight) footerHeightPx = parseUnitsPx(footerheight,dpi);
contentWidth = pageWidth - marginLeft - marginRight;
contentHeight = pageHeight - marginTop - marginBottom - headerHeightPx - footerHeightPx;
if (jsh.Config.debug_params.report_debug) { jsh.Log.debug('Calculated Page Size: '+contentHeight + 'x'+contentWidth); }
pagesettings.margin = {
top: (marginTop+headerHeightPx)+'px',
right: marginRight+'px',
bottom: (marginBottom+footerHeightPx)+'px',
left: marginLeft+'px',
};
if(pagesettings.headerTemplate){
pagesettings.headerTemplate = '<style type="text/css">#header{ padding:'+Math.round(marginTop*_HEADER_ZOOM)+'px '+Math.round(marginRight*_HEADER_ZOOM)+'px '+Math.round(marginBottom*_HEADER_ZOOM)+'px '+Math.round(marginLeft*_HEADER_ZOOM)+'px; -webkit-print-color-adjust: exact; }</style><div style="position:absolute;width:'+(contentWidth)+'px;font-size:12px;transform: scale('+_HEADER_ZOOM+'); transform-origin: top left;">'+pagesettings.headerTemplate+'</div>';
}
if(pagesettings.footerTemplate){
pagesettings.footerTemplate = '<style type="text/css">#footer{ padding:'+Math.round(marginTop*_HEADER_ZOOM)+'px '+Math.round(marginRight*_HEADER_ZOOM)+'px '+Math.round(marginBottom*_HEADER_ZOOM)+'px '+Math.round(marginLeft*_HEADER_ZOOM)+'px; -webkit-print-color-adjust: exact; }</style><div style="position:absolute;width:'+(contentWidth)+'px;font-size:12px;transform: scale('+_HEADER_ZOOM+'); transform-origin: bottom left;">'+pagesettings.footerTemplate+'</div>';
}
return rpt_cb();
},
//Load fonts
function(rpt_cb){
if(cancelRender) return rpt_cb();
var report_fonts = [].concat(jsh.Config.default_report_fonts||[]).concat(model.fonts||[]);
jsh.loadFonts(report_fonts, function(err, _font_css){
if(err) return jsh.Log.error(err);
font_css = _font_css;
for(var i=0;i<report_fonts.length;i++){
var font = report_fonts[i];
var font_str = '';
if(font['font-family']) font_str += "font-family:'"+Helper.escapeCSS(font['font-family'].toString())+"';";
if(font['font-style']) font_str += 'font-style:'+font['font-style'].toString()+';';
if(font['font-weight']) font_str += 'font-weight:'+font['font-weight'].toString()+';';
if(font_str) font_render.push(font_str);
}
if(font_css){
if(pagesettings.headerTemplate) pagesettings.headerTemplate = '<style type="text/css">'+font_css+'</style>' + pagesettings.headerTemplate;
if(pagesettings.footerTemplate) pagesettings.footerTemplate = '<style type="text/css">'+font_css+'</style>' + pagesettings.footerTemplate;
}
return rpt_cb();
});
},
//model.onrendered
function(rpt_cb){
if(!model.onrendered) return rpt_cb();
model.onrendered({ page: page, content: rptcontent, data: data, params: params }, function(err){
if(err) return genReportError(err, 'onrendered');
return rpt_cb();
}, require, jsh, fullmodelid);
},
//Render
function(rpt_cb){
if(cancelRender) return rpt_cb();
//Sets styles and returns document width
var onPageLoad = function(font_render,font_css){
if(!document||!document.body) return 0;
//Load CSS in header
var head = document.getElementsByTagName('head');
if(head.length) head = head[0];
if(head){
var css = document.createElement('style');
css.type='text/css';
css.innerHTML = font_css;
head.appendChild(css);
}
//Add fonts to body (otherwise using them in the page header / footer will cause a Page Crash)
if(font_render) for(var i=0;i<font_render.length;i++){
var fontElement = document.createElement('div');
fontElement.innerHTML = ' ';
fontElement.setAttribute('style', font_render[i]+'visibility:hidden;position:absolute;top:0px;left:0px;');
document.body.appendChild(fontElement);
}
document.body.style['-webkit-print-color-adjust'] = 'exact';
return document.body.clientWidth;
};
fs.writeFile(tmphtmlpath, rptcontent.body||'','utf8',function(err){
if(err) return jsh.Log.error(err);
page.goto('file://'+tmphtmlpath, { timeout: jsh.Config.report_timeout, waitUntil: 'networkidle0' })
.then(function(){
page.evaluate(onPageLoad, font_render, font_css).then(function(documentWidth){
if(documentWidth && !pagesettings.scale){
var scale = contentWidth * 0.998 / documentWidth;
if(scale < 0.1) scale = 0.1;
if(scale > 1) scale = 1;
pagesettings.scale = scale;
}
return rpt_cb();
}).catch(function (err) { genReportError(err, 'onPageLoad'); });
}).catch(function (err) { genReportError(err, 'page.goto'); });
});
},
//Save to PDF
function(rpt_cb){
setTimeout(function(){
page.pdf(pagesettings).then(function () {
page.close().then(function () {
page = null;
return rpt_cb();
}).catch(function (err) { jsh.Log.error(err); return rpt_cb(); });
}).catch(function (err) { genReportError(err, 'page.pdf'); });
}, (jsh.Config.debug_params.report_interactive ? 500000 : 0));
}
], function(err){
if(err) return genReportError(err);
return callback(null, tmppdfpath);
});
}).catch(function (err) { genReportError(err, 'browser.newPage'); });
} catch (err) {
genReportError(err);
}
}); //, { dnodeOpts: { weak: false } }
};
AppSrvRpt.prototype.genReportXlsx = function (req, res, fullmodelid, params, data, tmppath, callback) {
var _this = this;
var jsh = _this.AppSrv.jsh;
var model = jsh.getModel(req, fullmodelid);
if(!model) throw new Error('Model '+fullmodelid+' not found');
if (model.layout !== 'report') throw new Error('Model '+fullmodelid+' is not a report');
function genReportError(err, source){
var rpterr = Helper.NewError('Error occurred during report generation'+(source ? ' - ' + source: '')+' (' + err.toString() + ')', -99999);
jsh.Log.error(rpterr);
return callback(rpterr, null);
}
var rsltpath = tmppath + '.xlsx';
jsh.Extensions.report.getExcelJS(function(err, excelJS){
if(err) return genReportError(err);
try{
//Create workbook
var cancelRender = false;
var workbook = new excelJS.Workbook();
async.waterfall([
//model.onrender
function(rpt_cb){
if(!model.onrender) return rpt_cb();
model.onrender({ workbook: workbook, data: data, params: params }, function(err, _cancelRender){
cancelRender = _cancelRender;
return rpt_cb(err);
}, require, jsh, fullmodelid);
},
//Generate worksheets
function(rpt_cb){
if(cancelRender) return rpt_cb();
var sheetNames = [];
if(data) sheetNames = _.keys(data).reverse();
async.eachSeries(sheetNames, function(rsName, rs_cb){
var rsData = data[rsName];
try{
//Create worksheet
var worksheet = workbook.addWorksheet(rsName);
//Add Data
if(rsData && rsData.length){
var cols = _.keys(rsData[0]);
worksheet.addRow(cols);
_.each(rsData, function(row){ worksheet.addRow(_.values(row)); });
if(cols.length){
//AutoFilter
worksheet.autoFilter = {
from: { row: 1, column: 1 },
to: { row: 1, column: cols.length },
};
}
}
//AutoSize
_.each(worksheet.columns, function(col){
var maxlen = 10;
col.eachCell({ includeEmpty: true }, function(cell){
var len = 0;
if(_.isDate(cell.value)) len = 15;
else len = (cell.value||'').toString().length + 3;
if(len > 100) len = 100;
if(len > maxlen) maxlen = len;
});
col.width = maxlen;
});
}
catch(ex){
return genReportError(ex, 'Sheet '+rsName);
}
return rs_cb();
}, rpt_cb);
},
//model.onrendered
function(rpt_cb){
if(!model.onrendered) return rpt_cb();
model.onrendered({ workbook: workbook, data: data, params: params }, rpt_cb, require, jsh, fullmodelid);
},
//Save to disk
function(rpt_cb){
workbook.xlsx.writeFile(rsltpath)
.then(function(){ return rpt_cb(); })
.catch(function(err){ return rpt_cb(err); });
},
], function(err){
if(err) return genReportError(err);
return callback(null, rsltpath);
});
}
catch(ex){
return genReportError(ex);
}
});
};
AppSrvRpt.prototype.genReport = function (req, res, fullmodelid, params, data, done) {
var _this = this;
var jsh = _this.AppSrv.jsh;
var report_folder = jsh.Config.datadir + 'temp/report/';
var model = jsh.getModel(req, fullmodelid);
if(!model) throw new Error('Model '+fullmodelid+' not found');
if (model.layout !== 'report') throw new Error('Model '+fullmodelid+' is not a report');
HelperFS.createFolderIfNotExists(report_folder, function (err) {
if (err) throw err;
HelperFS.clearFiles(report_folder, jsh.Config.public_temp_expiration, -1, function () {
tmp.file({ dir: report_folder }, function (tmperr, tmppath, tmpfd) {
if (tmperr) throw tmperr;
var reportFunc = (model.format == 'xlsx' ? _this.genReportXlsx : _this.genReportPdf);
reportFunc.call(_this, req, res, fullmodelid, params, data, tmppath, function(err, rsltpath){
if(err) return done(err, null);
var dispose = function (disposedone) {
fs.close(tmpfd, function () {
fs.unlink(rsltpath, function (err) {
fs.unlink(tmppath, function (err) {
if (disposedone) disposedone();
});
});
});
};
return done(null, rsltpath, dispose, data);
});
});
});
});
};
function parseUnitsPx(val,dpi){
//mm,cm,in,px or no units = px
if(!val) return 0;
val = val.toLowerCase();
//Get value in px
if(val.indexOf('mm') >= 0){ val = Helper.ReplaceAll(val,'mm','').trim(); val = parseFloat(val) * dpi / 25.4; }
else if(val.indexOf('cm') >= 0){ val = Helper.ReplaceAll(val,'cm','').trim(); val = parseFloat(val) * dpi / 2.54; }
else if(val.indexOf('in') >= 0){ val = Helper.ReplaceAll(val,'in','').trim(); val = parseFloat(val) * dpi; }
else if(val.indexOf('px') >= 0){ val = Helper.ReplaceAll(val,'px','').trim(); val = parseFloat(val); }
if(isNaN(val)) return 0;
return Math.floor(val);
}
AppSrvRpt.prototype.RenderEJS = function (ejssrc, ejsdata) {
return ejs.render(ejssrc, _.extend(ejsdata, { _: _, ejsext: ejsext }));
};
AppSrvRpt.prototype.getBrowser = function (callback) {
var _this = this;
var jsh = _this.AppSrv.jsh;
if (_this.browser) {
//Recycle browser after _BROWSER_RECYCLE_COUNT uses
_this.browserreqcount++;
if (_this.browserreqcount >= _BROWSER_RECYCLE_COUNT) {
jsh.Log.info('Recycling Report Renderer');
var oldbrowser = _this.browser;
_this.browser = null;
return oldbrowser.close()
.then(function(){ return _this.getBrowser(callback); })
.catch(function(err){ var errmsg = 'Cound not exit report renderer: '+err.toString(); jsh.Log.error(errmsg); return callback(new Error(errmsg)); });
}
else return callback(null, _this.browser);
}
jsh.Log.info('Launching Report Renderer');
jsh.Extensions.report.getPuppeteer(function(err, puppeteer){
if(err){ jsh.Log.error(err.toString()); return callback(new Error(err.toString())); }
var launchParams = { ignoreHTTPSErrors: true };
if(jsh.Config.debug_params.report_interactive) launchParams.headless = false;
puppeteer.launch(launchParams)
.then(function(rslt){
_this.browser = rslt;
_this.browser.on('disconnected', function(){
_this.browser = null;
});
_this.browserreqcount = 0;
return callback(null, _this.browser);
})
.catch(function(err){ jsh.Log.error(err); return callback(err); });
});
};
AppSrvRpt.prototype.runReportJob = function (req, res, fullmodelid, Q, P, onComplete) {
var thisapp = this.AppSrv;
var jsh = thisapp.jsh;
var model = jsh.getModel(req, fullmodelid);
if(!model) throw new Error('Model '+fullmodelid+' not found');
if (!Helper.hasModelAction(req, model, 'B')) { Helper.GenError(req, res, -11, jsh._tP('Invalid Model Access for @fullmodelid', { fullmodelid })); return; }
if (!('jobqueue' in model)) throw new Error(fullmodelid + ' job queue not enabled');
if (!thisapp.JobProc) throw new Error('Job Processor not configured');
if (model.layout !== 'report') throw new Error('Model '+fullmodelid+' is not a report');
//Validate Parameters
var fieldlist = thisapp.getFieldNames(req, model.fields, 'B');
_.map(fieldlist, function (field) { if (!(field in Q)) Q[field] = ''; });
var Qfields = _.map(fieldlist, function (field) { return '&' + field; }).concat(_.map(_.keys(req.jshsite.datalock), function(field) { return '|' + field; }));
Qfields.push('|_test');
if (!thisapp.ParamCheck('Q', Q, Qfields)) { Helper.GenError(req, res, -4, 'Invalid Parameters'); return; }
if (!thisapp.ParamCheck('P', P, [])) { Helper.GenError(req, res, -4, 'Invalid Parameters'); return; }
var sql_ptypes = [];
var sql_params = {};
var verrors = {};
var db = jsh.getModelDB(req, fullmodelid);
var dbcontext = jsh.getDBContext(req, model, db);
var fields = thisapp.getFieldsByName(model.fields, fieldlist);
if (fields.length == 0) return onComplete(null, {});
_.each(fields, function (field) {
var fname = field.name;
if (fname in Q) {
var dbtype = thisapp.getDBType(field);
sql_ptypes.push(dbtype);
sql_params[fname] = thisapp.DeformatParam(field, Q[fname], verrors);
}
else throw new Error('Missing parameter ' + fname);
});
verrors = _.merge(verrors, model.xvalidate.Validate('B', sql_params));
if (!_.isEmpty(verrors)) { Helper.GenError(req, res, -2, verrors[''].join('\n')); return; }
if (!('sql' in model.jobqueue)) throw new Error(fullmodelid + ' missing job queue sql');
//Add DataLock parameters to SQL
var datalockqueries = [];
thisapp.getDataLockSQL(req, model, model.fields, sql_ptypes, sql_params, verrors, function (datalockquery) { datalockqueries.push(datalockquery); });
var sql = db.sql.runReportJob(jsh, model, datalockqueries);
var dbtasks = {};
dbtasks['jobqueue'] = function (callback) {
db.Recordset(dbcontext, sql, sql_ptypes, sql_params, function (err, rslt, stats) {
if ((err == null) && (rslt == null)) err = Helper.NewError('Record not found', -1);
if (err != null) { err.model = model; err.sql = sql; }
if (stats) stats.model = model;
callback(err, rslt, stats);
});
};
db.ExecTasks(dbtasks, function (err, rslt, stats) {
if (err != null) { thisapp.AppDBError(req, res, err, stats); return; }
if (rslt == null) rslt = {};
if (('_test' in Q) && (Q._test == 1)) {
for (let i = 0; i < rslt.jobqueue.length; i++) {
var reporturl = req.baseurl + '_d/_report/' + model.id + '/?';
let jrow = rslt.jobqueue[i];
let rparams = {};
let verrors = {};
//Add each parameter to url
_.each(fields, function (field) {
var fname = field.name;
if (fname in jrow) rparams[fname] = thisapp.DeformatParam(field, jrow[fname], verrors);
else rparams[fname] = sql_params[fname];
if (_.isDate(rparams[fname])) rparams[fname] = rparams[fname].toISOString();
});
verrors = _.merge(verrors, model.xvalidate.Validate('B', rparams));
if (!_.isEmpty(verrors)) { Helper.GenError(req, res, -99999, 'Error during job queue: ' + verrors[''].join('\n') + ' ' + JSON.stringify(rparams)); return; }
reporturl += querystring.stringify(rparams);
rslt.jobqueue[i] = _.merge({ 'Run Report': '<a href="' + reporturl + '" target="_blank">Run Report</a>' }, jrow);
}
res.send(Helper.renderTable(rslt.jobqueue));
return;
}
else {
var jobtasks = {};
for (let i = 0; i < rslt.jobqueue.length; i++) {
let jrow = rslt.jobqueue[i];
let rparams = {};
let verrors = {};
//Add each parameter to url
_.each(fields, function (field) {
var fname = field.name;
if (fname in jrow) rparams[fname] = thisapp.DeformatParam(field, jrow[fname], verrors);
else rparams[fname] = sql_params[fname];
if (_.isDate(rparams[fname])) rparams[fname] = rparams[fname].toISOString();
});
verrors = _.merge(verrors, model.xvalidate.Validate('B', rparams));
if (!_.isEmpty(verrors)) { Helper.GenError(req, res, -99999, 'Error during job queue: ' + verrors[''].join('\n') + ' ' + JSON.stringify(rparams)); return; }
if(!thisapp.JobProc.AddDBJob(req, res, jobtasks, i, jrow, model.id, rparams)) return;
}
thisapp.JobProc.db.ExecTransTasks(jobtasks, function (err, rslt, stats) {
if (err != null) { thisapp.AppDBError(req, res, err, stats); return; }
else rslt = { '_success': _.size(jobtasks) };
rslt['_stats'] = Helper.FormatStats(req, stats);
res.type('json');
res.send(JSON.stringify(rslt));
});
}
});
};
module.exports = AppSrvRpt;