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 75b1a73a..874cce36 100644 Binary files a/mods/themes/NU-MAYA/LASTCALL.ANS and b/mods/themes/NU-MAYA/LASTCALL.ANS differ