From 99ea870ebc783caf7e963f6fb248c2cce258b3a4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 26 Jul 2015 22:51:06 -0600 Subject: [PATCH] * @method for view properties. WIP... hacked in, needs cleaned up & DRY * Messing around with different approaches to last callers... --- core/database.js | 8 ++ core/menu_util.js | 1 + core/system_menu_method.js | 37 +++++- core/view.js | 2 +- core/view_controller.js | 57 ++++++++- mods/last_callers.js | 210 +++++++++++++++++++++++++++++-- mods/menu.json | 39 +++++- mods/themes/NU-MAYA/LASTCALL.ANS | Bin 1796 -> 1872 bytes 8 files changed, 337 insertions(+), 17 deletions(-) diff --git a/core/database.js b/core/database.js index 32719179..c0d3dc74 100644 --- a/core/database.js +++ b/core/database.js @@ -69,6 +69,14 @@ function createUserTables() { ' FOREIGN KEY(group_id) REFERENCES user_group(group_id) ON DELETE CASCADE' + ');' ); + + dbs.user.run( + 'CREATE TABLE IF NOT EXISTS user_login_history (' + + ' user_id INTEGER NOT NULL,' + + ' user_name VARCHAR NOT NULL,' + + ' timestamp DATETIME NOT NULL' + + ');' + ); } function createMessageBaseTables() { diff --git a/core/menu_util.js b/core/menu_util.js index 510cb6f0..97f3775d 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -162,6 +162,7 @@ function handleAction(client, formData, conf) { var actionAsset = asset.parseAsset(conf.action); assert(_.isObject(actionAsset)); + // :TODO: Most of this should be moved elsewhere .... DRY... function callModuleMenuMethod(path) { if('' === paths.extname(path)) { path += '.js'; diff --git a/core/system_menu_method.js b/core/system_menu_method.js index d70c8f8a..69258256 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -4,6 +4,7 @@ var theme = require('../core/theme.js'); //var Log = require('../core/logger.js').log; var ansi = require('../core/ansi_term.js'); +var userDb = require('./database.js').dbs.user; var async = require('async'); @@ -19,21 +20,49 @@ function login(callingMenu, formData, extraArgs) { client.gotoMenuModule( { name : callingMenu.menuConfig.fallback } ); } else { + var now = new Date(); + var user = callingMenu.client.user; + // use client.user so we can get correct case - client.log.info( { username : callingMenu.client.user.username }, 'Successful login'); + client.log.info( { username : user.username }, 'Successful login'); async.parallel( [ function loadThemeConfig(callback) { - theme.loadTheme(client.user.properties.theme_id, function themeLoaded(err, theme) { + theme.loadTheme(user.properties.theme_id, function themeLoaded(err, theme) { client.currentTheme = theme; callback(null); // always non-fatal }); }, - function recordLogin(callback) { - client.user.persistProperty('last_login_timestamp', new Date().toISOString(), function persisted(err) { + function recordLastLogin(callback) { + user.persistProperty('last_login_timestamp', now.toISOString(), function persisted(err) { callback(err); }); + }, + function recordLoginHistory(callback) { + userDb.run( + 'INSERT INTO user_login_history (user_id, user_name, timestamp) ' + + 'VALUES(?, ?, ?);', [ user.userId, user.username, now.toISOString() ], function inserted(err) { + callback(err); + }); + + + /* + userDb.run( + 'DELETE FROM last_caller ' + + 'WHERE id NOT IN (' + + ' SELECT id ' + + ' FROM last_caller ' + + ' ORDER BY timestamp DESC ' + + ' LIMIT 100);'); + + userDb.run( + 'DELETE FROM last_caller ' + + 'WHERE user_id IN (' + + ' SELECT user_id ' + + ' ORDER BY timestamp DESC ' + + 'LIMIT 1;') + */ } ], function complete(err, results) { diff --git a/core/view.js b/core/view.js index e55c3086..0ae3bd4a 100644 --- a/core/view.js +++ b/core/view.js @@ -185,7 +185,7 @@ View.prototype.setPropertyValue = function(propName, value) { case 'focus' : this.setFocus(value); break; case 'text' : - if('setText' in this) { + if('setText' in this) { this.setText(value); } break; diff --git a/core/view_controller.js b/core/view_controller.js index 2c42c140..f11aa869 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -131,9 +131,26 @@ function ViewController(options) { // :TODO: move this elsewhere this.setViewPropertiesFromMCIConf = function(view, conf) { + + var propAsset; + var propValue; + + function callModuleMethod(path) { + if('' === paths.extname(path)) { + path += '.js'; + } + + try { + var methodMod = require(path); + // :TODO: fix formData & extraArgs + return methodMod[propAsset.asset](self.client.currentMenuModule, {}, {} ); + } catch(e) { + self.client.log.error( { error : e.toString(), methodName : propAsset.asset }, 'Failed to execute asset method'); + } + } + for(var propName in conf) { - var propValue; - var propAsset = asset.getViewPropertyAsset(conf[propName]); + propAsset = asset.getViewPropertyAsset(conf[propName]); if(propAsset) { switch(propAsset.type) { case 'config' : @@ -142,6 +159,42 @@ function ViewController(options) { // :TODO: handle @art (e.g. text : @art ...) + case 'method' : + case 'systemMethod' : + if(_.isString(propAsset.location)) { + + } else { + if('systemMethod' === propAsset.type) { + // :TODO: + } else { + // local to current module + var currentModule = self.client.currentMenuModule; + if(_.isFunction(currentModule.menuMethods[propAsset.asset])) { + // :TODO: Fix formData & extraArgs... this all needs general processing + propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs); + } + } + } + break; + /*case 'method' : + case 'systemMethod' : + if(_.isString(actionAsset.location)) { + callModuleMenuMethod(paths.join(Config.paths.mods, actionAsset.location)); + } else { + if('systemMethod' === actionAsset.type) { + // :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. () + // :TODO: Probably better as system_method.js + callModuleMenuMethod(paths.join(__dirname, 'system_menu_method.js')); + } else { + // local to current module + var currentModule = client.currentMenuModule; + if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) { + currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs); + } + } + }*/ + break; + default : propValue = propValue = conf[propName]; break; diff --git a/mods/last_callers.js b/mods/last_callers.js index 53e540a7..a09d3f21 100644 --- a/mods/last_callers.js +++ b/mods/last_callers.js @@ -5,6 +5,12 @@ var MenuModule = require('../core/menu_module.js').MenuModule; var userDb = require('../core/database.js').dbs.user; var ViewController = require('../core/view_controller.js').ViewController; +var util = require('util'); +var moment = require('moment'); +var async = require('async'); +var assert = require('assert'); +var _ = require('lodash'); + exports.moduleInfo = { name : 'Last Callers', desc : 'Last 10 callers to the system', @@ -13,12 +19,199 @@ exports.moduleInfo = { exports.getModule = LastCallersModule; -function LastCallersModule(menuConfig) { - MenuModule.call(this, menuConfig); +// :TODO: +// * Order should be menu/theme defined +// * Text needs overflow defined (optional), e.g. "..." +// * Date/time format should default to theme short date + short time +// * + +function LastCallersModule(options) { + MenuModule.call(this, options); + + var self = this; + this.menuConfig = options.menuConfig; + + this.menuMethods = { + getLastCaller : function(formData, extraArgs) { + //console.log(self.lastCallers[self.lastCallerIndex]) + var lc = self.lastCallers[self.lastCallerIndex++]; + var when = moment(lc.timestamp).format(self.menuConfig.config.dateTimeFormat); + return util.format('%s %s %s %s', lc.name, lc.location, lc.affiliation, when); + } + }; } -require('util').inherits(LastCallersModule, MenuModule); +util.inherits(LastCallersModule, MenuModule); +/* +LastCallersModule.prototype.enter = function(client) { + LastCallersModule.super_.prototype.enter.call(this, client); + + var self = this; + self.lastCallers = []; + self.lastCallerIndex = 0; + + var userInfoStmt = userDb.prepare( + 'SELECT prop_name, prop_value ' + + 'FROM user_property ' + + 'WHERE user_id=? AND (prop_name=? OR prop_name=?);'); + + var caller; + + userDb.each( + 'SELECT user_id, user_name, timestamp ' + + 'FROM user_login_history ' + + 'ORDER BY timestamp DESC ' + + 'LIMIT 10;', + function userRows(err, userEntry) { + caller = { + who : userEntry.user_name, + when : userEntry.timestamp, + }; + + userInfoStmt.each( [ userEntry.user_id, 'location', 'affiliation' ], function propRow(err, propEntry) { + if(!err) { + caller[propEntry.prop_name] = propEntry.prop_value; + } + }, function complete(err) { + if(!err) { + self.lastCallers.push(caller); + } + }); + } + ); +}; +*/ + +/* +LastCallersModule.prototype.mciReady = function(mciData) { + LastCallersModule.super_.prototype.mciReady.call(this, mciData); + + // we do this so other modules can be both customized and still perform standard tasks + LastCallersModule.super_.prototype.standardMCIReadyHandler.call(this, mciData); +}; +*/ + +LastCallersModule.prototype.mciReady = function(mciData) { + LastCallersModule.super_.prototype.mciReady.call(this, mciData); + + var self = this; + var vc = self.viewControllers.lastCallers = new ViewController( { client : self.client } ); + var lc = []; + var count = _.size(mciData.menu) / 4; + + if(count < 1) { + // :TODO: Log me! + return; + } + + async.series( + [ + function loadFromConfig(callback) { + var loadOpts = { + callingMenu : self, + mciMap : mciData.menu, + noInput : true, + }; + + vc.loadFromMenuConfig(loadOpts, function startingViewReady(err) { + callback(err); + }); + }, + function fetchHistory(callback) { + userDb.each( + 'SELECT user_id, user_name, timestamp ' + + 'FROM user_login_history ' + + 'ORDER BY timestamp DESC ' + + 'LIMIT ' + count + ';', + function historyRow(err, histEntry) { + lc.push( { + userId : histEntry.user_id, + who : histEntry.user_name, + when : histEntry.timestamp, + } ); + }, + function complete(err, recCount) { + count = recCount; // adjust to retrieved + callback(err); + } + ); + }, + function fetchUserProperties(callback) { + async.each(lc, function callEntry(c, next) { + userDb.each( + 'SELECT prop_name, prop_value ' + + 'FROM user_property ' + + 'WHERE user_id=? AND (prop_name="location" OR prop_name="affiliation");', + [ c.userId ], + function propRow(err, propEntry) { + c[propEntry.prop_name] = propEntry.prop_value; + }, + function complete(err) { + next(); + } + ); + }, function complete(err) { + callback(err); + }); + }, + function createAndPopulateViews(callback) { + assert(lc.length === count); + + var rowsPerColumn = count / 4; + + // + // TL1...count = who + // TL... = location + // + var i; + var v; + for(i = 0; i < rowsPerColumn; ++i) { + v = vc.getView(i + 1); + v.setText(lc[i].who); + } + + for( ; i < rowsPerColumn * 2; ++i) { + v = vc.getView(i + 1); + v.setText(lc[i].location); + } + + // + + // 1..count/4 = who + // count/10 + + /* + var viewOpts = { + client : self.client, + }; + + var rowViewId = 1; + var v; + lc.forEach(function lcEntry(caller) { + v = vc.getView(rowViewId++); + + self.menuConfig.config.fields.forEach(function field(f) { + switch(f.name) { + case 'who' : + + } + }); + + v.setText(caller.who) + }); + */ + + } + ], + function complete(err) { + console.log(lc) + } + ); +}; + + +/* LastCallersModule.prototype.mciReady = function(mciData) { LastCallersModule.super_.prototype.mciReady.call(this, mciData); @@ -35,12 +228,10 @@ LastCallersModule.prototype.mciReady = function(mciData) { var caller; userDb.each( - 'SELECT u.id, u.user_name, up.prop_value ' + - 'FROM user u ' + - 'INNER JOIN user_property up ' + - 'ON u.id=up.user_id AND up.prop_name="last_login_timestamp" ' + - 'ORDER BY up.prop_value DESC' + - 'LIMIT 10;', + 'SELECT id, user_name, timestamp ' + + 'FROM user_last_login ' + + 'ORDER BY timestamp DESC ' + + 'LIMIT 10;', function userRows(err, userEntry) { caller = { name : userEntry.user_name }; @@ -85,3 +276,4 @@ LastCallersModule.prototype.mciReady = function(mciData) { } ); }; +*/ \ No newline at end of file diff --git a/mods/menu.json b/mods/menu.json index b178e311..b50682fb 100644 --- a/mods/menu.json +++ b/mods/menu.json @@ -210,7 +210,44 @@ "lastCallers" :{ "module" : "last_callers", "art" : "LASTCALL.ANS", - "options" : { "cls" : true } + "options" : { "cls" : true }, + "config" : { + "dateTimeFormat" : "ddd MMM Do H:mm a", + "fields" : [ + { + "name" : "who", + "width" : 17 + }, + { + "name" : "location", + "width" : 20 + }, + { + "name" : "affiliation", + "width" : 20 + }, + { + "name" : "when", + "width" : 20 + } + ] + + }, + "form" : { + "0" : { + "TLTLTLTLTLTLTLTLTLTLTLTLTLTLTLTLTLTLTLTL" : { + "mci" : { + "TL1" : { + //"text" : "@method:getLastCaller" + }, + "TL2" : { + //"text" : "@method:getLastCaller" + } + } + } + } + } + }, "demoMain" : { "art" : "demo_selection_vm.ans", diff --git a/mods/themes/NU-MAYA/LASTCALL.ANS b/mods/themes/NU-MAYA/LASTCALL.ANS index 75b1a73a7d80c97593515b20fc31961bd7321158..874cce36e8495d00c244baffc59116edfa985695 100644 GIT binary patch delta 427 zcmZwDF$#k)6b4`lA|mJoT(US6IwW~&G>%;z-JA*9eR&?gI=XC@d-t; zz5L(*-jHXyq_2Aral=?Ph<1Ji#|V^JAMfBaWy5A5jMfMM=61b|I0(bE#zO1J?gKYl zz;JK@(@C7f_CD6~z7zH&8k}8W&4g_r${kK_Lz2Qy9RgwD!-BB%VM$o|uyQ!{o${%o i)!K(OVdKMwP;gTD_YaaM@fsHjV-Ry1E{D(j4f7w@4r5mU delta 349 zcmX}oJr2S!3|Ojz9+k!23?JP(JR-X^-c~`pQTw^6v}BD%gdRRV?OzUePK~(us>^& zM#L;)MOu+=5vqKLkU|JHp<+SpTT@hoM{PvS|0?n$wt}4?V?sct1sSV{SjL2iwIH^S jhymsp-fBhhf~c4fuIegFm8-4)uxgmEoA2r|@3-X-EbUC>