diff --git a/core/menu_module.js b/core/menu_module.js index aaec5646..22462df4 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -7,7 +7,6 @@ var art = require('./art.js'); var Log = require('./logger.js').log; var ansi = require('./ansi_term.js'); var asset = require('./asset.js'); -//var promptUtil = require('./prompt_util.js'); var ViewController = require('./view_controller.js').ViewController; var async = require('async'); diff --git a/core/menu_util.js b/core/menu_util.js index 66ebd4e7..b705a157 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -17,8 +17,8 @@ var _ = require('lodash'); var stripJsonComments = require('strip-json-comments'); -exports.loadMenu = loadMenu; -exports.getFormConfig = getFormConfig; +exports.loadMenu = loadMenu; +exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap; function loadModJSON(fileName, cb) { @@ -181,7 +181,7 @@ function loadMenu2(options, cb) { ); } -function getFormConfig(menuConfig, formId, mciMap, cb) { +function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { assert(_.isObject(menuConfig)); if(!_.isObject(menuConfig.form)) { diff --git a/core/view_controller.js b/core/view_controller.js index b1e6e8cf..6cd66dc1 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -25,11 +25,12 @@ function ViewController(options) { events.EventEmitter.call(this); - var self = this; + var self = this; - this.client = options.client; - this.views = {}; // map of ID -> view - this.formId = options.formId || 0; + this.client = options.client; + this.views = {}; // map of ID -> view + this.formId = options.formId || 0; + this.mciViewFactory = new MCIViewFactory(this.client); this.onClientKeyPress = function(key, isSpecial) { if(isSpecial) { @@ -108,6 +109,14 @@ function ViewController(options) { self.emit('submit', formData); }; + this.getLogFriendlyFormData = function(formData) { + var safeFormData = _.cloneDeep(formData); + if(safeFormData.value.password) { + safeFormData.value.password = '*****'; + } + return safeFormData; + }; + this.switchFocusEvent = function(event, view) { if(self.emitSwitchFocus) { return; @@ -154,6 +163,80 @@ function ViewController(options) { } }; + this.createViewsFromMCI = function(mciMap, cb) { + async.each(Object.keys(mciMap), function entry(name, nextItem) { + var mci = mciMap[name]; + var view = self.mciViewFactory.createFromMCI(mci); + + if(view) { + view.on('action', self.viewActionListener); + + self.addView(view); + + view.redraw(); // :TODO: fix double-redraw if this is the item we set focus to! + } + + nextItem(null); + }, + function complete(err) { + self.setViewOrder(); + cb(err); + }); + }; + + this.setViewPropertiesFromMCIConf = function(view, conf) { + view.submit = conf.submit || false; + + if(_.isArray(conf.items)) { + view.setItems(conf.items); + } + + if(_.isString(conf.text)) { + view.setText(conf.text); + } + + if(_.isString(conf.argName)) { + view.submitArgName = conf.argName; + } + }; + + this.applyViewConfig = function(config, cb) { + var highestId = 1; + var submitId; + var initialFocusId = 1; + + async.each(Object.keys(config.mci), function entry(mci, nextItem) { + var mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs???? + + var viewId = parseInt(mciMatch[2]); + assert(!isNaN(viewId)); + + var view = self.getView(viewId); + var mciConf = config.mci[mci]; + + self.setViewPropertiesFromMCIConf(view, mciConf); + + if(mciConf.focus) { + initialFocusId = viewId; + } + + if(view.submit) { + submitId = viewId; + } + + nextItem(null); + }, + function complete(err) { + + // default to highest ID if no 'submit' entry present + if(!submitId) { + self.getView(highestId).submit = true; + } + + cb(err, { initialFocusId : initialFocusId } ); + }); + }; + this.attachClientEvents(); } @@ -271,25 +354,8 @@ ViewController.prototype.loadFromMCIMap = function(mciMap) { }); }; -function setViewPropertiesFromMCIConf(view, conf) { - view.submit = conf.submit || false; - - if(_.isArray(conf.items)) { - view.setItems(conf.items); - } - - if(_.isString(conf.text)) { - view.setText(conf.text); - } - - if(_.isString(conf.argName)) { - view.submitArgName = conf.argName; - } -} - -ViewController.prototype.loadFromPrompt = function(options, cb) { +ViewController.prototype.loadFromPromptConfig = function(options, cb) { assert(_.isObject(options)); - //assert(_.isObject(options.promptConfig)); assert(_.isObject(options.callingMenu)); assert(_.isObject(options.callingMenu.menuConfig)); assert(_.isObject(options.callingMenu.menuConfig.promptConfig)); @@ -297,172 +363,166 @@ ViewController.prototype.loadFromPrompt = function(options, cb) { var promptConfig = options.callingMenu.menuConfig.promptConfig; var self = this; - var factory = new MCIViewFactory(this.client); var initialFocusId = 1; // default to first - // :TODO: if 'submit' is not present anywhere default to last ID - async.waterfall( [ function createViewsFromMCI(callback) { - async.each(Object.keys(options.mciMap), function entry(name, nextItem) { - var mci = options.mciMap[name]; - var view = factory.createFromMCI(mci); - - if(view) { - view.on('action', self.viewActionListener); - - self.addView(view); - - view.redraw(); // :TODO: fix double-redraw if this is the item we set focus to! - } - - nextItem(null); - }, - function complete(err) { - self.setViewOrder(); + self.createViewsFromMCI(options.mciMap, function viewsCreated(err) { callback(err); }); }, - function applyPromptConfig(callback) { - var highestId = 1; - var submitId; - - async.each(Object.keys(promptConfig.mci), function entry(mci, nextItem) { - var mciMatch = mci.match(MCI_REGEXP); // :TODO: what about auto-generated IDs? Do they simply not apply to menu configs? - - var viewId = parseInt(mciMatch[2]); - assert(!isNaN(viewId)); - - var view = self.getView(viewId); - var mciConf = promptConfig.mci[mci]; - - setViewPropertiesFromMCIConf(view, mciConf); - - if(mciConf.focus) { - initialFocusId = viewId; - } - - if(view.submit) { - submitId = viewId; - } - - nextItem(null); - }, - function complete(err) { - - // default to highest ID if no 'submit' entry present - if(!submitId) { - self.getView(highestId).submit = true; - } - + function applyViewConfiguration(callback) { + self.applyViewConfig(promptConfig, function configApplied(err, info) { + initialFocusId = info.initialFocusId; callback(err); - }); + }); }, - function setupSubmit(callback) { + function prepareFormSubmission(callback) { self.on('submit', function promptSubmit(formData) { - // :TODO: Need to come up with a way to log without dumping sensitive form data here, e.g. remove password, etc. - Log.trace( { formData : formData }, 'Prompt submit'); + Log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Prompt submit'); - var actionAsset = asset.parseAsset(promptConfig.action); - assert(_.isObject(actionAsset)); - - var extraArgs; - if(promptConfig.extraArgs) { - extraArgs = self.formatMenuArgs(promptConfig.extraArgs); - } - - - self.handleSubmitAction(options.callingMenu, formData, promptConfig); - - - - - /*var formattedArgs; - if(conf.args) { - formattedArgs = self.formatMenuArgs(conf.args); - } - - var actionAsset = asset.parseAsset(conf.action); - assert(_.isObject(actionAsset)); - - if('method' === actionAsset.type) { - if(actionAsset.location) { - // :TODO: call with (client, args, ...) at least. - } else { - // local to current module - var currentMod = self.client.currentMenuModule; - if(currentMod.menuMethods[actionAsset.asset]) { - currentMod.menuMethods[actionAsset.asset](formattedArgs); - } - } - } else if('menu' === actionAsset.type) { - self.client.gotoMenuModule( { name : actionAsset.asset, args : formattedArgs } ); - }*/ + self.handleSubmitAction(options.callingMenu, formData, options.callingMenu.menuConfig); }); callback(null); }, - function setInitialFocus(callback) { + function setInitialViewFocus(callback) { if(initialFocusId) { self.switchFocus(initialFocusId); } + callback(null); } ], function complete(err) { - console.log(err) cb(err); } ); }; -/* -ViewController.prototype.loadFromPrompt = function(options, cb) { +ViewController.prototype.loadFromMenuConfig = function(options, cb) { assert(_.isObject(options)); - assert(_.isObject(options.prompt)); - assert(_.isObject(options.prompt.artInfo)); - assert(_.isObject(options.prompt.artInfo.mciMap)); - assert(_.isObject(options.prompt.config)); + assert(_.isObject(options.callingMenu)); + assert(_.isObject(options.callingMenu.menuConfig)); + assert(_.isObject(options.mciMap)); + var self = this; + var formIdKey = options.formId ? options.formId.toString() : '0'; + var initialFocusId = 1; // default to first + var formConfig; - // - // Prompts are like simplified forms: - // * They do not contain submit information themselves; this must - // the owning menu: options.prompt.config - // * There is only one form in a prompt (e.g. form 0, but this is not explicit) - // * Only one MCI mapping: options.prompt.artInfo.mciMap - // - var self = this; - var factory = new MCIViewFactory(this.client); - var mciMap = options.prompt.artInfo.mciMap; + // :TODO: honor options.withoutForm + + // method for comparing submitted form data to configuration entries + var actionBlockValueComparator = function(formValue, actionValue) { + // + // Any key(s) in actionValue must: + // 1) Be present in formValue + // 2) Either: + // a) Be set to null (wildcard/any) + // b) Have matching value(s) + // + var keys = Object.keys(actionValue); + for(var i = 0; i < keys.length; ++i) { + var name = keys[i]; + + // submit data contains config key? + if(!_.has(formValue, name)) { + return false; // not present in what was submitted + } + + if(null !== actionValue[name] && actionValue[name] !== formValue[name]) { + return false; + } + } + + return true; + }; async.waterfall( [ - function createViewsFromMCI(callback) { - async.each(Object.keys(mciMap), function mciEntry(name, nextItem) { - var mci = mciMap[name]; - var view = factory.createFromMCI(mci); + function findMatchingFormConfig(callback) { + menuUtil.getFormConfigByIDAndMap(options.callingMenu.menuConfig, formIdKey, options.mciMap, function matchingConfig(err, fc) { + formConfig = fc; - if(view) { - self.addView(view); - view.redraw(); // :TODO: fix double-redraw if this is the item we set focus to! + if(err) { + // non-fatal + Log.trace( + { error : err, mci : Object.keys(options.mciMap), formId : formIdKey }, + 'Unable to find matching form configuration'); } - nextItem(null); - }, - function mciComplete(err) { + callback(null); + }); + }, + function createViews(callback) { + self.createViewsFromMCI(options.mciMap, function viewsCreated(err) { callback(err); }); + }, + function applyViewConfiguration(callback) { + if(_.isObject(formConfig)) { + self.applyViewConfig(formConfig, function configApplied(err, info) { + initialFocusId = info.initialFocusId; + callback(err); + }); + } else { + callback(null); + } + }, + function prepareFormSubmission(callback) { + if(!_.isObject(formConfig) || !_.isObject(formConfig.submit)) { + callback(null); + return; + } + + self.on('submit', function formSubmit(formData) { + Log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Form submit'); + + // + // Locate configuration for this form ID + // + var confForFormId; + if(_.isObject(formConfig.submit[formData.submitId])) { + confForFormId = formConfig.submit[formData.submitId]; + } else if(_.isObject(formConfig.submit['*'])) { + confForFormId = formConfig.submit['*']; + } else { + // no configuration for this submitId + Log.debug( { formId : formData.submitId }, 'No configuration for form ID'); + return; + } + + // + // Locate a matching action block based on the submitted data + // + for(var c = 0; c < confForFormId.length; ++c) { + var actionBlock = confForFormId[c]; + + if(_.isEqual(formData.value, actionBlock.value, actionBlockValueComparator)) { + self.handleSubmitAction(options.callingMenu, formData, actionBlock); + break; // there an only be one... + } + } + }); + + callback(null); + }, + function setInitialViewFocus(callback) { + if(initialFocusId) { + self.switchFocus(initialFocusId); + } + callback(null); } ], - function compelte(err) { - cb(err); + function complete(err) { + if(_.isFunction(cb)) { + cb(err); + } } ); - }; -*/ ViewController.prototype.loadFromMCIMapAndConfig = function(options, cb) { assert(options.mciMap); @@ -481,7 +541,7 @@ ViewController.prototype.loadFromMCIMapAndConfig = function(options, cb) { async.waterfall( [ function getFormConfig(callback) { - menuUtil.getFormConfig(options.menuConfig, formIdKey, options.mciMap, function onFormConfig(err, fc) { + menuUtil.getFormConfigByIDAndMap(options.menuConfig, formIdKey, options.mciMap, function onFormConfig(err, fc) { formConfig = fc; if(err) { @@ -494,21 +554,9 @@ ViewController.prototype.loadFromMCIMapAndConfig = function(options, cb) { callback(null); }); }, - function createViewsFromMCI(callback) { - async.each(Object.keys(options.mciMap), function onMciEntry(name, eachCb) { - var mci = options.mciMap[name]; - var view = factory.createFromMCI(mci); - - if(view) { - view.on('action', self.viewActionListener); - self.addView(view); - view.redraw(); // :TODO: This can result in double redraw() if we set focus on this item after - } - eachCb(null); - }, - function eachMciComplete(err) { - self.setViewOrder(); - callback(err); + function createViews(callback) { + self.createViewsFromMCI(options.mciMap, function viewsCreated(err) { + callback(err); }); }, function applyFormConfig(callback) { diff --git a/mods/apply.js b/mods/apply.js index b5f7629e..fa702a4e 100644 --- a/mods/apply.js +++ b/mods/apply.js @@ -30,13 +30,13 @@ function ApplyModule(menuConfig) { var self = this; - this.menuMethods.submitApplication = function(args) { + this.menuMethods.submitApplication = function(formData, extraArgs) { var usernameView = self.viewController.getView(1); var passwordView = self.viewController.getView(9); var pwConfirmView = self.viewController.getView(10); var statusView = self.viewController.getView(11); - self.validateApplication(args, function validated(errString, clearFields) { + self.validateApplication(formData, function validated(errString, clearFields) { if(errString) { statusView.setText(errString); @@ -47,15 +47,17 @@ function ApplyModule(menuConfig) { self.viewController.switchFocus(clearFields[0]); } else { var newUser = new user.User(); - newUser.username = args.username; + newUser.username = formData.value.username; + newUser.properties = { - real_name : args.realName, - age : args.age, - sex : args.sex, - location : args.location, - affiliation : args.affils, - email_address : args.email, - web_address : args.web, + real_name : formData.value.realName, + age : formData.value.age, + sex : formData.value.sex, + location : formData.value.location, + affiliation : formData.value.affils, + email_address : formData.value.email, + web_address : formData.value.web, + art_theme_id : Config.defaults.theme, // :TODO: allow '*' = random account_status : user.User.AccountStatus.inactive, @@ -64,16 +66,16 @@ function ApplyModule(menuConfig) { // :TODO: set account_status to default based on Config.user... }; - newUser.create({ password : args.pw }, function created(err) { + newUser.create({ password : formData.value.pw }, function created(err) { if(err) { - self.client.gotoMenuModule( { name : args.next.error } ); + self.client.gotoMenuModule( { name : extraArgs.error } ); } else { - Log.info( { username : args.username, userId : newUser.userId }, 'New user created'); + Log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created'); if(user.User.AccountStatus.inactive === self.client.user.properties.account_status) { - self.client.gotoMenuModule( { name : args.next.inactive } ); + self.client.gotoMenuModule( { name : extraArgs.inactive } ); } else { - self.client.gotoMenuModule( { name : args.next.active } ); + self.client.gotoMenuModule( { name : this.menuConfig.next } ); } } }); @@ -81,34 +83,34 @@ function ApplyModule(menuConfig) { }); }; - this.validateApplication = function(args, cb) { - if(args.username.length < Config.users.usernameMin) { + this.validateApplication = function(formData, cb) { + if(formData.value.username.length < Config.users.usernameMin) { cb('Handle too short!', [ 1 ]); return; } - if(args.username.length > Config.users.usernameMax) { + if(formData.value.username.length > Config.users.usernameMax) { cb('Handle too long!', [ 1 ]); return; } var re = new RegExp(Config.users.usernamePattern); - if(!re.test(args.username)) { + if(!re.test(formData.value.username)) { cb('Handle contains invalid characters!', [ 1 ] ); return; } - if(args.pw.length < Config.users.passwordMin) { + if(formData.value.pw.length < Config.users.passwordMin) { cb('Password too short!', [ 9, 10 ]); return; } - if(args.pw !== args.pwConfirm) { + if(formData.value.pw !== formData.value.pwConfirm) { cb('Passwords do not match!', [ 9, 10 ]); return; } - user.getUserIdAndName(args.username, function userIdAndName(err) { + user.getUserIdAndName(formData.value.username, function userIdAndName(err) { var alreadyExists = !err; if(alreadyExists) { cb('Username unavailable!', [ 1 ] ); @@ -129,13 +131,13 @@ ApplyModule.prototype.beforeArt = function() { ApplyModule.super_.prototype.beforeArt.call(this); }; -ApplyModule.prototype.mciReady = function(mciMaps) { - ApplyModule.super_.prototype.mciReady.call(this, mciMaps); +ApplyModule.prototype.mciReady = function(mciData) { + ApplyModule.super_.prototype.mciReady.call(this, mciData); var self = this; self.viewController = self.addViewController(new ViewController({ client : self.client } )); - self.viewController.loadFromMCIMapAndConfig( { mciMap : mciMaps.menu, menuConfig : self.menuConfig }, function onViewReady(err) { + self.viewController.loadFromMCIMapAndConfig( { mciMap : mciData.menu, menuConfig : self.menuConfig }, function onViewReady(err) { }); }; \ No newline at end of file diff --git a/mods/login.js b/mods/login.js index 91925f2d..b2f2daba 100644 --- a/mods/login.js +++ b/mods/login.js @@ -13,13 +13,13 @@ var async = require('async'); // :TODO: clean up requires -exports.moduleInfo = { +/*exports.moduleInfo = { name : 'Login', desc : 'Login Module', author : 'NuSkooler', -}; +};*/ -exports.getModule = LoginModule; +//exports.getModule = LoginModule; exports.attemptLogin = attemptLogin; @@ -52,7 +52,7 @@ function attemptLogin(callingMenu, formData, extraArgs) { }); } - +/* function LoginModule(menuConfig) { MenuModule.call(this, menuConfig); @@ -130,4 +130,5 @@ LoginModule.prototype.mciReady = function(mciData) { self.viewController = self.addViewController(new ViewController( { client : self.client } )); self.viewController.loadFromMCIMapAndConfig( { mciMap : mciData.menu, menuConfig : self.menuConfig }, function onViewReady(err) { }); -}; \ No newline at end of file +}; +*/ \ No newline at end of file diff --git a/mods/menu.json b/mods/menu.json index 323d8142..bdb4d10d 100644 --- a/mods/menu.json +++ b/mods/menu.json @@ -66,59 +66,11 @@ } }, "login" : { - //"art" : "login", // TODO: rename to login_form + // :TODO: may want { "prompt" : { "name" : "blah", "action" : ... }} "prompt" : "userCredentials", "fallback" : "matrix", "next" : "newUserActive", - //"module" : "login", - /* - "form" : { - "0" : { - "BT3BT4ET1ET2TL5" :{ - "mci" :{ - // :TODO: LIke prompts, assign "argName" values here, e.g.: - // "argName" : "username", ... - "ET1" : { - "focus" : true - }, - "BT3" : { - "submit" : true, - "text" : "Login" - }, - "BT4" : { - "submit" : true, - "text" : "Cancel" - } - }, - "submit" : { - "3" : [ // Login - { - "value" : { "3" : null }, - "action" : "@method:attemptLogin", - // :TODO: see above about argName; - // any other args should be "extraArgs" - "args" : { - "next" : { - // :TODO: just use menu.next - "success" : "newUserActive" - }, - "username" : "{1}", - "password" : "{2}" - } - } - ], - "4" : [ // Cancel - { - "value" : { "4" : null }, - "action" : "@menu:matrix" - // :TODO: Just use menu.fallback, e.g. @fallback - } - ] - } - } - } - }, - */ + "action" : "@method:login.js/attemptLogin", "options" : { "clearScreen" : true } @@ -130,13 +82,24 @@ "apply" : { "art" : "apply", "module" : "apply", + "next" : "newUserActive", "form" : { "0" : { "BT12BT13ET1ET10ET2ET3ET4ET5ET6ET7ET8ET9TL11" : { "mci" : { "ET1" : { - "focus" : true + "focus" : true, + "argName" : "username" }, + "ET2" : { "argName" : "realName" }, + "ET3" : { "argName" : "age" }, + "ET4" : { "argName" : "sex" }, + "ET5" : { "argName" : "location" }, + "ET6" : { "argName" : "affils" }, + "ET7" : { "argName" : "email" }, + "ET8" : { "argName" : "web" }, + "ET9" : { "argName" : "pw" }, + "ET10" : { "argName" : "pwConfirm" }, "BT12" : { "submit" : true, "text" : "Apply" @@ -151,23 +114,10 @@ { "value" : { "12" : null }, "action" : "@method:submitApplication", - "args" : { - "next" : { - "inactive" : "userNeedsActivated", - "active" : "newUserActive", - "error" : "newUserCreateError" - }, - "username" : "{1}", - "realName" : "{2}", - "age" : "{3}", - "sex" : "{4}", - "location" : "{5}", - "affils" : "{6}", - "email" : "{7}", - "web" : "{8}", - "pw" : "{9}", - "pwConfirm" : "{10}" - } + "extraArgs" : { + "inactive" : "userNeedsActivated", + "error" : "newUserCreateError" + } } ], "13" : [ // Cancel diff --git a/mods/standard_menu.js b/mods/standard_menu.js index 69249c85..bd186e33 100644 --- a/mods/standard_menu.js +++ b/mods/standard_menu.js @@ -6,6 +6,8 @@ var MenuModule = require('../core/menu_module.js').MenuModule; var ViewController = require('../core/view_controller.js').ViewController; var menuUtil = require('../core/menu_util.js'); +var _ = require('lodash'); + exports.getModule = StandardMenuModule; exports.moduleInfo = { @@ -40,6 +42,7 @@ StandardMenuModule.prototype.mciReady = function(mciData) { // * Prompt form is favored over menu form if both are present. // * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve) // + // :TODO: Create MenuModule.standardMciReady() method that others can call that does this -- even custom modules will generally want most of this self.viewControllers = {}; var vcOpts = { client : self.client }; @@ -60,21 +63,21 @@ StandardMenuModule.prototype.mciReady = function(mciData) { if(self.viewControllers.menu) { var menuLoadOpts = { mciMap : mciData.menu, - menuConfig : self.menuConfig, - withForm : !mciData.prompt, + callingMenu : self, + //menuConfig : self.menuConfig, + withoutForm : _.isObject(mciData.prompt), }; - self.viewControllers.menu.loadFromMCIMapAndConfig(menuLoadOpts, viewsReady); + self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, viewsReady); } if(self.viewControllers.prompt) { var promptLoadOpts = { callingMenu : self, mciMap : mciData.prompt, - //promptConfig : self.menuConfig.promptConfig, }; - self.viewControllers.prompt.loadFromPrompt(promptLoadOpts, viewsReady); + self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, viewsReady); } /*