| Line | Hits | Source |
|---|---|---|
| 1 | /** | |
| 2 | * Configuration library | |
| 3 | */ | |
| 4 | ||
| 5 | 1 | var rootpath = process.cwd() + '/', |
| 6 | path = require('path'), | |
| 7 | step = require('step'), | |
| 8 | _ = require('underscore'), | |
| 9 | fs = require('fs'); | |
| 10 | ||
| 11 | /** | |
| 12 | * Config object, wrapper for nconf | |
| 13 | * @type | |
| 14 | * @options | |
| 15 | */ | |
| 16 | ||
| 17 | 1 | function Configuration(options) { |
| 18 | ||
| 19 | // Defaults | |
| 20 | 11 | this.type = options && options.type ? options.type : 'file'; |
| 21 | 11 | this.env = options && options.env ? options.env : (process.env.NODE_ENV || 'development'); |
| 22 | 11 | this.path = options && options.path ? options.path : path.join(rootpath, 'conf'); |
| 23 | 11 | this.defaultConfig = options && options.defaultConfig ? options.defaultConfig : path.join(rootpath, 'lib', 'conf', 'default.json'); |
| 24 | 11 | this.file = path.join(this.path, this.env + '.json'); |
| 25 | ||
| 26 | // Track if changes have been made to config | |
| 27 | 11 | this.dirty = false; |
| 28 | ||
| 29 | } | |
| 30 | ||
| 31 | 1 | Configuration.prototype.init = function(next) { |
| 32 | 10 | if (typeof next !== 'function') next = function(err) { |
| 33 | 0 | if (err) console.error(err.message) |
| 34 | }; | |
| 35 | 10 | var Provider = require('nconf').Provider; |
| 36 | 10 | this.nconf = new Provider(); |
| 37 | 10 | this.load(next); |
| 38 | } | |
| 39 | ||
| 40 | /** | |
| 41 | * Check to see if configuration for this environment | |
| 42 | * doesn't exist, if so, it loads the default from default.json. | |
| 43 | */ | |
| 44 | 1 | Configuration.prototype.check = function() { |
| 45 | ||
| 46 | 11 | if (!(fs.existsSync || path.existsSync)(this.file)) { |
| 47 | 8 | try { |
| 48 | 8 | var defaultFile = fs.readFileSync(this.defaultConfig); |
| 49 | // Parse it to make sure there are no errors | |
| 50 | 7 | defaultFile = JSON.stringify(JSON.parse(defaultFile), true); |
| 51 | 7 | fs.writeFileSync(this.file, defaultFile); |
| 52 | } catch (ex) { | |
| 53 | 1 | return ex.message; |
| 54 | } | |
| 55 | 7 | return; |
| 56 | } else { | |
| 57 | 3 | return; |
| 58 | } | |
| 59 | ||
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Load the configuration | |
| 64 | */ | |
| 65 | 1 | Configuration.prototype.load = function(next) { |
| 66 | ||
| 67 | // Check if config exists for this environment or default it | |
| 68 | 11 | var checkConfig = this.check(); |
| 69 | 11 | if (!checkConfig) { |
| 70 | ||
| 71 | // Initialise nconf | |
| 72 | 10 | try { |
| 73 | 10 | this.nconf.use(this.type, this); |
| 74 | } catch (ex) { | |
| 75 | 0 | return next(ex); |
| 76 | } | |
| 77 | ||
| 78 | 10 | this.nconf.load(next); |
| 79 | ||
| 80 | } else { | |
| 81 | ||
| 82 | 1 | next(new Error("Unable to load configuration defined in " + this.env + ".json, there may be a problem with the default configuration in " + this.defaultConfig + ", reason: " + checkConfig)); |
| 83 | ||
| 84 | } | |
| 85 | ||
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * Get config - wrapper | |
| 90 | */ | |
| 91 | 1 | Configuration.prototype.get = function(key) { |
| 92 | 94 | return this.nconf.get(key); |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * Get config for module - wrapper | |
| 97 | */ | |
| 98 | 1 | Configuration.prototype.getModuleConfig = function(moduleName, key) { |
| 99 | 2 | var moduleKey = 'modules:' + moduleName + ':config' + (key ? ':' + key : ''); |
| 100 | 2 | return this.nconf.get(moduleKey); |
| 101 | } | |
| 102 | ||
| 103 | ||
| 104 | /** | |
| 105 | * Set config | |
| 106 | */ | |
| 107 | 1 | Configuration.prototype.set = function(key, value) { |
| 108 | 4 | this.dirty = true; |
| 109 | 4 | this.nconf.set(key, value); |
| 110 | } | |
| 111 | ||
| 112 | /** | |
| 113 | * Set config for module - wrapper | |
| 114 | */ | |
| 115 | 1 | Configuration.prototype.setModuleConfig = function(moduleName, key, value) { |
| 116 | 1 | var moduleKey = 'modules:' + moduleName + ':config' + (key ? ':' + key : ''); |
| 117 | 1 | this.dirty = true; |
| 118 | 1 | this.nconf.set(moduleKey, value); |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Set default config for module - wrapper | |
| 123 | */ | |
| 124 | 1 | Configuration.prototype.setDefaultModuleConfig = function(moduleName, config) { |
| 125 | ||
| 126 | 1 | var moduleKey = 'modules:' + moduleName + ':config'; |
| 127 | 1 | this.dirty = true; |
| 128 | ||
| 129 | // Extract the defaults from the config | |
| 130 | 1 | var defaultConfig = _.reduce(_.keys(config), function(memo, key) { |
| 131 | 1 | memo[key] = config[key]. |
| 132 | default; | |
| 133 | 1 | return memo; |
| 134 | }, {}) | |
| 135 | ||
| 136 | 1 | this.nconf.set(moduleKey, defaultConfig); |
| 137 | ||
| 138 | } | |
| 139 | ||
| 140 | /** | |
| 141 | * Save config | |
| 142 | */ | |
| 143 | 1 | Configuration.prototype.save = function(next) { |
| 144 | 2 | this.dirty = false; |
| 145 | 2 | this.nconf.save(next); |
| 146 | } | |
| 147 | ||
| 148 | /** | |
| 149 | * Set & save config | |
| 150 | */ | |
| 151 | ||
| 152 | 1 | Configuration.prototype.setSave = function(key, value, next) { |
| 153 | 1 | this.set(key, value); |
| 154 | 1 | this.dirty = false; |
| 155 | 1 | this.save(next); |
| 156 | } | |
| 157 | ||
| 158 | /** | |
| 159 | * Export the config object | |
| 160 | */ | |
| 161 | 1 | module.exports = Configuration; |
| Line | Hits | Source |
|---|---|---|
| 1 | ||
| 2 | /** | |
| 3 | * This helper allows us to include files that either have or haven't been marked up by jscoverage. | |
| 4 | * All modules under test should be included via; | |
| 5 | * | |
| 6 | * library = require('./helpers/require')('core/Config.js'); | |
| 7 | * | |
| 8 | * The path is always relative to the lib folder, and this approach only works for core Calipso libraries. | |
| 9 | * | |
| 10 | */ | |
| 11 | 6 | if (process.env.CALIPSO_COV) { |
| 12 | 6 | var jsc = require('jscoverage'), |
| 13 | require = jsc.require(module); // rewrite require function | |
| 14 | 6 | module.exports = function (library) { |
| 15 | 24 | return require('../../lib-cov/' + library); |
| 16 | } | |
| 17 | } else { | |
| 18 | 0 | module.exports = function (library) { |
| 19 | 0 | return require('../../lib/' + library); |
| 20 | } | |
| 21 | } |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso Core Library | |
| 3 | * | |
| 4 | * Copyright(c) 2011 Clifton Cunningham <clifton.cunningham@gmail.com> | |
| 5 | * MIT Licensed | |
| 6 | * | |
| 7 | * This is the core Calipso middleware that controls the bootstrapping, and core routing functions, required for | |
| 8 | * Calipso to function. This is loaded into an Express application via: | |
| 9 | * | |
| 10 | * app.use(calipso.calipsoRouter(next); | |
| 11 | * | |
| 12 | * Further detail is contained in comments for each function and object. | |
| 13 | * | |
| 14 | */ | |
| 15 | 1 | var rootpath = process.cwd() + '/', |
| 16 | path = require('path'), | |
| 17 | fs = require('fs'), | |
| 18 | events = require('events'); | |
| 19 | ||
| 20 | // Core object | |
| 21 | 1 | var calipso = module.exports = { |
| 22 | ||
| 23 | // Router and initialisation | |
| 24 | routingFn: routingFn, | |
| 25 | init: init, | |
| 26 | ||
| 27 | // Configuration exposed | |
| 28 | reloadConfig: reloadConfig, | |
| 29 | ||
| 30 | // Core objects - themes, data, modules | |
| 31 | theme: {}, | |
| 32 | data: {}, | |
| 33 | modules: {} | |
| 34 | ||
| 35 | }; | |
| 36 | ||
| 37 | // Load libraries in the core folder | |
| 38 | 1 | loadCore(calipso); |
| 39 | ||
| 40 | 1 | function loadCore(calipso) { |
| 41 | ||
| 42 | 1 | fs.readdirSync(__dirname + '/core').forEach(function(library) { |
| 43 | 19 | var isLibrary = library.split(".").length > 0 && library.split(".")[1] === 'js', |
| 44 | libName = library.split(".")[0].toLowerCase(); | |
| 45 | 37 | if (isLibrary) calipso[libName] = require(__dirname + '/core/' + library); |
| 46 | }); | |
| 47 | ||
| 48 | } | |
| 49 | 1 | module.exports.loaded = true; |
| 50 | ||
| 51 | /** | |
| 52 | * Calipso initialisation | |
| 53 | */ | |
| 54 | ||
| 55 | 1 | function init(app, initCallback) { |
| 56 | ||
| 57 | 1 | calipso.app = app; |
| 58 | ||
| 59 | // Load the calipso package.json into app.about | |
| 60 | 1 | calipso.module.loadAbout(app, rootpath, 'package.json'); |
| 61 | ||
| 62 | // config is the actual instance of loaded config, configuration is the library. | |
| 63 | 1 | calipso.config = app.config; |
| 64 | ||
| 65 | // Store the callback function for later | |
| 66 | 1 | calipso.initCallback = function() { |
| 67 | 1 | initCallback(); |
| 68 | }; | |
| 69 | ||
| 70 | // Configure the cache | |
| 71 | 1 | calipso.cacheService = calipso.cache.Cache({ |
| 72 | ttl: calipso.config.get('performance:cache:ttl') | |
| 73 | }); | |
| 74 | ||
| 75 | // Create our calipso event emitter | |
| 76 | 1 | calipso.e = new calipso.event.CalipsoEventEmitter({maxListeners: calipso.config.get('server:events:maxListeners')}); |
| 77 | ||
| 78 | // Load configuration | |
| 79 | 1 | initialiseCalipso(); |
| 80 | ||
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Core router function. | |
| 85 | * | |
| 86 | * Returns a connect middleware function that manages the roucting | |
| 87 | * of requests to modules. | |
| 88 | * | |
| 89 | * Expects Calipso to be initialised. | |
| 90 | */ | |
| 91 | ||
| 92 | 1 | function routingFn() { |
| 93 | ||
| 94 | // Return the function that manages the routing | |
| 95 | // Ok being non-synchro | |
| 96 | 4 | return function(req, res, next) { |
| 97 | ||
| 98 | // Default menus and blocks for each request | |
| 99 | // More of these can be added in modules, these are jsut the defaults | |
| 100 | 4 | res.menu = { |
| 101 | admin: new calipso.menu('admin', 'weight', 'root', { | |
| 102 | cls: 'admin' | |
| 103 | }), | |
| 104 | adminToolbar: new calipso.menu('adminToolbar', 'weight', 'root', { | |
| 105 | cls: 'admin-toolbar toolbar' | |
| 106 | }), | |
| 107 | // TODO - Configurable! | |
| 108 | userToolbar: new calipso.menu('userToolbar', 'weight', 'root', { | |
| 109 | cls: 'user-toolbar toolbar' | |
| 110 | }), | |
| 111 | primary: new calipso.menu('primary', 'name', 'root', { | |
| 112 | cls: 'primary' | |
| 113 | }), | |
| 114 | secondary: new calipso.menu('secondary', 'name', 'root', { | |
| 115 | cls: 'secondary' | |
| 116 | }) | |
| 117 | }; | |
| 118 | ||
| 119 | ||
| 120 | // Initialise our clientJS library linked to this request | |
| 121 | 4 | var Client = require('./client/Client'); |
| 122 | 4 | res.client = new Client(); |
| 123 | ||
| 124 | // Initialise helpers - first pass | |
| 125 | 4 | calipso.helpers.getDynamicHelpers(req, res, calipso); |
| 126 | ||
| 127 | // Route the modules | |
| 128 | 4 | calipso.module.eventRouteModules(req, res, next); |
| 129 | ||
| 130 | }; | |
| 131 | ||
| 132 | } | |
| 133 | ||
| 134 | /** | |
| 135 | * Load the application configuration | |
| 136 | * Configure the logging | |
| 137 | * Configure the theme | |
| 138 | * Load the modules | |
| 139 | * Initialise the modules | |
| 140 | * | |
| 141 | * @argument config | |
| 142 | * | |
| 143 | */ | |
| 144 | ||
| 145 | 1 | function initialiseCalipso(reloadConfig) { |
| 146 | ||
| 147 | // Check if we need to reload the config from disk (e.g. from cluster mode) | |
| 148 | 2 | if (reloadConfig) { |
| 149 | 1 | calipso.config.load(); |
| 150 | } | |
| 151 | ||
| 152 | // Clear Event listeners | |
| 153 | 2 | calipso.e.init(); |
| 154 | ||
| 155 | // Configure the logging | |
| 156 | 2 | calipso.logging.configureLogging(); |
| 157 | ||
| 158 | // Check / Connect Mongo | |
| 159 | 2 | calipso.storage.mongoConnect(calipso.config.get('database:uri'), false, function(err, connected) { |
| 160 | ||
| 161 | 2 | if (err) { |
| 162 | 0 | console.log("There was an error connecting to the database: " + err.message); |
| 163 | 0 | process.exit(); |
| 164 | } | |
| 165 | ||
| 166 | // Load all the themes | |
| 167 | 2 | loadThemes(function() { |
| 168 | ||
| 169 | // Initialise the modules and theming engine | |
| 170 | 2 | configureTheme(function() { |
| 171 | ||
| 172 | // Load all the modules | |
| 173 | 2 | calipso.module.loadModules(function() { |
| 174 | ||
| 175 | // Initialise, callback via calipso.initCallback | |
| 176 | 2 | calipso.module.initModules(); |
| 177 | ||
| 178 | }); | |
| 179 | ||
| 180 | }); | |
| 181 | ||
| 182 | }); | |
| 183 | ||
| 184 | }); | |
| 185 | ||
| 186 | } | |
| 187 | ||
| 188 | /** | |
| 189 | * Called both via a hook.io event as | |
| 190 | * well as via the server that initiated it. | |
| 191 | */ | |
| 192 | 1 | function reloadConfig(event, data, next) { |
| 193 | ||
| 194 | // Create a callback | |
| 195 | 1 | calipso.initCallback = function(err) { |
| 196 | // If called via event emitter rather than hook | |
| 197 | 2 | if (typeof next === "function") next(err); |
| 198 | }; | |
| 199 | 1 | return initialiseCalipso(true); |
| 200 | ||
| 201 | } | |
| 202 | ||
| 203 | /** | |
| 204 | * Load the available themes into the calipso.themes object | |
| 205 | */ | |
| 206 | ||
| 207 | 1 | function loadThemes(next) { |
| 208 | ||
| 209 | 2 | var themeBasePath = calipso.config.get('server:themePath'), |
| 210 | themePath, legacyTheme, themes; | |
| 211 | ||
| 212 | // Load the available themes | |
| 213 | 2 | calipso.availableThemes = calipso.availableThemes || {}; |
| 214 | ||
| 215 | 2 | calipso.lib.fs.readdirSync(calipso.lib.path.join(rootpath, themeBasePath)).forEach(function(folder) { |
| 216 | ||
| 217 | 2 | if (folder != "README" && folder[0] != '.') { |
| 218 | ||
| 219 | 2 | themes = calipso.lib.fs.readdirSync(calipso.lib.path.join(rootpath, themeBasePath, folder)); |
| 220 | ||
| 221 | // First scan for legacy themes | |
| 222 | 2 | legacyTheme = false; |
| 223 | 2 | themes.forEach(function(theme) { |
| 224 | 2 | if (theme === "theme.json") { |
| 225 | 0 | legacyTheme = true; |
| 226 | 0 | console.log("Themes are now stored in sub-folders under the themes folder, please move: " + folder + " (e.g. to custom/" + folder + ").\r\n"); |
| 227 | } | |
| 228 | }); | |
| 229 | ||
| 230 | // Process | |
| 231 | 2 | if (!legacyTheme) { |
| 232 | 2 | themes.forEach(function(theme) { |
| 233 | ||
| 234 | 2 | if (theme != "README" && theme[0] != '.') { |
| 235 | 2 | themePath = calipso.lib.path.join(rootpath, themeBasePath, folder, theme); |
| 236 | // Create the theme object | |
| 237 | 2 | calipso.availableThemes[theme] = { |
| 238 | name: theme, | |
| 239 | path: themePath | |
| 240 | }; | |
| 241 | // Load the about info from package.json | |
| 242 | 2 | calipso.module.loadAbout(calipso.availableThemes[theme], themePath, 'theme.json'); |
| 243 | } | |
| 244 | }); | |
| 245 | } | |
| 246 | } | |
| 247 | }); | |
| 248 | ||
| 249 | 2 | next(); |
| 250 | ||
| 251 | } | |
| 252 | ||
| 253 | /** | |
| 254 | * Configure a theme using the theme library. | |
| 255 | */ | |
| 256 | ||
| 257 | 1 | function configureTheme(next, overrideTheme) { |
| 258 | ||
| 259 | 2 | var defaultTheme = calipso.config.get("theme:default"); |
| 260 | 2 | var themeName = overrideTheme ? overrideTheme : calipso.config.get('theme:front'); |
| 261 | 2 | var themeConfig = calipso.availableThemes[themeName]; // Reference to theme.json |
| 262 | 2 | if (themeConfig) { |
| 263 | ||
| 264 | // Themes is the library | |
| 265 | 2 | calipso.themes.Theme(themeConfig, function(err, loadedTheme) { |
| 266 | ||
| 267 | // Current theme is always in calipso.theme | |
| 268 | 2 | calipso.theme = loadedTheme; |
| 269 | ||
| 270 | 2 | if (err) { |
| 271 | 0 | calipso.error(err.message); |
| 272 | } | |
| 273 | ||
| 274 | 2 | if (!calipso.theme) { |
| 275 | ||
| 276 | 0 | if (loadedTheme.name === defaultTheme) { |
| 277 | 0 | calipso.error('There has been a failure loading the default theme, calipso cannot start until this is fixed, terminating.'); |
| 278 | 0 | process.exit(); |
| 279 | 0 | return; |
| 280 | } else { | |
| 281 | 0 | calipso.error('The `' + themeName + '` theme failed to load, attempting to use the default theme: `' + defaultTheme + '`'); |
| 282 | 0 | configureTheme(next, defaultTheme); |
| 283 | 0 | return; |
| 284 | } | |
| 285 | ||
| 286 | } else { | |
| 287 | ||
| 288 | // Search for middleware that already has themeStatic tag | |
| 289 | 2 | var foundMiddleware = false, |
| 290 | mw; | |
| 291 | 2 | calipso.app.stack.forEach(function(middleware, key) { |
| 292 | ||
| 293 | 6 | if (middleware.handle.tag === 'theme.stylus') { |
| 294 | 1 | foundMiddleware = true; |
| 295 | 1 | mw = calipso.app.mwHelpers.stylusMiddleware(themeConfig.path); |
| 296 | 1 | calipso.app.stack[key].handle = mw; |
| 297 | } | |
| 298 | ||
| 299 | 6 | if (middleware.handle.tag === 'theme.static') { |
| 300 | 1 | foundMiddleware = true; |
| 301 | 1 | mw = calipso.app.mwHelpers.staticMiddleware(themeConfig.path); |
| 302 | 1 | calipso.app.stack[key].handle = mw; |
| 303 | } | |
| 304 | ||
| 305 | }); | |
| 306 | ||
| 307 | 2 | next(); |
| 308 | ||
| 309 | } | |
| 310 | ||
| 311 | }); | |
| 312 | ||
| 313 | } else { | |
| 314 | ||
| 315 | 0 | if (themeName === defaultTheme) { |
| 316 | 0 | console.error("Unable to locate the theme: " + themeName + ", terminating."); |
| 317 | 0 | process.exit(); |
| 318 | } else { | |
| 319 | 0 | calipso.error('The `' + themeName + '` theme is missing, trying the defaul theme: `' + defaultTheme + '`'); |
| 320 | 0 | configureTheme(next, defaultTheme); |
| 321 | } | |
| 322 | ||
| 323 | } | |
| 324 | ||
| 325 | } |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso Core Library - Storage of Rendered Blocks | |
| 3 | * Copyright(c) 2011 Clifton Cunningham | |
| 4 | * MIT Licensed | |
| 5 | * | |
| 6 | * This class controls the storage and retrieval of blocks rendered via the Router, e.g. specific pieces of output. | |
| 7 | * | |
| 8 | */ | |
| 9 | ||
| 10 | 1 | var rootpath = process.cwd() + '/', |
| 11 | path = require('path'), | |
| 12 | calipso = require(path.join('..', 'calipso')); | |
| 13 | ||
| 14 | /** | |
| 15 | * Holder for rendered blocks (get / set) | |
| 16 | * Idea is that this will give us an opportunity | |
| 17 | * to cache expensive sections of a page. | |
| 18 | */ | |
| 19 | ||
| 20 | 1 | function RenderedBlocks(cache) { |
| 21 | ||
| 22 | // Store the content rendered by modules | |
| 23 | 1 | this.content = {}; |
| 24 | ||
| 25 | // Flags to indicate if it should be cached | |
| 26 | 1 | this.contentCache = {}; |
| 27 | ||
| 28 | // The cache itself | |
| 29 | 1 | this.cache = cache; |
| 30 | ||
| 31 | } | |
| 32 | ||
| 33 | /** | |
| 34 | * Set block content | |
| 35 | */ | |
| 36 | 1 | RenderedBlocks.prototype.set = function(block, content, layout, params, next) { |
| 37 | ||
| 38 | 1 | var cacheKey = calipso.cacheService.getCacheKey(['block', block], params); |
| 39 | ||
| 40 | 1 | this.content[block] = this.content[block] || []; |
| 41 | 1 | this.content[block].push(content); |
| 42 | ||
| 43 | // If we are caching, then cache it. | |
| 44 | 1 | if (this.contentCache[block]) { |
| 45 | 0 | calipso.silly("Cache set for " + cacheKey); |
| 46 | 0 | this.cache.set(cacheKey, { |
| 47 | content: content, | |
| 48 | layout: layout | |
| 49 | }, null, next); | |
| 50 | } else { | |
| 51 | 1 | next(); |
| 52 | } | |
| 53 | ||
| 54 | }; | |
| 55 | ||
| 56 | /** | |
| 57 | * Get block content | |
| 58 | */ | |
| 59 | 1 | RenderedBlocks.prototype.get = function(key, next) { |
| 60 | ||
| 61 | // Check to see if the key is a regex, for 0.4 and 0.5 nodej | |
| 62 | 8 | if (typeof key === 'object' || typeof key === "function") { |
| 63 | 8 | var item, items = []; |
| 64 | 8 | for (item in this.content) { |
| 65 | 3 | if (this.content.hasOwnProperty(item)) { |
| 66 | 3 | if (item.match(key)) { |
| 67 | 1 | items.push(this.content[item]); |
| 68 | } | |
| 69 | } | |
| 70 | } | |
| 71 | 8 | next(null, items); |
| 72 | } else { | |
| 73 | 0 | next(null, this.content[key] || []); |
| 74 | } | |
| 75 | ||
| 76 | }; | |
| 77 | ||
| 78 | /** | |
| 79 | * Get content from cache and load into block | |
| 80 | */ | |
| 81 | 1 | RenderedBlocks.prototype.getCache = function(key, block, next) { |
| 82 | ||
| 83 | 0 | calipso.silly("Cache hit for block " + key); |
| 84 | ||
| 85 | 0 | var self = this; |
| 86 | 0 | this.cache.get(key, function(err, cache) { |
| 87 | ||
| 88 | 0 | self.content[block] = self.content[block] || []; |
| 89 | 0 | self.content[block].push(cache.content); |
| 90 | 0 | next(err, cache.layout); |
| 91 | ||
| 92 | }); | |
| 93 | ||
| 94 | }; | |
| 95 | ||
| 96 | 1 | module.exports.RenderedBlocks = RenderedBlocks; |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso Core Caching Library | |
| 3 | * | |
| 4 | * Copyright(c) 2011 Clifton Cunningham <clifton.cunningham@gmail.com> | |
| 5 | * MIT Licensed | |
| 6 | * | |
| 7 | * This is the core Calipso library that enables caching to be turned on | |
| 8 | * that will cache both module and full page output. | |
| 9 | * | |
| 10 | * The idea is to have a pluggable cache storage module, copied liberally | |
| 11 | * from concepts behind the express session module. | |
| 12 | * | |
| 13 | * | |
| 14 | * | |
| 15 | */ | |
| 16 | 1 | var rootpath = process.cwd() + '/', |
| 17 | path = require('path'), | |
| 18 | calipso = require(path.join('..', 'calipso')), | |
| 19 | MemoryStore = require('./cacheAdapters/memory'), | |
| 20 | Store = require('./cacheAdapters/store'); | |
| 21 | ||
| 22 | // Exports | |
| 23 | 1 | exports.Cache = Cache; |
| 24 | 1 | exports.Store = Store; |
| 25 | 1 | exports.MemoryStore = MemoryStore; |
| 26 | ||
| 27 | /** | |
| 28 | * Very simple wrapper that | |
| 29 | * Enables pluggability of cache store, defaulting to in Memory | |
| 30 | * | |
| 31 | * cache.set('cc','RAH!',500,function() { | |
| 32 | * cache.get('cc',function(err,item) { | |
| 33 | * console.log(item); | |
| 34 | * }); | |
| 35 | * }); | |
| 36 | * | |
| 37 | */ | |
| 38 | ||
| 39 | 1 | function Cache(options) { |
| 40 | ||
| 41 | 1 | var options = options || {}, |
| 42 | store = store || new MemoryStore(options); | |
| 43 | ||
| 44 | 1 | return store; |
| 45 | ||
| 46 | } |
| Line | Hits | Source |
|---|---|---|
| 1 | ||
| 2 | /*! | |
| 3 | * Calipso - cache - Memory Store | |
| 4 | * Approach copied from Connect session store | |
| 5 | * MIT Licensed | |
| 6 | */ | |
| 7 | ||
| 8 | /** | |
| 9 | * Module dependencies. | |
| 10 | */ | |
| 11 | ||
| 12 | 1 | var Store = require('./store'); |
| 13 | ||
| 14 | /** | |
| 15 | * Initialize a new `MemoryStore`. | |
| 16 | * | |
| 17 | * @api public | |
| 18 | */ | |
| 19 | ||
| 20 | 1 | var MemoryStore = module.exports = function MemoryStore(options) { |
| 21 | 1 | this.cache = {}; |
| 22 | 1 | this.options = options || {}; |
| 23 | }; | |
| 24 | ||
| 25 | /** | |
| 26 | * Inherit from `Store.prototype`. | |
| 27 | */ | |
| 28 | 1 | MemoryStore.prototype.__proto__ = Store.prototype; |
| 29 | ||
| 30 | /** | |
| 31 | * Attempt to fetch cache by the given `key'. | |
| 32 | * | |
| 33 | * @param {String} key | |
| 34 | * @param {Function} fn | |
| 35 | * @api public | |
| 36 | */ | |
| 37 | ||
| 38 | 1 | MemoryStore.prototype.get = function(key, fn){ |
| 39 | 0 | var self = this; |
| 40 | ||
| 41 | //process.nextTick(function(){ | |
| 42 | 0 | var cache = self.cache[key]; |
| 43 | 0 | if (cache) { |
| 44 | 0 | fn(null, cache.item); |
| 45 | } else { | |
| 46 | 0 | fn(new Error('Cache miss: ' + key)); |
| 47 | } | |
| 48 | //}); | |
| 49 | }; | |
| 50 | ||
| 51 | /** | |
| 52 | * Check cache by the given `key'. | |
| 53 | * | |
| 54 | * @param {String} key | |
| 55 | * @param {Function} fn | |
| 56 | * @api public | |
| 57 | */ | |
| 58 | ||
| 59 | 1 | MemoryStore.prototype.check = function(key, fn){ |
| 60 | ||
| 61 | 0 | var self = this; |
| 62 | 0 | var cache = self.cache[key]; |
| 63 | ||
| 64 | 0 | if (cache) { |
| 65 | // Check to see if it has expired | |
| 66 | 0 | if (!cache.expires || Date.now() < cache.expires) { // TODO |
| 67 | 0 | fn(null, true); |
| 68 | } else { | |
| 69 | 0 | self.destroy(key, fn); |
| 70 | } | |
| 71 | } else { | |
| 72 | 0 | fn(null,false); |
| 73 | } | |
| 74 | ||
| 75 | }; | |
| 76 | ||
| 77 | /** | |
| 78 | * Add an item to the cache, referenced by key | |
| 79 | * with expires | |
| 80 | * | |
| 81 | * @param {String} key | |
| 82 | * @param {String} item | |
| 83 | * @param {Number} expires (milliseconds) | |
| 84 | * @param {Function} fn | |
| 85 | * @api public | |
| 86 | */ | |
| 87 | ||
| 88 | 1 | MemoryStore.prototype.set = function(key, item, ttl, fn){ |
| 89 | 0 | var self = this; |
| 90 | //process.nextTick(function(){ | |
| 91 | 0 | ttl = ttl || (self.options.ttl || 600); |
| 92 | 0 | var expires = Date.now() + ttl; |
| 93 | 0 | self.cache[key] = {item:item, expires:expires} |
| 94 | 0 | fn && fn(); |
| 95 | //}); | |
| 96 | }; | |
| 97 | ||
| 98 | /** | |
| 99 | * Destroy the session associated with the given `key`. | |
| 100 | * | |
| 101 | * @param {String} key | |
| 102 | * @api public | |
| 103 | */ | |
| 104 | ||
| 105 | 1 | MemoryStore.prototype.destroy = function(key, fn){ |
| 106 | 0 | var self = this; |
| 107 | //process.nextTick(function(){ | |
| 108 | 0 | delete self.cache[key]; |
| 109 | 0 | fn && fn(); |
| 110 | //}); | |
| 111 | }; | |
| 112 | ||
| 113 | /** | |
| 114 | * Invoke the given callback `fn` with all active sessions. | |
| 115 | * | |
| 116 | * @param {Function} fn | |
| 117 | * @api public | |
| 118 | */ | |
| 119 | ||
| 120 | 1 | MemoryStore.prototype.all = function(fn){ |
| 121 | 0 | var arr = [] |
| 122 | , keys = Object.keys(this.cache); | |
| 123 | 0 | for (var i = 0, len = keys.length; i < len; ++i) { |
| 124 | 0 | arr.push(this.cache[keys[i]]); |
| 125 | } | |
| 126 | 0 | fn(null, arr); |
| 127 | }; | |
| 128 | ||
| 129 | /** | |
| 130 | * Clear cache | |
| 131 | * | |
| 132 | * @param {Function} fn | |
| 133 | * @api public | |
| 134 | */ | |
| 135 | ||
| 136 | 1 | MemoryStore.prototype.clear = function(fn){ |
| 137 | 0 | this.cache = {}; |
| 138 | 0 | fn && fn(); |
| 139 | }; | |
| 140 | ||
| 141 | /** | |
| 142 | * Fetch number of cache items | |
| 143 | * | |
| 144 | * @param {Function} fn | |
| 145 | * @api public | |
| 146 | */ | |
| 147 | ||
| 148 | 1 | MemoryStore.prototype.length = function(fn){ |
| 149 | 0 | fn(null, Object.keys(this.cache).length); |
| 150 | }; |
| Line | Hits | Source |
|---|---|---|
| 1 | ||
| 2 | /*! | |
| 3 | * Calipso - cache - Store | |
| 4 | * Concepts taken from connect session | |
| 5 | * MIT Licensed | |
| 6 | */ | |
| 7 | ||
| 8 | /** | |
| 9 | * Initialize abstract `Store`. | |
| 10 | * | |
| 11 | * @api private | |
| 12 | */ | |
| 13 | 1 | var rootpath = process.cwd() + '/', |
| 14 | path = require('path'), | |
| 15 | calipso = require(path.join('..', '..', 'calipso')); | |
| 16 | ||
| 17 | /** | |
| 18 | * Store object - options: | |
| 19 | * prefix - a prefix to attach to all cache keys, defaults to calipso. | |
| 20 | */ | |
| 21 | 1 | var Store = module.exports = function Store(options){ |
| 22 | ||
| 23 | 0 | this.options = options || {}; |
| 24 | ||
| 25 | }; | |
| 26 | ||
| 27 | /** | |
| 28 | * Generate a cache key - applies to all store types | |
| 29 | */ | |
| 30 | 1 | Store.prototype.getCacheKey = function(keys, params) { |
| 31 | ||
| 32 | 1 | var prefix = this.options.prefix || "calipso"; |
| 33 | ||
| 34 | // Append the theme, allows for theme change | |
| 35 | 1 | var cacheKey = prefix + "::" + calipso.theme.theme, paramCount = 0; |
| 36 | ||
| 37 | // Create the key from the keys | |
| 38 | 1 | keys.forEach(function(value) { |
| 39 | 2 | cacheKey += "::" + value; |
| 40 | }) | |
| 41 | ||
| 42 | 1 | var qs = require("querystring"); |
| 43 | ||
| 44 | 1 | if(params) { |
| 45 | 1 | cacheKey += "::"; |
| 46 | 1 | calipso.lib._.each(params,function(param,key) { |
| 47 | 0 | if(param) { |
| 48 | 0 | cacheKey += (paramCount > 0 ? "::" : "") + (param ? (key + "=" + qs.escape(param)) : ""); |
| 49 | 0 | paramCount += 1; |
| 50 | } | |
| 51 | }); | |
| 52 | } | |
| 53 | ||
| 54 | 1 | return cacheKey; |
| 55 | } |
| Line | Hits | Source |
|---|---|---|
| 1 | /** | |
| 2 | * This is a generic date parsing and formatting library, to avoid any confusion | |
| 3 | * about how dates are handled across both the back and front end (assuming jQuery UI will be) | |
| 4 | * the default. | |
| 5 | * | |
| 6 | * These functions are extracted from the jQuery UI Datepicker (see below). | |
| 7 | */ | |
| 8 | ||
| 9 | /** | |
| 10 | * jQuery UI Datepicker | |
| 11 | * | |
| 12 | * | |
| 13 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) | |
| 14 | * Dual licensed under the MIT or GPL Version 2 licenses. | |
| 15 | * - License http://jquery.org/license | |
| 16 | * - Original Source http://docs.jquery.com/UI/Datepicker | |
| 17 | */ | |
| 18 | ||
| 19 | 1 | function CalipsoDate() { |
| 20 | ||
| 21 | 1 | this.regional = []; // Available regional settings, indexed by language code |
| 22 | 1 | this.regional[''] = { // Default regional settings |
| 23 | closeText: 'Done', | |
| 24 | // Display text for close link | |
| 25 | prevText: 'Prev', | |
| 26 | // Display text for previous month link | |
| 27 | nextText: 'Next', | |
| 28 | // Display text for next month link | |
| 29 | currentText: 'Today', | |
| 30 | // Display text for current month link | |
| 31 | monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], | |
| 32 | // Names of months for drop-down and formatting | |
| 33 | monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], | |
| 34 | // For formatting | |
| 35 | dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], | |
| 36 | // For formatting | |
| 37 | dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], | |
| 38 | // For formatting | |
| 39 | dayNamesMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], | |
| 40 | // Column headings for days starting at Sunday | |
| 41 | weekHeader: 'Wk', | |
| 42 | // Column header for week of the year | |
| 43 | dateFormat: 'mm/dd/yy', | |
| 44 | // See format options on parseDate | |
| 45 | firstDay: 0, | |
| 46 | // The first day of the week, Sun = 0, Mon = 1, ... | |
| 47 | isRTL: false, | |
| 48 | // True if right-to-left language, false if left-to-right | |
| 49 | showMonthAfterYear: false, | |
| 50 | // True if the year select precedes month, false for month then year | |
| 51 | yearSuffix: '' // Additional text to append to the year in the month headers | |
| 52 | }; | |
| 53 | ||
| 54 | 1 | this._defaults = this.regional['']; |
| 55 | ||
| 56 | // Standard date formats. | |
| 57 | 1 | this.ATOM = 'yy-mm-dd'; // RFC 3339 (ISO 8601) |
| 58 | 1 | this.COOKIE = 'D, dd M yy'; |
| 59 | 1 | this.ISO_8601 = 'yy-mm-dd'; |
| 60 | 1 | this.RFC_822 = 'D, d M y'; |
| 61 | 1 | this.RFC_850 = 'DD, dd-M-y'; |
| 62 | 1 | this.RFC_1036 = 'D, d M y'; |
| 63 | 1 | this.RFC_1123 = 'D, d M yy'; |
| 64 | 1 | this.RFC_2822 = 'D, d M yy'; |
| 65 | 1 | this.RSS = 'D, d M y'; // RFC 822 |
| 66 | 1 | this.TICKS = '!'; |
| 67 | 1 | this.TIMESTAMP = '@'; |
| 68 | 1 | this.W3C = 'yy-mm-dd'; // ISO 8601 |
| 69 | 1 | this._ticksTo1970 = (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) + Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000); |
| 70 | ||
| 71 | } | |
| 72 | ||
| 73 | /* Parse a string value into a date object. | |
| 74 | See formatDate below for the possible formats. | |
| 75 | ||
| 76 | @param format string - the expected format of the date | |
| 77 | @param value string - the date in the above format | |
| 78 | @param settings Object - attributes include: | |
| 79 | shortYearCutoff number - the cutoff year for determining the century (optional) | |
| 80 | dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) | |
| 81 | dayNames string[7] - names of the days from Sunday (optional) | |
| 82 | monthNamesShort string[12] - abbreviated names of the months (optional) | |
| 83 | monthNames string[12] - names of the months (optional) | |
| 84 | @return Date - the extracted date value or null if value is blank */ | |
| 85 | 1 | CalipsoDate.prototype.parseDate = function(format, value, settings) { |
| 86 | 0 | if (format == null || value == null) throw 'Invalid arguments'; |
| 87 | 0 | value = (typeof value == 'object' ? value.toString() : value + ''); |
| 88 | 0 | if (value == '') return null; |
| 89 | 0 | var shortYearCutoff = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff; |
| 90 | 0 | shortYearCutoff = (typeof shortYearCutoff != 'string' ? shortYearCutoff : new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); |
| 91 | 0 | var dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort; |
| 92 | 0 | var dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames; |
| 93 | 0 | var monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort; |
| 94 | 0 | var monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames; |
| 95 | 0 | var year = -1; |
| 96 | 0 | var month = -1; |
| 97 | 0 | var day = -1; |
| 98 | 0 | var doy = -1; |
| 99 | 0 | var literal = false; |
| 100 | // Check whether a format character is doubled | |
| 101 | 0 | var lookAhead = function(match) { |
| 102 | 0 | var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) == match); |
| 103 | 0 | if (matches) iFormat++; |
| 104 | 0 | return matches; |
| 105 | }; | |
| 106 | // Extract a number from the string value | |
| 107 | 0 | var getNumber = function(match) { |
| 108 | 0 | var isDoubled = lookAhead(match); |
| 109 | 0 | var size = (match == '@' ? 14 : (match == '!' ? 20 : (match == 'y' && isDoubled ? 4 : (match == 'o' ? 3 : 2)))); |
| 110 | 0 | var digits = new RegExp('^\\d{1,' + size + '}'); |
| 111 | 0 | var num = value.substring(iValue).match(digits); |
| 112 | 0 | if (!num) throw 'Missing number at position ' + iValue; |
| 113 | 0 | iValue += num[0].length; |
| 114 | 0 | return parseInt(num[0], 10); |
| 115 | }; | |
| 116 | // Extract a name from the string value and convert to an index | |
| 117 | 0 | var getName = function(match, shortNames, longNames) { |
| 118 | 0 | var names = $.map(lookAhead(match) ? longNames : shortNames, function(v, k) { |
| 119 | 0 | return [[k, v]]; |
| 120 | }).sort(function(a, b) { | |
| 121 | 0 | return -(a[1].length - b[1].length); |
| 122 | }); | |
| 123 | 0 | var index = -1; |
| 124 | 0 | $.each(names, function(i, pair) { |
| 125 | 0 | var name = pair[1]; |
| 126 | 0 | if (value.substr(iValue, name.length).toLowerCase() == name.toLowerCase()) { |
| 127 | 0 | index = pair[0]; |
| 128 | 0 | iValue += name.length; |
| 129 | 0 | return false; |
| 130 | } | |
| 131 | }); | |
| 132 | 0 | if (index != -1) return index + 1; |
| 133 | 0 | else throw 'Unknown name at position ' + iValue; |
| 134 | }; | |
| 135 | // Confirm that a literal character matches the string value | |
| 136 | 0 | var checkLiteral = function() { |
| 137 | 0 | if (value.charAt(iValue) != format.charAt(iFormat)) throw 'Unexpected literal at position ' + iValue; |
| 138 | 0 | iValue++; |
| 139 | }; | |
| 140 | 0 | var iValue = 0; |
| 141 | 0 | for (var iFormat = 0; iFormat < format.length; iFormat++) { |
| 142 | 0 | if (literal) if (format.charAt(iFormat) == "'" && !lookAhead("'")) literal = false; |
| 143 | 0 | else checkLiteral(); |
| 144 | 0 | else switch (format.charAt(iFormat)) { |
| 145 | case 'd': | |
| 146 | 0 | day = getNumber('d'); |
| 147 | 0 | break; |
| 148 | case 'D': | |
| 149 | 0 | getName('D', dayNamesShort, dayNames); |
| 150 | 0 | break; |
| 151 | case 'o': | |
| 152 | 0 | doy = getNumber('o'); |
| 153 | 0 | break; |
| 154 | case 'm': | |
| 155 | 0 | month = getNumber('m'); |
| 156 | 0 | break; |
| 157 | case 'M': | |
| 158 | 0 | month = getName('M', monthNamesShort, monthNames); |
| 159 | 0 | break; |
| 160 | case 'y': | |
| 161 | 0 | year = getNumber('y'); |
| 162 | 0 | break; |
| 163 | case '@': | |
| 164 | 0 | var date = new Date(getNumber('@')); |
| 165 | 0 | year = date.getFullYear(); |
| 166 | 0 | month = date.getMonth() + 1; |
| 167 | 0 | day = date.getDate(); |
| 168 | 0 | break; |
| 169 | case '!': | |
| 170 | 0 | var date = new Date((getNumber('!') - this._ticksTo1970) / 10000); |
| 171 | 0 | year = date.getFullYear(); |
| 172 | 0 | month = date.getMonth() + 1; |
| 173 | 0 | day = date.getDate(); |
| 174 | 0 | break; |
| 175 | case "'": | |
| 176 | 0 | if (lookAhead("'")) checkLiteral(); |
| 177 | 0 | else literal = true; |
| 178 | 0 | break; |
| 179 | default: | |
| 180 | 0 | checkLiteral(); |
| 181 | } | |
| 182 | } | |
| 183 | 0 | if (year == -1) year = new Date().getFullYear(); |
| 184 | 0 | else if (year < 100) year += new Date().getFullYear() - new Date().getFullYear() % 100 + (year <= shortYearCutoff ? 0 : -100); |
| 185 | 0 | if (doy > -1) { |
| 186 | 0 | month = 1; |
| 187 | 0 | day = doy; |
| 188 | 0 | do { |
| 189 | 0 | var dim = this._getDaysInMonth(year, month - 1); |
| 190 | 0 | if (day <= dim) break; |
| 191 | 0 | month++; |
| 192 | 0 | day -= dim; |
| 193 | } while (true); | |
| 194 | } | |
| 195 | 0 | var date = this._daylightSavingAdjust(new Date(year, month - 1, day)); |
| 196 | 0 | if (date.getFullYear() != year || date.getMonth() + 1 != month || date.getDate() != day) throw 'Invalid date'; // E.g. 31/02/00 |
| 197 | 0 | return date; |
| 198 | } | |
| 199 | ||
| 200 | /* | |
| 201 | Format a date object into a string value. | |
| 202 | ||
| 203 | The format can be combinations of the following | |
| 204 | ||
| 205 | d - day of month (no leading zero) | |
| 206 | dd - day of month (two digit) | |
| 207 | o - day of year (no leading zeros) | |
| 208 | oo - day of year (three digit) | |
| 209 | D - day name short | |
| 210 | DD - day name long | |
| 211 | m - month of year (no leading zero) | |
| 212 | mm - month of year (two digit) | |
| 213 | M - month name short | |
| 214 | MM - month name long | |
| 215 | y - year (two digit) | |
| 216 | yy - year (four digit) | |
| 217 | @ - Unix timestamp (ms since 01/01/1970) | |
| 218 | ! - Windows ticks (100ns since 01/01/0001) | |
| 219 | '...' - literal text | |
| 220 | '' - single quote | |
| 221 | ||
| 222 | @param format string - the desired format of the date | |
| 223 | @param date Date - the date value to format | |
| 224 | @param settings Object - attributes include: | |
| 225 | dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) | |
| 226 | dayNames string[7] - names of the days from Sunday (optional) | |
| 227 | monthNamesShort string[12] - abbreviated names of the months (optional) | |
| 228 | monthNames string[12] - names of the months (optional) | |
| 229 | @return string - the date in the above format */ | |
| 230 | ||
| 231 | 1 | CalipsoDate.prototype.formatDate = function(format, date, settings) { |
| 232 | 0 | if (!date) return ''; |
| 233 | 0 | var dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort; |
| 234 | 0 | var dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames; |
| 235 | 0 | var monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort; |
| 236 | 0 | var monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames; |
| 237 | // Check whether a format character is doubled | |
| 238 | 0 | var lookAhead = function(match) { |
| 239 | 0 | var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) == match); |
| 240 | 0 | if (matches) iFormat++; |
| 241 | 0 | return matches; |
| 242 | }; | |
| 243 | // Format a number, with leading zero if necessary | |
| 244 | 0 | var formatNumber = function(match, value, len) { |
| 245 | 0 | var num = '' + value; |
| 246 | 0 | if (lookAhead(match)) while (num.length < len) |
| 247 | 0 | num = '0' + num; |
| 248 | 0 | return num; |
| 249 | }; | |
| 250 | // Format a name, short or long as requested | |
| 251 | 0 | var formatName = function(match, value, shortNames, longNames) { |
| 252 | 0 | return (lookAhead(match) ? longNames[value] : shortNames[value]); |
| 253 | }; | |
| 254 | 0 | var output = ''; |
| 255 | 0 | var literal = false; |
| 256 | 0 | if (date) for (var iFormat = 0; iFormat < format.length; iFormat++) { |
| 257 | 0 | if (literal) if (format.charAt(iFormat) == "'" && !lookAhead("'")) literal = false; |
| 258 | 0 | else output += format.charAt(iFormat); |
| 259 | 0 | else switch (format.charAt(iFormat)) { |
| 260 | case 'd': | |
| 261 | 0 | output += formatNumber('d', date.getDate(), 2); |
| 262 | 0 | break; |
| 263 | case 'D': | |
| 264 | 0 | output += formatName('D', date.getDay(), dayNamesShort, dayNames); |
| 265 | 0 | break; |
| 266 | case 'o': | |
| 267 | 0 | output += formatNumber('o', (date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000, 3); |
| 268 | 0 | break; |
| 269 | case 'm': | |
| 270 | 0 | output += formatNumber('m', date.getMonth() + 1, 2); |
| 271 | 0 | break; |
| 272 | case 'M': | |
| 273 | 0 | output += formatName('M', date.getMonth(), monthNamesShort, monthNames); |
| 274 | 0 | break; |
| 275 | case 'y': | |
| 276 | 0 | output += (lookAhead('y') ? date.getFullYear() : (date.getYear() % 100 < 10 ? '0' : '') + date.getYear() % 100); |
| 277 | 0 | break; |
| 278 | case '@': | |
| 279 | 0 | output += date.getTime(); |
| 280 | 0 | break; |
| 281 | case '!': | |
| 282 | 0 | output += date.getTime() * 10000 + this._ticksTo1970; |
| 283 | 0 | break; |
| 284 | case "'": | |
| 285 | 0 | if (lookAhead("'")) output += "'"; |
| 286 | 0 | else literal = true; |
| 287 | 0 | break; |
| 288 | default: | |
| 289 | 0 | output += format.charAt(iFormat); |
| 290 | } | |
| 291 | } | |
| 292 | 0 | return output; |
| 293 | } | |
| 294 | ||
| 295 | /** | |
| 296 | * Export an instance of our date object | |
| 297 | */ | |
| 298 | 1 | module.exports = new CalipsoDate(); |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso Module Event Library | |
| 3 | * Copyright(c) 2011 Clifton Cunningham | |
| 4 | * MIT Licensed | |
| 5 | * | |
| 6 | * This library provides an event emitter for modules that is created on each request, | |
| 7 | * to provide the ability for module dependencies to be managed, as well as enable modules | |
| 8 | * to ensure that they run after all other modules have emitted certain events (e.g. menu rendering). | |
| 9 | * | |
| 10 | * | |
| 11 | */ | |
| 12 | ||
| 13 | /** | |
| 14 | * Includes | |
| 15 | */ | |
| 16 | 1 | var rootpath = process.cwd() + '/', |
| 17 | path = require('path'), | |
| 18 | util = require('util'), | |
| 19 | events = require('events'), | |
| 20 | calipso = require(path.join('..', 'calipso')); | |
| 21 | ||
| 22 | 1 | exports = module.exports = { |
| 23 | CalipsoEventEmitter: CalipsoEventEmitter, | |
| 24 | RequestEventListener: RequestEventListener, | |
| 25 | addModuleEventListener: addModuleEventListener, | |
| 26 | // Module & Routing Event constants | |
| 27 | ROUTE_START: 'route_s', | |
| 28 | ROUTE_FINISH: 'route_f', | |
| 29 | INIT_START: 'init_s', | |
| 30 | INIT_FINISH: 'init_f' | |
| 31 | }; | |
| 32 | ||
| 33 | ||
| 34 | /** | |
| 35 | * Calipso event emitter, object that enables calipso to emit events. | |
| 36 | * Events are always triggered at server scope, and cannot be used to | |
| 37 | * Execute functions in request scope | |
| 38 | */ | |
| 39 | ||
| 40 | 1 | function CalipsoEventEmitter(options) { |
| 41 | ||
| 42 | 5 | var self = this; |
| 43 | ||
| 44 | // Initialise options | |
| 45 | 5 | this.options = options || {}; |
| 46 | ||
| 47 | // Create an emitter to drive events | |
| 48 | 5 | this.emitter = new events.EventEmitter(); |
| 49 | 5 | this.emitter.setMaxListeners(this.options.maxListeners || 100); |
| 50 | ||
| 51 | // Holder for events, enable debugging of module events | |
| 52 | 5 | this.events = {}; |
| 53 | ||
| 54 | // Clear all existing listeners | |
| 55 | 5 | this.init = function() { |
| 56 | ||
| 57 | // Clear down the event emitters | |
| 58 | 7 | for (var event in self.events) { |
| 59 | ||
| 60 | 3 | self.emitter.removeAllListeners("PRE_" + event); |
| 61 | 3 | self.emitter.removeAllListeners("POST_" + event); |
| 62 | ||
| 63 | 3 | if (self.events[event].custom) { |
| 64 | 3 | for (var key in self.events[event].custom) { |
| 65 | 0 | self.emitter.removeAllListeners(event + "_" + key); |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | } | |
| 70 | ||
| 71 | // Add core events not created by modules | |
| 72 | 7 | this.addEvent('FORM'); |
| 73 | ||
| 74 | }; | |
| 75 | ||
| 76 | // Wrapper for event emitter, enable turn on / off | |
| 77 | 5 | this.addEvent = function(event, options) { |
| 78 | ||
| 79 | 11 | options = calipso.lib._.extend({ |
| 80 | enabled: true | |
| 81 | }, options); | |
| 82 | ||
| 83 | 11 | this.events[event] = options; |
| 84 | // Enable tracking of attached listeners for debugging purposes | |
| 85 | 11 | this.events[event].preListeners = { |
| 86 | '#': 0 | |
| 87 | }; | |
| 88 | 11 | this.events[event].postListeners = { |
| 89 | '#': 0 | |
| 90 | }; | |
| 91 | 11 | this.events[event].custom = {}; |
| 92 | }; | |
| 93 | ||
| 94 | // Pre and post event prefixes | |
| 95 | 5 | var pre_prefix = 'PRE_', |
| 96 | post_prefix = 'POST_'; | |
| 97 | ||
| 98 | // Register a pre listener | |
| 99 | 5 | this.pre = function(event, listener, fn) { |
| 100 | ||
| 101 | 2 | self.emitter.on(pre_prefix + event, fn); |
| 102 | 2 | this.events[event].preListeners[listener] = this.events[event].preListeners[listener] || []; |
| 103 | 2 | this.events[event].preListeners[listener].push({ |
| 104 | name: fn.name | |
| 105 | }); | |
| 106 | 2 | this.events[event].preListeners['#'] += 1; |
| 107 | }; | |
| 108 | ||
| 109 | // Register a post listener | |
| 110 | 5 | this.post = function(event, listener, fn) { |
| 111 | 2 | self.emitter.on(post_prefix + event, fn); |
| 112 | 2 | this.events[event].postListeners[listener] = this.events[event].postListeners[listener] || []; |
| 113 | 2 | this.events[event].postListeners[listener].push({ |
| 114 | name: fn.name | |
| 115 | }); | |
| 116 | 2 | this.events[event].postListeners['#'] += 1; |
| 117 | }; | |
| 118 | ||
| 119 | // Register a custom event listener | |
| 120 | 5 | this.custom = function(event, key, listener, fn) { |
| 121 | ||
| 122 | 2 | self.emitter.on(event + '_' + key, fn); |
| 123 | ||
| 124 | // Register under key | |
| 125 | 2 | this.events[event].custom[key] = this.events[event].custom[key] || { |
| 126 | customListeners: { | |
| 127 | '#': 0 | |
| 128 | } | |
| 129 | }; | |
| 130 | ||
| 131 | // Register | |
| 132 | 2 | this.events[event].custom[key].customListeners[listener] = this.events[event].custom[key].customListeners[listener] || []; |
| 133 | 2 | this.events[event].custom[key].customListeners[listener].push({ |
| 134 | name: fn.name | |
| 135 | }); | |
| 136 | 2 | this.events[event].custom[key].customListeners['#'] += 1; |
| 137 | ||
| 138 | }; | |
| 139 | ||
| 140 | // Emit a pre event | |
| 141 | 5 | this.pre_emit = function(event, data, next) { |
| 142 | ||
| 143 | 2 | var cb; |
| 144 | ||
| 145 | // Create a callback to track completion of all events (only if next exists) | |
| 146 | 2 | if (typeof next === "function") { |
| 147 | 1 | cb = createCallback(this.events[event].preListeners['#'], data, next); |
| 148 | } else { | |
| 149 | 1 | cb = function() {}; |
| 150 | } | |
| 151 | ||
| 152 | 2 | if (this.events[event] && this.events[event].enabled) { |
| 153 | 2 | self.emitter.emit(pre_prefix + event, pre_prefix + event, data, cb); |
| 154 | } | |
| 155 | ||
| 156 | }; | |
| 157 | ||
| 158 | // Emit a post event | |
| 159 | 5 | this.post_emit = function(event, data, next) { |
| 160 | ||
| 161 | 2 | var cb; |
| 162 | ||
| 163 | // Create a callback to track completion of all events (only if next exists) | |
| 164 | 2 | if (typeof next === "function") { |
| 165 | 1 | cb = createCallback(this.events[event].postListeners['#'], data, next); |
| 166 | } else { | |
| 167 | 1 | cb = function() {}; |
| 168 | } | |
| 169 | ||
| 170 | 2 | if (this.events[event] && this.events[event].enabled) { |
| 171 | 2 | self.emitter.emit(post_prefix + event, post_prefix + event, data, cb); |
| 172 | } | |
| 173 | ||
| 174 | }; | |
| 175 | ||
| 176 | // Emit a custom event | |
| 177 | 5 | this.custom_emit = function(event, key, data, next) { |
| 178 | ||
| 179 | 2 | var cb; |
| 180 | ||
| 181 | 2 | if (this.events[event] && this.events[event].custom[key] && this.events[event].enabled) { |
| 182 | ||
| 183 | // Create a callback to track completion of all events (only if next exists) | |
| 184 | 2 | if (typeof next === "function") { |
| 185 | 2 | cb = createCallback(this.events[event].custom[key].customListeners['#'], data, next); |
| 186 | } else { | |
| 187 | 0 | cb = function() {}; |
| 188 | } | |
| 189 | ||
| 190 | 2 | self.emitter.emit(event + '_' + key, event + '_' + key, data, cb); |
| 191 | ||
| 192 | } else { | |
| 193 | 0 | next(data); |
| 194 | } | |
| 195 | ||
| 196 | }; | |
| 197 | ||
| 198 | // Create a curried callback function for use in the emit code | |
| 199 | ||
| 200 | 5 | function createCallback(total, data, callback) { |
| 201 | ||
| 202 | 4 | var count = 0, |
| 203 | total = total, | |
| 204 | outputStack = []; | |
| 205 | ||
| 206 | 8 | if (data) outputStack.push(data); |
| 207 | ||
| 208 | // No listeners, so callback immediately | |
| 209 | 4 | if (total === 0) { |
| 210 | 0 | callback(data); |
| 211 | 0 | return; |
| 212 | } | |
| 213 | ||
| 214 | 4 | return function(data) { |
| 215 | ||
| 216 | 4 | count += 1; |
| 217 | ||
| 218 | 8 | if (data) outputStack.push(data); |
| 219 | ||
| 220 | // Merge the outputs from the stack | |
| 221 | 4 | if (count === total) { |
| 222 | 4 | callback(mergeArray(outputStack)); |
| 223 | } | |
| 224 | ||
| 225 | }; | |
| 226 | ||
| 227 | } | |
| 228 | ||
| 229 | } | |
| 230 | ||
| 231 | /** | |
| 232 | * Module event emitter, object that enables modules to emit events. | |
| 233 | * This contains both server and request scope event emitters, though clearly | |
| 234 | * an instance of an object only emits one or the other depending on | |
| 235 | * where it is instantiated. | |
| 236 | */ | |
| 237 | ||
| 238 | 1 | function ModuleInitEventEmitter(moduleName, options) { |
| 239 | ||
| 240 | 9 | events.EventEmitter.call(this); |
| 241 | ||
| 242 | 9 | var self = this; |
| 243 | ||
| 244 | 9 | self.options = options || {}; |
| 245 | 9 | this.moduleName = moduleName; |
| 246 | ||
| 247 | // Set the max listeners | |
| 248 | 9 | var maxListeners = self.options.maxListeners || 100; |
| 249 | 9 | this.setMaxListeners(maxListeners); |
| 250 | ||
| 251 | 9 | this.init_start = function(options) { |
| 252 | 8 | self.emit(exports.INIT_START, self.moduleName, options); |
| 253 | }; | |
| 254 | ||
| 255 | 9 | this.init_finish = function(options) { |
| 256 | 8 | self.emit(exports.INIT_FINISH, self.moduleName, options); |
| 257 | }; | |
| 258 | ||
| 259 | } | |
| 260 | ||
| 261 | ||
| 262 | /** | |
| 263 | * Event listener linked to the module itself | |
| 264 | * This is for server events (e.g. init, reload) | |
| 265 | * No events here can sit within the request context as | |
| 266 | * they will apply to all requests | |
| 267 | */ | |
| 268 | ||
| 269 | 1 | function addModuleEventListener(module, options) { |
| 270 | ||
| 271 | 9 | options = options || {}; |
| 272 | ||
| 273 | 9 | var moduleEventEmitter = module.event = new ModuleInitEventEmitter(module.name, options), |
| 274 | notifyDependencyFn = options.notifyDependencyFn || function() {}; | |
| 275 | ||
| 276 | // Link events | |
| 277 | 9 | moduleEventEmitter.once(exports.INIT_START, function(moduleName, options) { |
| 278 | // Do nothing | |
| 279 | }); | |
| 280 | ||
| 281 | 9 | moduleEventEmitter.once(exports.INIT_FINISH, function(moduleName, options) { |
| 282 | // Check for dependent modules, init them | |
| 283 | 8 | notifyDependencyFn(moduleName, options); |
| 284 | }); | |
| 285 | ||
| 286 | } | |
| 287 | ||
| 288 | /** | |
| 289 | * Module event emitter, object that enables modules to emit events. | |
| 290 | * This contains both server and request scope event emitters, though clearly | |
| 291 | * an instance of an object only emits one or the other depending on | |
| 292 | * where it is instantiated. | |
| 293 | */ | |
| 294 | 1 | function ModuleRequestEventEmitter(moduleName, options) { |
| 295 | ||
| 296 | 17 | events.EventEmitter.call(this); |
| 297 | ||
| 298 | // Refresh the require | |
| 299 | 17 | var self = this; |
| 300 | 17 | self.options = options || {}; |
| 301 | 17 | self.moduleName = moduleName; |
| 302 | ||
| 303 | // Set the max listeners | |
| 304 | 17 | var maxListeners = self.options.maxListeners || 100; |
| 305 | 17 | this.setMaxListeners(maxListeners); |
| 306 | ||
| 307 | 17 | this.route_start = function(options) { |
| 308 | 9 | self.emit(exports.ROUTE_START, self.moduleName, options); |
| 309 | }; | |
| 310 | ||
| 311 | 17 | this.route_finish = function(options) { |
| 312 | 9 | self.emit(exports.ROUTE_FINISH, self.moduleName, options); |
| 313 | }; | |
| 314 | ||
| 315 | } | |
| 316 | ||
| 317 | /** | |
| 318 | * Event listener linked to the request object | |
| 319 | * This is the object that will listen to each module event emitter | |
| 320 | * and call other modules or perform other defined functions | |
| 321 | */ | |
| 322 | ||
| 323 | 1 | function RequestEventListener(options) { |
| 324 | ||
| 325 | 5 | options = options || {}; |
| 326 | ||
| 327 | // Register a module, listen to its events | |
| 328 | 5 | var self = this, |
| 329 | notifyDependencyFn = options.notifyDependencyFn || function() {}, | |
| 330 | registerDependenciesFn = options.registerDependenciesFn || function() {}; | |
| 331 | ||
| 332 | // Local hash of module event emitters, used to track routing status | |
| 333 | 5 | this.modules = {}; |
| 334 | ||
| 335 | // Register a module | |
| 336 | 5 | this.registerModule = function(req, res, moduleName, options) { |
| 337 | ||
| 338 | // Register event emitter | |
| 339 | 17 | var moduleEventEmitter = self.modules[moduleName] = new ModuleRequestEventEmitter(moduleName, options); |
| 340 | ||
| 341 | // Configure event listener | |
| 342 | 17 | self.modules[moduleName].routed = false; // Is it done |
| 343 | 17 | self.modules[moduleName].check = {}; // Hash of dependent modules to check if initialised |
| 344 | ||
| 345 | // Curried function to notify dependent modules that we have finished | |
| 346 | 17 | var notifyDependencies = function(moduleName) { |
| 347 | 9 | notifyDependencyFn(req, res, moduleName, self.modules); |
| 348 | }; | |
| 349 | ||
| 350 | 17 | registerDependenciesFn(self, moduleName); |
| 351 | ||
| 352 | // Start | |
| 353 | 17 | moduleEventEmitter.once(exports.ROUTE_START, function(moduleName, options) { |
| 354 | 9 | self.modules[moduleName].start = new Date(); |
| 355 | }); | |
| 356 | ||
| 357 | // Finish | |
| 358 | 17 | moduleEventEmitter.once(exports.ROUTE_FINISH, function(moduleName, options) { |
| 359 | ||
| 360 | 9 | self.modules[moduleName].finish = new Date(); |
| 361 | 9 | self.modules[moduleName].duration = self.modules[moduleName].finish - self.modules[moduleName].start; |
| 362 | 9 | self.modules[moduleName].routed = true; |
| 363 | ||
| 364 | // Callback to Calipso to notify dependent objects of route | |
| 365 | // calipso.notifyDependenciesOfRoute(req, res, moduleName, self.modules); | |
| 366 | 9 | notifyDependencies(moduleName); |
| 367 | ||
| 368 | }); | |
| 369 | ||
| 370 | }; | |
| 371 | ||
| 372 | } | |
| 373 | ||
| 374 | /** | |
| 375 | * Inherits | |
| 376 | */ | |
| 377 | 1 | util.inherits(ModuleInitEventEmitter, events.EventEmitter); |
| 378 | 1 | util.inherits(ModuleRequestEventEmitter, events.EventEmitter); |
| 379 | ||
| 380 | ||
| 381 | /** | |
| 382 | * Helper functions TODO CONSOLIDATE! | |
| 383 | */ | |
| 384 | ||
| 385 | 1 | function mergeArray(arr, first) { |
| 386 | 4 | var output = {}; |
| 387 | 4 | arr.forEach(function(value, key) { |
| 388 | 8 | if (first) { |
| 389 | 0 | output = merge(value, output); |
| 390 | } else { | |
| 391 | 8 | output = merge(output, value); |
| 392 | } | |
| 393 | }); | |
| 394 | 4 | return output; |
| 395 | } | |
| 396 | ||
| 397 | 1 | function merge(a, b) { |
| 398 | 8 | if (a && b) { |
| 399 | 8 | for (var key in b) { |
| 400 | 16 | a[key] = b[key]; |
| 401 | } | |
| 402 | } | |
| 403 | 8 | return a; |
| 404 | } |
| Line | Hits | Source |
|---|---|---|
| 1 | /*!a | |
| 2 | * Calipso Form Library | |
| 3 | * | |
| 4 | * Copyright(c) 2011 Clifton Cunningham <clifton.cunningham@gmail.com> | |
| 5 | * MIT Licensed | |
| 6 | * | |
| 7 | * Core form generation module. | |
| 8 | * | |
| 9 | * This is loaded by calipso as a plugin, so can be replaced by modules. | |
| 10 | * Module must expose a single object as below, with a single | |
| 11 | * function that is called by other modules to generate form markup. | |
| 12 | * | |
| 13 | * This is most likely a synchronous call, but has been written asynch just | |
| 14 | * in case a module author wants to make an asynch version (for some reason!). | |
| 15 | * | |
| 16 | * TODO: validation, redisplay of submitted values | |
| 17 | * | |
| 18 | */ | |
| 19 | ||
| 20 | ||
| 21 | 1 | var rootpath = process.cwd() + '/', |
| 22 | path = require('path'), | |
| 23 | calipso = require(path.join('..', 'calipso')), | |
| 24 | qs = require('qs'), | |
| 25 | merge = require('connect').utils.merge; | |
| 26 | ||
| 27 | // Global variable (in this context) for translation function | |
| 28 | 1 | var t; |
| 29 | ||
| 30 | /** | |
| 31 | * The default calipso form object, with default configuration values. | |
| 32 | * Constructor | |
| 33 | */ | |
| 34 | 1 | function Form() { |
| 35 | ||
| 36 | // TODO - tagStyle should also affect whether attributes can be minimised ('selected' vs. 'selected="selected"') | |
| 37 | ||
| 38 | // tagStyle should be one of [html, xhtml, xml] | |
| 39 | 1 | this.tagStyle = "html"; |
| 40 | ||
| 41 | // adjust the way tags are closed based on the given tagStyle. | |
| 42 | 1 | this.tagClose = this.tagStyle == "html" ? '>' : ' />'; |
| 43 | ||
| 44 | // cheap way of ensuring unique radio ids | |
| 45 | 1 | this.radioCount = 0; |
| 46 | ||
| 47 | } | |
| 48 | ||
| 49 | 1 | var f = new Form(); |
| 50 | ||
| 51 | 1 | var me = Form.prototype; |
| 52 | ||
| 53 | // instead of referring to the singleton (`f`), we could implement a function | |
| 54 | // that would give us the current instance, for a more sure `this` | |
| 55 | // but it would be a little bit slower, due to the function call | |
| 56 | //me.getInstance = function(){ | |
| 57 | // return this; | |
| 58 | //}; | |
| 59 | //me.getContext = function(){ | |
| 60 | // return this; | |
| 61 | //}; | |
| 62 | ||
| 63 | /* just an idea. | |
| 64 | function getAttributeString(el){ | |
| 65 | var validAttrs = ['type','name','id','class','value','disabled']; | |
| 66 | var output = ''; | |
| 67 | validAttrs.forEach(function(i, attrName){ | |
| 68 | if(el[attrName]){ | |
| 69 | output += ' ' + attrName + '="' + el.attr[attrName] + '"'; | |
| 70 | } | |
| 71 | }); | |
| 72 | return output; | |
| 73 | } | |
| 74 | */ | |
| 75 | ||
| 76 | // if complete for every country, this will be a lot of data and should | |
| 77 | // probably be broken out to a separate file. | |
| 78 | 1 | me.countries = [ |
| 79 | "Afghanistan", "Albania", "Algeria", "Andorra", "Angola", | |
| 80 | "Antigua and Barbuda", "Argentina", "Armenia", "Australia", "Austria", | |
| 81 | "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", | |
| 82 | "Belgium", "Belize", "Benin", "Bhutan", "Bolivia", "Bosnia and Herzegovina", | |
| 83 | "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", "Burundi", | |
| 84 | "Cambodia", "Cameroon", "Canada", "Cape Verde", "Central African Republic", | |
| 85 | "Chad", "Chile", "China (People's Republic of China)", "Colombia", "Comoros", | |
| 86 | "Democratic Republic of the Congo", "Republic of the Congo", | |
| 87 | "Costa Rica", "Côte d'Ivoire", "Croatia", "Cuba", "Cyprus", | |
| 88 | "Czech Republic", "Denmark, the Kingdom of", "Djibouti", "Dominica", | |
| 89 | "Dominican Republic", "East Timor", "Ecuador", "Egypt", "El Salvador", | |
| 90 | "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Fiji", | |
| 91 | "Finland", "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", | |
| 92 | "Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", | |
| 93 | "Haiti", "Honduras", "Hungary", "Iceland", "India", "Indonesia", "Iran", | |
| 94 | "Iraq", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan", | |
| 95 | "Kazakhstan", "Kenya", "Kiribati", "North Korea", "South Korea", | |
| 96 | "Kosovo", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon", | |
| 97 | "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", | |
| 98 | "Macedonia", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", | |
| 99 | "Malta", "Marshall Islands", "Mauritania", "Mauritius", "Mexico", | |
| 100 | "Federated States of Micronesia", "Moldova", "Monaco", "Mongolia", | |
| 101 | "Montenegro", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", | |
| 102 | "Nepal", "Netherlands, the Kingdom of", "New Zealand", "Nicaragua", "Niger", | |
| 103 | "Nigeria", "Norway", "Oman", "Pakistan", "Palau", "Palestinian territories", | |
| 104 | "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", | |
| 105 | "Portugal", "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", | |
| 106 | "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", | |
| 107 | "São Tomé and PrÃncipe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", | |
| 108 | "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", | |
| 109 | "Somalia", "South Africa", "South Sudan", "Spain", "Sri Lanka", "Sudan", | |
| 110 | "Suriname", "Swaziland", "Sweden", "Switzerland", "Syria", | |
| 111 | "Taiwan (Republic of China)", "Tajikistan", "Tanzania", "Thailand", "Togo", | |
| 112 | "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", | |
| 113 | "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", | |
| 114 | "United States", "Uruguay", "Uzbekistan", "Vanuatu", "Vatican City", | |
| 115 | "Venezuela", "Vietnam", "Western Sahara", "Yemen", "Zambia", "Zimbabwe" | |
| 116 | ]; | |
| 117 | ||
| 118 | 1 | me.states = { |
| 119 | "United States": { | |
| 120 | AL:"Alabama", | |
| 121 | AK:"Alaska", | |
| 122 | AZ:"Arizona", | |
| 123 | AR:"Arkansas", | |
| 124 | CA:"California", | |
| 125 | CO:"Colorado", | |
| 126 | CT:"Connecticut", | |
| 127 | DE:"Delaware", | |
| 128 | DC:"District Of Columbia", | |
| 129 | FL:"Florida", | |
| 130 | GA:"Georgia", | |
| 131 | HI:"Hawaii", | |
| 132 | ID:"Idaho", | |
| 133 | IL:"Illinois", | |
| 134 | IN:"Indiana", | |
| 135 | IA:"Iowa", | |
| 136 | KS:"Kansas", | |
| 137 | KY:"Kentucky", | |
| 138 | LA:"Louisiana", | |
| 139 | ME:"Maine", | |
| 140 | MD:"Maryland", | |
| 141 | MA:"Massachusetts", | |
| 142 | MI:"Michigan", | |
| 143 | MN:"Minnesota", | |
| 144 | MS:"Mississippi", | |
| 145 | MO:"Missouri", | |
| 146 | MT:"Montana", | |
| 147 | NE:"Nebraska", | |
| 148 | NV:"Nevada", | |
| 149 | NH:"New Hampshire", | |
| 150 | NJ:"New Jersey", | |
| 151 | NM:"New Mexico", | |
| 152 | NY:"New York", | |
| 153 | NC:"North Carolina", | |
| 154 | ND:"North Dakota", | |
| 155 | OH:"Ohio", | |
| 156 | OK:"Oklahoma", | |
| 157 | OR:"Oregon", | |
| 158 | PA:"Pennsylvania", | |
| 159 | RI:"Rhode Island", | |
| 160 | SC:"South Carolina", | |
| 161 | SD:"South Dakota", | |
| 162 | TN:"Tennessee", | |
| 163 | TX:"Texas", | |
| 164 | UT:"Utah", | |
| 165 | VT:"Vermont", | |
| 166 | VA:"Virginia", | |
| 167 | WA:"Washington", | |
| 168 | WV:"West Virginia", | |
| 169 | WI:"Wisconsin", | |
| 170 | WY:"Wyoming" | |
| 171 | } | |
| 172 | }; | |
| 173 | ||
| 174 | ||
| 175 | /** | |
| 176 | * Functions for each tag type, these are now exposed directly on the object | |
| 177 | * so that they can be redefined within modules (e.g. in a module that provides | |
| 178 | * a rich text editor), or a module can add new types specific to that module. | |
| 179 | * | |
| 180 | * Current field types available are: | |
| 181 | * | |
| 182 | * text : default text field (used if no function matches field type) | |
| 183 | * textarea : default textarea, can set rows in form definition to control rows in a textarea field item. | |
| 184 | * hidden : hidden field | |
| 185 | * select : single select box, requires values to be set (as array or function) | |
| 186 | * submit : submit button | |
| 187 | * button : general button | |
| 188 | * date : date input control (very rudimentary) | |
| 189 | * time : time input controls | |
| 190 | * datetime : combination of date and time controls | |
| 191 | * crontime : crontime editor (6 small text boxes) | |
| 192 | * password : password field | |
| 193 | * checkbox : checkbox field | |
| 194 | * radio : radio button | |
| 195 | * file : file field | |
| 196 | * | |
| 197 | **/ | |
| 198 | ||
| 199 | 1 | me.defaultTagRenderer = function(field, value, bare){ |
| 200 | 0 | var isCheckable = field.type == 'radio' || field.type == 'checkbox'; |
| 201 | 0 | var checked = field.checked || (isCheckable && value && (field.value == value || value===true)); |
| 202 | ||
| 203 | //console.log('... field: ', field, value); | |
| 204 | 0 | var tagOutput = ""; |
| 205 | ||
| 206 | 0 | if(field.type == 'checkbox' && !field.readonly && !field.disabled){ |
| 207 | // Workaround for readonly/disabled fields (esp. checkboxes/radios) - add a hidden field with the value | |
| 208 | 0 | tagOutput += '<input type="hidden" name="' + field.name + '" value="false" />'; |
| 209 | } | |
| 210 | ||
| 211 | 0 | tagOutput += '<input type="' + field.type + '"' |
| 212 | + ' class="'+ field.type + (field.cls ? ' ' + field.cls : "") + (field.labelFirst ? ' labelFirst' : '') + '"' | |
| 213 | + ' name="' + field.name + '"' | |
| 214 | + (field.href ? ' onClick=\'window.location="' + field.href + '"\';' : '') | |
| 215 | + ' id="' + (field.id ? field.id : field.name + (field.type=='radio' ? (++f.radioCount) : '')) + '"' | |
| 216 | + (field.src ? ' src="' + field.src + '"' : '') // for input type=image .. which should be avoided anyway. | |
| 217 | + (field.multiple ? ' multiple="' + field.multiple + '"' : '') // for input type=file | |
| 218 | + (field.directory ? ' mozdirectory webkitdirectory directory' : '') //for input type=file | |
| 219 | + ' value="' + calipso.utils.escapeHtmlQuotes(value || field.value || (isCheckable && 'on') || '') + '"' | |
| 220 | + (field.readonly || field.disabled ? ' disabled' : '') | |
| 221 | + (checked ? ' checked' : '') | |
| 222 | + f.tagClose; | |
| 223 | 0 | if(field.readonly || field.disabled){ |
| 224 | // Workaround for readonly/disabled fields (esp. checkboxes/radios) - add a hidden field with the value | |
| 225 | 0 | tagOutput += '<input type="hidden" name="' + field.name + '" value="' + (checked ? 'true' : 'false') + '" />'; |
| 226 | } | |
| 227 | ||
| 228 | 0 | return bare ? tagOutput : me.decorateField(field, tagOutput); |
| 229 | ||
| 230 | }; | |
| 231 | ||
| 232 | 1 | me.decorateField = function(field, tagHTML){ |
| 233 | 0 | calipso.silly('FORM: decorateField, field: ', field); |
| 234 | 0 | var isCheckable = !field.labelFirst && (field.type == "checkbox" || field.type == "radio"); |
| 235 | 0 | var labelHTML = field.label ? ( |
| 236 | '<label' + (isCheckable ? ' class="for-checkable"' : '') | |
| 237 | + ' for="' + field.name + (field.type == 'radio' ? f.radioCount : '') | |
| 238 | + '">' + t(field.label) + (isCheckable ? '' : ':') + '</label>' | |
| 239 | ) : ''; | |
| 240 | ||
| 241 | 0 | var wrapperId = ( |
| 242 | field.name.replace(/\[/g, '_').replace(/\]/g, '') | |
| 243 | + (field.type == 'radio' ? '-radio' + me.radioCount : '') | |
| 244 | ); | |
| 245 | ||
| 246 | 0 | return field.label && field.label.length > 0 ? ( |
| 247 | '<div class="form-item field-type-' + field.type + '" id="' + wrapperId + '-wrapper">' + | |
| 248 | '<div class="form-field">' + | |
| 249 | // put checkboxes and radios ("checkables") before their labels, unless field.labelFirst is true | |
| 250 | (isCheckable ? tagHTML + labelHTML : labelHTML + tagHTML) + | |
| 251 | '</div>' + | |
| 252 | (field.description ? '<span class="description ' + field.type + '-description">' + t(field.description) + '</span>' : '') + | |
| 253 | '</div>' | |
| 254 | ) : tagHTML; | |
| 255 | }; | |
| 256 | ||
| 257 | // if there is no `canContain` or `cannotContain`, then the element is not a container. | |
| 258 | // the following psuedofunction should suffice: | |
| 259 | // x.canContain(y) = | |
| 260 | // ((x.canContain && y in x.canContain) || (x.cannotContain && !(y in x.cannotContain))) | |
| 261 | // && (!y.canBeContainedBy || x in y.canBeContainedBy) | |
| 262 | 1 | me.elementTypes = { |
| 263 | ||
| 264 | 'page': { | |
| 265 | cannotContain: ['page'], | |
| 266 | render: function(el){} | |
| 267 | }, | |
| 268 | ||
| 269 | 'section': { | |
| 270 | cannotContain: ['page'], | |
| 271 | isTab : false, | |
| 272 | render: function(el, values, isTabs){ | |
| 273 | 0 | return ( |
| 274 | '<section' + (el.isTab || isTabs ? ' class="tab-content"':'') + ' id="' + el.id + '">' + | |
| 275 | (el.label ? '<h3>' + t(el.label) + '</h3>' : '') + | |
| 276 | (el.description ? '<p>' + el.description + '</p>' : '') + | |
| 277 | '<div class="section-fields">' + | |
| 278 | me.render_fields(el, values) + | |
| 279 | '</div>' + | |
| 280 | '</section>' | |
| 281 | ); | |
| 282 | } | |
| 283 | }, | |
| 284 | ||
| 285 | // todo: allow for pre-rendered markup for the description, or other renderers (such as markdown) | |
| 286 | 'fieldset': { | |
| 287 | cannotContain: ['section', 'page'], | |
| 288 | render: function(el, values){ | |
| 289 | 0 | if(!el.label) el.label = el.legend; |
| 290 | 0 | return ( |
| 291 | '<fieldset class="' + (el.type != 'fieldset' ? el.type + '-fieldset' : 'fieldset') + '">' + | |
| 292 | // <legend> is preferable, but legends are not fully stylable, so 'label' = <h4> | |
| 293 | (el.label ? '<h4>' + t(el.label) + '</h4>' : '') + | |
| 294 | (el.description ? '<p>' + el.description + '</p>' : '') + | |
| 295 | '<div class="fieldset-fields">' + | |
| 296 | me.render_fields(el, values) + | |
| 297 | '</div>' + | |
| 298 | '</fieldset>' | |
| 299 | ); | |
| 300 | } | |
| 301 | }, | |
| 302 | ||
| 303 | // special .. might also be used as a container (i.e., depending on what radio is active, elements 'under' it are active?) | |
| 304 | // special - have to share ids .. are part of a set - TODO - allow for more than one radio group (already done?) | |
| 305 | 'radios': { // it's a container because radios must belong to a 'set' .. also, sometimes a form uses radios kindof like tabs.... | |
| 306 | canContain: ['option'], | |
| 307 | render: function(field, values){ | |
| 308 | 0 | return me.elementTypes.fieldset.render(field, values); |
| 309 | } | |
| 310 | }, | |
| 311 | ||
| 312 | // special .. might also be used as a container (i.e., depending on whether a checkbox is checked, elements 'under' it are active?) | |
| 313 | 'checkboxes': { | |
| 314 | canContain: ['option'], | |
| 315 | render: function(field, values){ | |
| 316 | 0 | return me.elementTypes.fieldset.render(field, values); |
| 317 | } | |
| 318 | }, | |
| 319 | ||
| 320 | 'select': { // it's a container because it contains options | |
| 321 | canContain: ['options','optgroup'], | |
| 322 | render: function(field, value){ | |
| 323 | ||
| 324 | 0 | var tagOutput = '<select' |
| 325 | + ' class="select ' + (field.cls ? field.cls : "") + '"' | |
| 326 | + ' name="' + field.name + '"' | |
| 327 | + ' id="' + field.name + '"' | |
| 328 | + (field.multiple ? ' multiple="multiple"' : '') | |
| 329 | + '>'; | |
| 330 | ||
| 331 | 0 | var options = typeof field.options === 'function' ? field.options() : field.options; |
| 332 | ||
| 333 | 0 | if(field.optgroups){ |
| 334 | 0 | field.optgroups.forEach(function(optgroup){ |
| 335 | 0 | tagOutput += '<optgroup label="' + optgroup.label + '">'; |
| 336 | 0 | optgroup.options.forEach(function(option){ |
| 337 | 0 | tagOutput += me.elementTypes.option.render(option, value, 'select'); |
| 338 | }); | |
| 339 | 0 | tagOutput += '</optgroup>'; |
| 340 | }); | |
| 341 | } else { | |
| 342 | 0 | options.forEach(function(option){ |
| 343 | 0 | tagOutput += me.elementTypes.option.render(option, value, 'select'); |
| 344 | }); | |
| 345 | } | |
| 346 | 0 | tagOutput += '</select>'; |
| 347 | ||
| 348 | 0 | return me.decorateField(field, tagOutput); |
| 349 | } | |
| 350 | }, | |
| 351 | ||
| 352 | 'optgroup': { | |
| 353 | canBeContainedBy: ['select'], | |
| 354 | canContain: ['option'] | |
| 355 | }, | |
| 356 | ||
| 357 | 'options': { | |
| 358 | canBeContainedBy: ['select'], | |
| 359 | canContain: ['option'] | |
| 360 | }, | |
| 361 | ||
| 362 | // an "option" can be an <option> or a radio or a checkbox. | |
| 363 | 'option': { | |
| 364 | canBeContainedBy: ['radios','checkboxes','select','optgroup'], | |
| 365 | // container determines render method. | |
| 366 | render: function(option, value, containerType){ | |
| 367 | 0 | if(containerType == 'select'){ |
| 368 | 0 | var displayText = option.label || option; |
| 369 | 0 | var optionValue = option.value || option; |
| 370 | 0 | return ( |
| 371 | '<option' | |
| 372 | + ' value="' + optionValue + '"' | |
| 373 | + (value === optionValue ? ' selected' : '') | |
| 374 | + (option.cls ? ' class="' + option.cls + '"' : '') | |
| 375 | + '>' | |
| 376 | + displayText | |
| 377 | + '</option>' | |
| 378 | ); | |
| 379 | } else { | |
| 380 | 0 | return me.defaultTagRenderer(option, value); |
| 381 | } | |
| 382 | } | |
| 383 | }, | |
| 384 | ||
| 385 | // type: 'radio' should become type: option, and be in a {type: radios} | |
| 386 | 'radio': { | |
| 387 | render: me.defaultTagRenderer | |
| 388 | }, | |
| 389 | ||
| 390 | // type: 'checkbox' should become type: option, and be in a {type: checkboxes} | |
| 391 | 'checkbox': { | |
| 392 | render: function(field, value, bare) { | |
| 393 | ||
| 394 | // Quickly flip values to true/false if on/off | |
| 395 | 0 | value = (value === "on" ? true : (value === "off" ? false : value)); |
| 396 | ||
| 397 | // Now set the checked variable | |
| 398 | 0 | var checked = (value ? true : (field.value ? true : (field.checked ? true : false))); |
| 399 | ||
| 400 | 0 | var tagOutput = ""; |
| 401 | ||
| 402 | 0 | if(!field.readonly && !field.disabled){ |
| 403 | // Workaround for readonly/disabled fields (esp. checkboxes/radios) - add a hidden field with the value | |
| 404 | 0 | tagOutput += '<input type="hidden" name="' + field.name + '" value="off" />'; |
| 405 | } | |
| 406 | ||
| 407 | 0 | tagOutput += '<input type="' + field.type + '"' |
| 408 | + ' class="'+ field.type + (field.cls ? ' ' + field.cls : "") + (field.labelFirst ? ' labelFirst' : '') + '"' | |
| 409 | + ' name="' + field.name + '"' | |
| 410 | + ' id="' + field.name + '"' | |
| 411 | + (field.readonly || field.disabled ? ' disabled' : '') | |
| 412 | + (checked ? ' checked' : '') | |
| 413 | + f.tagClose; | |
| 414 | ||
| 415 | 0 | if(field.readonly || field.disabled){ |
| 416 | // Workaround for readonly/disabled fields (esp. checkboxes/radios) - add a hidden field with the value | |
| 417 | 0 | tagOutput += '<input type="hidden" name="' + field.name + '" value="' + (checked ? "on" : "off") + '" />'; |
| 418 | } | |
| 419 | 0 | return bare ? tagOutput : me.decorateField(field, tagOutput); |
| 420 | } | |
| 421 | }, | |
| 422 | ||
| 423 | 'text': { | |
| 424 | render: me.defaultTagRenderer | |
| 425 | }, | |
| 426 | ||
| 427 | 'textarea': { | |
| 428 | render: function(field, value){ | |
| 429 | 0 | return me.decorateField(field, '<textarea' |
| 430 | + ' class="textarea ' + (field.cls ? field.cls : "") + '"' | |
| 431 | + ' rows="' + (field.rows ? field.rows : "10") + '"' | |
| 432 | + ' name="' + field.name + '"' | |
| 433 | + ' id="' + field.name + '"' | |
| 434 | + (field.required ? ' required' : '') | |
| 435 | + '>' | |
| 436 | + value | |
| 437 | + '</textarea>'); | |
| 438 | } | |
| 439 | }, | |
| 440 | ||
| 441 | 'hidden': { | |
| 442 | render: me.defaultTagRenderer | |
| 443 | }, | |
| 444 | ||
| 445 | 'password': { // can be special, if there is a 'verify' | |
| 446 | render: me.defaultTagRenderer | |
| 447 | }, | |
| 448 | ||
| 449 | // might allow file to take a url | |
| 450 | 'file': { | |
| 451 | render: me.defaultTagRenderer | |
| 452 | }, | |
| 453 | ||
| 454 | 'buttons': { | |
| 455 | canContain: ['submit','image','reset','button','link'] | |
| 456 | }, | |
| 457 | ||
| 458 | // buttons should only be able to be added to the 'action set' | |
| 459 | // button can be [submit, reset, cancel (a link), button (generic), link (generic)] | |
| 460 | // if form has pages, 'previous' and 'next' buttons should interpolate until the last page, 'submit' | |
| 461 | 'button': { | |
| 462 | render: me.defaultTagRenderer | |
| 463 | }, | |
| 464 | ||
| 465 | 'submit': { | |
| 466 | render: me.defaultTagRenderer | |
| 467 | }, | |
| 468 | ||
| 469 | 'image': { | |
| 470 | render: me.defaultTagRenderer | |
| 471 | }, | |
| 472 | ||
| 473 | 'reset': { | |
| 474 | render: me.defaultTagRenderer | |
| 475 | }, | |
| 476 | ||
| 477 | // a link is not really a form control, but is provided here for convenience | |
| 478 | // it also doesn't really make sense for it to have a value. | |
| 479 | // a link should have an href and text, and optionally, cls ('class'), id | |
| 480 | 'link': { | |
| 481 | render: function(field, value){ | |
| 482 | 0 | var id = field.id || field.name; |
| 483 | 0 | var text = field.text || field.value; |
| 484 | 0 | return '<a href="' + field.href + '"' |
| 485 | + ' class="form-link' + (field.cls ? ' ' + field.cls : "") + '"' | |
| 486 | + (id ? ' id="' + id + '"' : '') | |
| 487 | + '>' + text + '</a>'; | |
| 488 | } | |
| 489 | }, | |
| 490 | ||
| 491 | 'date': { | |
| 492 | render: function(field, value, bare){ | |
| 493 | ||
| 494 | 0 | if(!value) { |
| 495 | 0 | value = new Date(); |
| 496 | } | |
| 497 | ||
| 498 | // TODO - use user's Locale | |
| 499 | 0 | var monthNames = calipso.date.regional[''].monthNamesShort; |
| 500 | ||
| 501 | 0 | var tagOutput = '<input type="text"' |
| 502 | + ' class="date date-day' + (field.cls ? ' date-day-'+field.cls : '') + '"' | |
| 503 | + ' name="' + field.name + '[day]"' | |
| 504 | + ' value="' + value.getDate() + '"' | |
| 505 | + (field.required ? ' required' : '') | |
| 506 | + f.tagClose; | |
| 507 | ||
| 508 | 0 | tagOutput += ' '; |
| 509 | ||
| 510 | 0 | tagOutput += '<select class="date date-month' + (field.cls ? ' date-month-'+field.cls : '') + '"' |
| 511 | + (field.required ? ' required' : '') | |
| 512 | + ' name="' + field.name + '[month]">'; | |
| 513 | 0 | for(var monthNameCounter=0; monthNameCounter<12; monthNameCounter++) { |
| 514 | 0 | tagOutput += ( |
| 515 | '<option value="'+monthNameCounter+'"' + (value.getMonth() === monthNameCounter ? ' selected' : '') + '>' | |
| 516 | + monthNames[monthNameCounter] | |
| 517 | + '</option>' | |
| 518 | ); | |
| 519 | } | |
| 520 | 0 | tagOutput += '</select>'; |
| 521 | ||
| 522 | 0 | tagOutput += ' '; |
| 523 | ||
| 524 | 0 | tagOutput += '<input type="text"' |
| 525 | + ' class="date date-year' + (field.cls ? ' date-year-'+field.cls : '') + '"' | |
| 526 | + ' name="' + field.name + '[year]"' | |
| 527 | + ' value="' + value.getFullYear() + '"' | |
| 528 | + (field.required ? ' required' : '') | |
| 529 | + f.tagClose; | |
| 530 | ||
| 531 | 0 | return bare ? tagOutput : me.decorateField(field, tagOutput); |
| 532 | } | |
| 533 | }, | |
| 534 | ||
| 535 | 'time': { | |
| 536 | render: function(field, value, bare) { | |
| 537 | ||
| 538 | // TODO | |
| 539 | 0 | if(!value) { |
| 540 | 0 | value = new Date(); // why 1900? why not 'now'? |
| 541 | } | |
| 542 | ||
| 543 | 0 | var tagOutput = '<input type="text" class="time time-hours' + (field.cls ? ' time-hours-'+field.cls : '') + '"' |
| 544 | + ' name="' + field.name + '[hours]"' | |
| 545 | + ' value="' + value.getHours() + '"' | |
| 546 | + (field.required ? ' required' : '') | |
| 547 | + f.tagClose; | |
| 548 | ||
| 549 | 0 | tagOutput += ' '; |
| 550 | ||
| 551 | 0 | tagOutput += '<input type="text" class="time time-minutes' + (field.cls ? ' time-minutes-'+field.cls : '') + '"' |
| 552 | + ' name="' + field.name + '[minutes]"' | |
| 553 | + ' value="' + value.getMinutes() + '"' | |
| 554 | + (field.required ? ' required' : '') | |
| 555 | + f.tagClose; | |
| 556 | ||
| 557 | 0 | return bare ? tagOutput : me.decorateField(field, tagOutput); |
| 558 | ||
| 559 | } | |
| 560 | }, | |
| 561 | ||
| 562 | 'datetime': { | |
| 563 | render: function(field, value) { | |
| 564 | // Call both types | |
| 565 | 0 | return me.decorateField(field, |
| 566 | me.elementTypes.date.render({ | |
| 567 | name: field.name, | |
| 568 | type: "date", | |
| 569 | required: field.required | |
| 570 | }, value, true) + | |
| 571 | ' ' + | |
| 572 | me.elementTypes.time.render({ | |
| 573 | name: field.name, | |
| 574 | type: "time", | |
| 575 | required: field.required | |
| 576 | }, value, true) | |
| 577 | ); | |
| 578 | } | |
| 579 | }, | |
| 580 | ||
| 581 | 'crontime': { | |
| 582 | render: function(field, value) { | |
| 583 | 0 | var tagOutput = ''; |
| 584 | 0 | var cronTimeValues = value ? value.split(/\s/) : ['*','*','*','*','*','*']; |
| 585 | 0 | for(var cronTimeInputCounter = 0; cronTimeInputCounter < 6; cronTimeInputCounter++) { |
| 586 | 0 | tagOutput += ( |
| 587 | '<input type="text" class="text crontime" value="' + | |
| 588 | cronTimeValues[cronTimeInputCounter] + | |
| 589 | '" name="job[cronTime' + cronTimeInputCounter + ']"' + | |
| 590 | (field.required ? ' required' : '') + | |
| 591 | f.tagClose | |
| 592 | ); | |
| 593 | } | |
| 594 | 0 | return me.decorateField(field, tagOutput); |
| 595 | } | |
| 596 | } | |
| 597 | ||
| 598 | }; | |
| 599 | ||
| 600 | // any element types that reference other element types have to be declared afterward | |
| 601 | // so that the references exist. | |
| 602 | 1 | me.elementTypes.richtext = { |
| 603 | render: me.elementTypes.textarea.render | |
| 604 | }; | |
| 605 | ||
| 606 | 1 | me.elementTypes.json = { |
| 607 | render: me.elementTypes.textarea.render | |
| 608 | }; | |
| 609 | ||
| 610 | 1 | me.elementTypes.email = { |
| 611 | render: function(field, value){ | |
| 612 | //var _field = copyProperties(field, {}); | |
| 613 | //_field.type = 'text'; | |
| 614 | 0 | field.type = 'text'; |
| 615 | 0 | field.cls = (field.cls ? field.cls + ' ' : '') + 'email'; |
| 616 | 0 | me.elementTypes.textarea.render(field, value); |
| 617 | } | |
| 618 | }; | |
| 619 | ||
| 620 | 1 | me.elementTypes.address = { |
| 621 | render: me.elementTypes.fieldset.render, | |
| 622 | defaultDefinition: { | |
| 623 | tag: 'fieldset', | |
| 624 | type: 'address', | |
| 625 | children: [ | |
| 626 | {type: 'text', name: 'street', label: 'Street Address'}, | |
| 627 | {type: 'select', name: 'country', label: 'Country', options: me.countries}, | |
| 628 | {type: 'select', name: 'state', label: 'State', options: me.states["United States"]}, | |
| 629 | {type: 'text', name: 'postalcode', label: 'Postal Code'} | |
| 630 | ] | |
| 631 | } | |
| 632 | }; | |
| 633 | ||
| 634 | ||
| 635 | /** | |
| 636 | * Form Renderer, controls the overall creation of the form based on a form json object passed | |
| 637 | * in as the first parameter. The structure of this object is as follows: | |
| 638 | * | |
| 639 | * form | |
| 640 | * id : Unique ID that will become the form ID. | |
| 641 | * title : Title to show at the top of the form. | |
| 642 | * type : Type of form (not used at present). | |
| 643 | * method: HTTP method to use. | |
| 644 | * action : URL to submit form to. | |
| 645 | * tabs : Should tabs be rendered for sections (default false). | |
| 646 | * sections [*] : Optional - divide the form into sections. | |
| 647 | * id : Unique identifier for a section. | |
| 648 | * label : Description of section (appears as header or tab label) | |
| 649 | * fields [*] : Array of fields in the section (see below). | |
| 650 | * fields [*] : Form fields array - can be in form or section. | |
| 651 | * label : Label for form field. | |
| 652 | * name : Name of form element to be passed back with the value. | |
| 653 | * type : Type of element, based on the form functions defined below. | |
| 654 | * description : Description text to be rendered after the element in a div tag. | |
| 655 | * buttons [*] : Array of buttons to be rendered at the bottom of the form. | |
| 656 | * name : Name of button (for submission). | |
| 657 | * type : Type of button. | |
| 658 | * value : Value to submit when pressed. | |
| 659 | * | |
| 660 | * A complete example is shown below: | |
| 661 | * | |
| 662 | * var myForm = {id:'my-form',title:'Create My Thing...',type:'form',method:'POST',action:'/myaction',tabs:false, | |
| 663 | * sections:[{ | |
| 664 | * id:'myform-section-1', | |
| 665 | * label:'Section 1', | |
| 666 | * fields:[ | |
| 667 | * {label:'Field A',name:'object[fieldA]',type:'text',description:'Description ... '}, | |
| 668 | * {label:'Field B',name:'object[fieldB]',type:'textarea',description:'Description ...'} | |
| 669 | * ] | |
| 670 | * },{ | |
| 671 | * id:'myform-section2', | |
| 672 | * label:'Section 2', | |
| 673 | * fields:[ | |
| 674 | * {label:'Select Field',name:'object[select]',type:'select',options:["option 1","option 2"],description:'Description...'}, | |
| 675 | * {label:'Date Field',name:'object[date]',type:'datetime',description:'Description...'}, | |
| 676 | * ] | |
| 677 | * } | |
| 678 | * ], | |
| 679 | * fields:[ | |
| 680 | * {label:'',name:'hiddenField',type:'hidden'} | |
| 681 | * ], | |
| 682 | * buttons:[ | |
| 683 | * {name:'submit',type:'submit',value:'Save'} | |
| 684 | * ]}; | |
| 685 | * | |
| 686 | * The values of the form are passed through (optionally) as the second parameter. This allows you to re-use | |
| 687 | * a form definition across different uses (e.g. CRU). | |
| 688 | * | |
| 689 | * @param item : the json object representing the form | |
| 690 | * @param values : The values to initialise the form with | |
| 691 | * @param next : Callback when done, pass markup as return val (TODO : deprecate this, then can use form.render in views) | |
| 692 | */ | |
| 693 | 1 | me.render = function(formJson, values, req, next) { |
| 694 | ||
| 695 | 0 | var self = this; |
| 696 | ||
| 697 | // Store local reference to the request for use during translation | |
| 698 | 0 | t = req.t; |
| 699 | ||
| 700 | // Emit a form pre-render event. | |
| 701 | 0 | calipso.e.custom_emit('FORM', formJson.id, formJson, function(formJson) { |
| 702 | ||
| 703 | 0 | var form = ( |
| 704 | self.start_form(formJson) + | |
| 705 | self.render_sections(formJson, values) + // todo: deprecate - sections should be treated the same as any other field (container) | |
| 706 | self.render_fields(formJson, values) + | |
| 707 | self.render_buttons(formJson.buttons) + // todo: deprecate - buttons should be treated the same as any other field (container) | |
| 708 | self.end_form(formJson) | |
| 709 | ); | |
| 710 | ||
| 711 | // Save the form object in session, this enables us to use it later | |
| 712 | // To parse the incoming data and validate against. | |
| 713 | // Saving it in the session allows for per-user customisation of the form without | |
| 714 | // impacting this code. | |
| 715 | 0 | saveFormInSession(formJson, req, function(err) { |
| 716 | 0 | if(err) calipso.error(err.message); |
| 717 | 0 | next(form); |
| 718 | }) | |
| 719 | ||
| 720 | }); | |
| 721 | ||
| 722 | }; | |
| 723 | ||
| 724 | /** | |
| 725 | * Helper to save a form in the session to be used later when processing. | |
| 726 | */ | |
| 727 | 1 | function saveFormInSession(form, req, next) { |
| 728 | ||
| 729 | // If we have a form id and a session, save it | |
| 730 | 0 | if(form.id && calipso.lib._.keys(req.session).length > 0) { |
| 731 | 0 | calipso.silly("Saving form " + form.id + " in session."); |
| 732 | 0 | req.session.forms = req.session.forms || {}; |
| 733 | 0 | req.session.forms[form.id] = form; |
| 734 | 0 | req.session.save(next); |
| 735 | } else { | |
| 736 | 0 | next(); |
| 737 | } | |
| 738 | } | |
| 739 | ||
| 740 | /** | |
| 741 | * Deal with form tabs in jQuery UI style if required. | |
| 742 | */ | |
| 743 | 1 | me.formTabs = function(sections) { |
| 744 | ||
| 745 | 0 | if(!sections) |
| 746 | 0 | return ''; |
| 747 | ||
| 748 | 0 | var tabOutput = '<nav><ul class="tabs">', |
| 749 | numSections = sections.length; | |
| 750 | ||
| 751 | 0 | sections.forEach( function(section, index) { |
| 752 | 0 | var classes = 'form-tab'; |
| 753 | 0 | if (index === 0) { |
| 754 | 0 | classes += ' first'; |
| 755 | } | |
| 756 | 0 | if ((index + 1) === numSections) { |
| 757 | 0 | classes += ' last'; |
| 758 | } | |
| 759 | 0 | tabOutput += '<li class="' + classes + '"><a href="#' + section.id + '">' + t(section.label) + '</a></li>'; |
| 760 | }); | |
| 761 | 0 | return tabOutput + '</ul></nav>'; |
| 762 | ||
| 763 | }; | |
| 764 | ||
| 765 | ||
| 766 | /** | |
| 767 | * Render the initial form tag | |
| 768 | * | |
| 769 | * @param form | |
| 770 | * @returns {String} | |
| 771 | */ | |
| 772 | 1 | me.start_form = function(form) { |
| 773 | 0 | return ( |
| 774 | '<form id="' + form.id + '" name="' + form.id + '"' + (form.cls ? ' class="' + form.cls + '"' : "") + | |
| 775 | ' method="' + form.method + '"' + ' enctype="' + (form.enctype ? form.enctype : "multipart/form-data") + '"' + ' action="' + form.action + '">' + | |
| 776 | '<input type="hidden" value="' + form.id + '" name="form[id]"/>' + | |
| 777 | '<header class="form-header">' + | |
| 778 | '<h2>' + t(form.title) + '</h2>' + | |
| 779 | '</header>' + | |
| 780 | '<div class="form-container">' + | |
| 781 | (form.tabs ? this.formTabs(form.sections) : '') + | |
| 782 | '<div class="form-fields'+(form.tabs ? ' tab-container' : '')+'">' | |
| 783 | ); | |
| 784 | }; | |
| 785 | ||
| 786 | /** | |
| 787 | * Close the form | |
| 788 | * @param form | |
| 789 | * @returns {String} | |
| 790 | */ | |
| 791 | 1 | me.end_form = function(form) { |
| 792 | 0 | return '</div></div></form>'; |
| 793 | }; | |
| 794 | ||
| 795 | ||
| 796 | ||
| 797 | /** | |
| 798 | * Render the form sections, iterating through and then rendering | |
| 799 | * each of the fields within a section. | |
| 800 | */ | |
| 801 | 1 | me.render_sections = function(form, values) { |
| 802 | ||
| 803 | 0 | var self = this; |
| 804 | 0 | var sections = form.sections; |
| 805 | ||
| 806 | 0 | if(!sections) |
| 807 | 0 | return ''; |
| 808 | ||
| 809 | 0 | var sectionOutput = ''; |
| 810 | ||
| 811 | 0 | sections.forEach(function(section) { |
| 812 | 0 | sectionOutput += ( |
| 813 | '<section' + (form.tabs?' class="tab-content"':'') + ' id="' + section.id + '">' + | |
| 814 | '<h3>' + t(section.label) + '</h3>' + | |
| 815 | self.render_fields(section, values) + | |
| 816 | '</section>' | |
| 817 | ); | |
| 818 | }); | |
| 819 | 0 | return sectionOutput; |
| 820 | ||
| 821 | }; | |
| 822 | ||
| 823 | ||
| 824 | /** | |
| 825 | * Render the buttons on a form | |
| 826 | * @param buttons | |
| 827 | * @returns {String} | |
| 828 | */ | |
| 829 | 1 | me.render_buttons = function(buttons) { |
| 830 | ||
| 831 | 0 | var self = this; |
| 832 | 0 | var buttonsOutput = '<div class="actions">'; |
| 833 | ||
| 834 | 0 | buttons.forEach(function(field) { |
| 835 | 0 | buttonsOutput += self.elementTypes[field.tag || field.type].render(field); |
| 836 | }); | |
| 837 | ||
| 838 | 0 | buttonsOutput += '</div>'; |
| 839 | ||
| 840 | 0 | return buttonsOutput; |
| 841 | }; | |
| 842 | ||
| 843 | ||
| 844 | ||
| 845 | /** | |
| 846 | * Render the fields on a form | |
| 847 | * @param fields | |
| 848 | * @returns {String} | |
| 849 | */ | |
| 850 | 1 | me.render_fields = function(fieldContainer, values) { |
| 851 | ||
| 852 | 0 | var fields = fieldContainer.fields || fieldContainer.children; |
| 853 | 0 | var self = this; |
| 854 | 0 | var fieldOutput = ''; |
| 855 | ||
| 856 | 0 | if(!fields) { |
| 857 | 0 | return ''; |
| 858 | } | |
| 859 | ||
| 860 | 0 | fields.forEach( function(field) { |
| 861 | ||
| 862 | 0 | var value = ''; |
| 863 | 0 | var fieldName = field.name; |
| 864 | ||
| 865 | // If we have a field name, lookup the value | |
| 866 | 0 | if(fieldName) { |
| 867 | 0 | value = getValueForField(fieldName, values); |
| 868 | } | |
| 869 | ||
| 870 | // if the 'field' is really just a container, pass the values on down | |
| 871 | // todo: consider adding a property 'isContainer' | |
| 872 | 0 | if(field.type == 'section' || field.type == 'fieldset'){ |
| 873 | 0 | value = values; |
| 874 | } | |
| 875 | ||
| 876 | // field.tag was introduced to allow for <button type="submit"> (without tag:button, that would be <input type="submit">) | |
| 877 | 0 | if(self.elementTypes[field.tag || field.type]){ |
| 878 | 0 | fieldOutput += self.elementTypes[field.tag || field.type].render(field, value, fieldContainer.tabs); //self.render_field(field, value); |
| 879 | } else { | |
| 880 | 0 | calipso.warn('No renderer for ', field); |
| 881 | } | |
| 882 | ||
| 883 | }); | |
| 884 | ||
| 885 | 0 | return fieldOutput; |
| 886 | }; | |
| 887 | ||
| 888 | /** | |
| 889 | * Get the value for a form field from the values object | |
| 890 | * @param from | |
| 891 | * @param to | |
| 892 | */ | |
| 893 | 1 | function getValueForField(field, values) { |
| 894 | ||
| 895 | 0 | if(!values) return ''; |
| 896 | ||
| 897 | // First of all, split the field name into keys | |
| 898 | 0 | var path = [] |
| 899 | 0 | if(field.match(/.*\]$/)) { |
| 900 | 0 | path = field.replace(/\]/g,"").split("["); |
| 901 | } else { | |
| 902 | 0 | path = field.split(':'); |
| 903 | } | |
| 904 | ||
| 905 | 0 | while (path.length > 0) { |
| 906 | ||
| 907 | 0 | key = path.shift(); |
| 908 | ||
| 909 | 0 | if (!(values && key in values)) { |
| 910 | 0 | if(values && (typeof values.get === "function")) { |
| 911 | 0 | values = values.get(key); |
| 912 | } else { | |
| 913 | 0 | if(values && values[field]) { |
| 914 | 0 | return values[field]; |
| 915 | } else { | |
| 916 | 0 | return ''; |
| 917 | } | |
| 918 | } | |
| 919 | } else { | |
| 920 | 0 | values = values[key]; |
| 921 | } | |
| 922 | ||
| 923 | 0 | if (path.length === 0) { |
| 924 | 0 | return (values || ''); |
| 925 | } | |
| 926 | } | |
| 927 | ||
| 928 | } | |
| 929 | ||
| 930 | ||
| 931 | ||
| 932 | ||
| 933 | /** | |
| 934 | * Get the value for a form field from the values object | |
| 935 | * @param from | |
| 936 | * @param to | |
| 937 | */ | |
| 938 | 1 | function setValueForField(field, values, value) { |
| 939 | ||
| 940 | 0 | if(!values) return ''; |
| 941 | ||
| 942 | // First of all, split the field name into keys | |
| 943 | 0 | var path = [] |
| 944 | 0 | if(field.match(/.*\]$/)) { |
| 945 | 0 | path = field.replace(/\]/g,"").split("["); |
| 946 | } else { | |
| 947 | 0 | path = [field]; |
| 948 | } | |
| 949 | ||
| 950 | // | |
| 951 | // Scope into the object to get the appropriate nested context | |
| 952 | // | |
| 953 | 0 | while (path.length > 1) { |
| 954 | 0 | key = path.shift(); |
| 955 | 0 | if (!values[key] || typeof values[key] !== 'object') { |
| 956 | 0 | values[key] = {}; |
| 957 | } | |
| 958 | 0 | values = values[key]; |
| 959 | } | |
| 960 | ||
| 961 | // Set the specified value in the nested JSON structure | |
| 962 | 0 | key = path.shift(); |
| 963 | 0 | values[key] = value; |
| 964 | 0 | return true; |
| 965 | ||
| 966 | } | |
| 967 | ||
| 968 | /** | |
| 969 | * Recursive copy of object | |
| 970 | * @param from | |
| 971 | * @param to | |
| 972 | */ | |
| 973 | 1 | function copyFormToObject(field, value, target) { |
| 974 | ||
| 975 | // First of all, split the field name into keys | |
| 976 | 0 | var path = [] |
| 977 | 0 | if(field.match(/.*\]$/)) { |
| 978 | ||
| 979 | 0 | path = field.replace(/\]/g,"").split("["); |
| 980 | ||
| 981 | // Now, copy over | |
| 982 | 0 | while (path.length > 1) { |
| 983 | 0 | key = path.shift(); |
| 984 | 0 | if (!target[key]) { |
| 985 | 0 | target[key] = {}; |
| 986 | } | |
| 987 | 0 | target = target[key]; |
| 988 | } | |
| 989 | ||
| 990 | // Shift one more time and set the value | |
| 991 | 0 | key = path.shift(); |
| 992 | 0 | target[key] = value; |
| 993 | ||
| 994 | } else { | |
| 995 | ||
| 996 | // We are probably an nconf form, hence just copy over | |
| 997 | 0 | target[field] = value; |
| 998 | ||
| 999 | } | |
| 1000 | ||
| 1001 | ||
| 1002 | } | |
| 1003 | ||
| 1004 | /** | |
| 1005 | * Process a field / section array from a contentType | |
| 1006 | * And modify the form | |
| 1007 | */ | |
| 1008 | 1 | me.processFields = function(form, fields) { |
| 1009 | ||
| 1010 | // Process fields | |
| 1011 | 0 | if(fields.fields) { |
| 1012 | 0 | processFieldArray(form,fields.fields); |
| 1013 | } | |
| 1014 | ||
| 1015 | 0 | if(fields.sections) { |
| 1016 | 0 | fields.sections.forEach(function(section,key) { |
| 1017 | // Process fields | |
| 1018 | 0 | if(section.label) { |
| 1019 | 0 | form.sections.push(section); |
| 1020 | } | |
| 1021 | // Remove it | |
| 1022 | 0 | if(section.hide) { |
| 1023 | 0 | form = removeSection(form,section.id); |
| 1024 | } | |
| 1025 | }); | |
| 1026 | } | |
| 1027 | ||
| 1028 | 0 | return form; |
| 1029 | ||
| 1030 | }; | |
| 1031 | ||
| 1032 | ||
| 1033 | // Helper function to process fields | |
| 1034 | 1 | function processFieldArray(form, fields) { |
| 1035 | ||
| 1036 | 0 | fields.forEach(function(field, key) { |
| 1037 | // Add it | |
| 1038 | 0 | if(field.type) { |
| 1039 | 0 | form.fields.push(field); |
| 1040 | } | |
| 1041 | // Remove it | |
| 1042 | 0 | if(field.hide) { |
| 1043 | 0 | form = removeField(form, field.name); |
| 1044 | } | |
| 1045 | }); | |
| 1046 | ||
| 1047 | } | |
| 1048 | ||
| 1049 | /** | |
| 1050 | * Remove a field from a form (any section) | |
| 1051 | */ | |
| 1052 | 1 | function removeField(form, fieldName) { |
| 1053 | ||
| 1054 | // Scan sections | |
| 1055 | 0 | form.sections.forEach(function(section, key) { |
| 1056 | 0 | scanFields(section.fields, fieldName); |
| 1057 | }); | |
| 1058 | ||
| 1059 | // Form fields | |
| 1060 | 0 | scanFields(form.fields, fieldName); |
| 1061 | ||
| 1062 | 0 | return form; |
| 1063 | ||
| 1064 | } | |
| 1065 | ||
| 1066 | // Helper function for removeField | |
| 1067 | 1 | function scanFields(fieldArray, fieldName) { |
| 1068 | 0 | fieldArray.forEach(function(field, key) { |
| 1069 | 0 | if(field.name === fieldName) { |
| 1070 | 0 | fieldArray = fieldArray.splice(key, 1); |
| 1071 | } | |
| 1072 | }); | |
| 1073 | } | |
| 1074 | ||
| 1075 | /** | |
| 1076 | * Remove a section from a form | |
| 1077 | */ | |
| 1078 | 1 | function removeSection(form, sectionId) { |
| 1079 | ||
| 1080 | // Scan sections | |
| 1081 | 0 | form.sections.forEach(function(section,key) { |
| 1082 | 0 | if(section.id === sectionId) { |
| 1083 | 0 | form.sections.splice(key,1); |
| 1084 | } | |
| 1085 | }); | |
| 1086 | ||
| 1087 | 0 | return form; |
| 1088 | ||
| 1089 | } | |
| 1090 | ||
| 1091 | ||
| 1092 | /** | |
| 1093 | * Simple object mapper, used to copy over form values to schemas | |
| 1094 | */ | |
| 1095 | 1 | me.mapFields = function(fields, record) { |
| 1096 | ||
| 1097 | 0 | var props = Object.getOwnPropertyNames(fields); |
| 1098 | 0 | props.forEach( function(name) { |
| 1099 | // If not private (e.g. _id), then copy | |
| 1100 | 0 | if(!name.match(/^_.*/)) { |
| 1101 | 0 | record.set(name, fields[name]); |
| 1102 | } | |
| 1103 | }); | |
| 1104 | ||
| 1105 | }; | |
| 1106 | ||
| 1107 | /** | |
| 1108 | * Process the values submitted by a form and return a JSON | |
| 1109 | * object representation (makes it simpler to then process a form submission | |
| 1110 | * from within a module. | |
| 1111 | */ | |
| 1112 | 1 | me.process = function(req, next) { |
| 1113 | ||
| 1114 | // Fix until all modules refactored to use formData | |
| 1115 | 0 | if(req.formProcessed) { |
| 1116 | ||
| 1117 | 0 | next(req.formData, req.uploadedFiles); |
| 1118 | 0 | return; |
| 1119 | ||
| 1120 | } else { | |
| 1121 | ||
| 1122 | // Data parsed based on original form structure | |
| 1123 | 0 | processFormData(req, function(err, formData) { |
| 1124 | ||
| 1125 | 0 | if(err) calipso.error(err); |
| 1126 | ||
| 1127 | 0 | req.formData = formData; |
| 1128 | 0 | req.formProcessed = true; |
| 1129 | ||
| 1130 | 0 | return next(req.formData, req.files); |
| 1131 | ||
| 1132 | }); | |
| 1133 | ||
| 1134 | } | |
| 1135 | ||
| 1136 | }; | |
| 1137 | ||
| 1138 | ||
| 1139 | /** | |
| 1140 | * This process the incoming form, if the form exists in session then use that | |
| 1141 | * to validate and convert incoming data against. | |
| 1142 | */ | |
| 1143 | 1 | function processFormData(req, next) { |
| 1144 | ||
| 1145 | 0 | if(calipso.lib._.keys(req.body).length === 0) { |
| 1146 | // No data | |
| 1147 | 0 | return next(); |
| 1148 | } | |
| 1149 | ||
| 1150 | // Get the form id and then remove from the response | |
| 1151 | 0 | var formId = req.body.form ? req.body.form.id : ''; |
| 1152 | 0 | delete req.body.form; |
| 1153 | ||
| 1154 | // Get the form and then delete the form from the user session to clean up | |
| 1155 | 0 | var form = req.session.forms ? req.session.forms[formId] : null; |
| 1156 | 0 | var formData = req.body; |
| 1157 | ||
| 1158 | 0 | if(formId && form) { |
| 1159 | ||
| 1160 | 0 | processSectionData(form, formData, function(err, formData) { |
| 1161 | ||
| 1162 | 0 | delete req.session.forms[formId]; |
| 1163 | ||
| 1164 | 0 | req.session.save(function(err) { |
| 1165 | 0 | if(err) calipso.error(err.message); |
| 1166 | }); // Doesn't matter that this is async, can happen in background | |
| 1167 | ||
| 1168 | 0 | return next(err, formData); |
| 1169 | ||
| 1170 | }); | |
| 1171 | ||
| 1172 | } else { | |
| 1173 | ||
| 1174 | // No form in session, do not process | |
| 1175 | 0 | next(null, formData); |
| 1176 | ||
| 1177 | } | |
| 1178 | ||
| 1179 | } | |
| 1180 | ||
| 1181 | /** | |
| 1182 | * Process form sections and fields. | |
| 1183 | */ | |
| 1184 | 1 | function processSectionData(form, formData, next) { |
| 1185 | ||
| 1186 | // Create a single array of all the form and section fields | |
| 1187 | 0 | var fields = []; |
| 1188 | ||
| 1189 | 0 | if(form.sections) { |
| 1190 | 0 | form.sections.forEach(function(section) { |
| 1191 | // Ensure section isn't null | |
| 1192 | 0 | if(section) { |
| 1193 | 0 | fields.push(section.fields); |
| 1194 | } | |
| 1195 | }); | |
| 1196 | } | |
| 1197 | 0 | if(form.fields) { |
| 1198 | 0 | fields.push(form.fields); |
| 1199 | } | |
| 1200 | ||
| 1201 | 0 | calipso.lib.async.map(fields, function(section, cb) { |
| 1202 | 0 | processFieldData(section, formData, cb); |
| 1203 | }, function(err, result) { | |
| 1204 | 0 | next(err, formData); |
| 1205 | }) | |
| 1206 | ||
| 1207 | } | |
| 1208 | ||
| 1209 | /** | |
| 1210 | * Process form fields. | |
| 1211 | */ | |
| 1212 | 1 | function processFieldData(fields, formData, next) { |
| 1213 | ||
| 1214 | // First create an array of all the fields (and field sets) to allow us to do an async.map | |
| 1215 | 0 | var formFields = []; |
| 1216 | ||
| 1217 | 0 | for(var fieldName in fields) { |
| 1218 | // It is a section that contains fields | |
| 1219 | 0 | var field = fields[fieldName]; |
| 1220 | 0 | if(field.fields) { |
| 1221 | // This is a field set | |
| 1222 | 0 | for(var subFieldName in field.fields) { |
| 1223 | 0 | formFields.push(field.fields[subFieldName]); |
| 1224 | } | |
| 1225 | } else { | |
| 1226 | // Just push the field | |
| 1227 | 0 | formFields.push(field) |
| 1228 | } | |
| 1229 | } | |
| 1230 | ||
| 1231 | 0 | var iteratorFn = function(field, cb) { |
| 1232 | ||
| 1233 | 0 | var value = getValueForField(field.name, formData); |
| 1234 | 0 | processFieldValue(field, value, function(err, processedValue) { |
| 1235 | 0 | if(value !== processedValue) setValueForField(field.name, formData, processedValue); |
| 1236 | 0 | cb(err, true); |
| 1237 | }); | |
| 1238 | ||
| 1239 | } | |
| 1240 | ||
| 1241 | 0 | calipso.lib.async.map(formFields, iteratorFn, next); |
| 1242 | ||
| 1243 | } | |
| 1244 | ||
| 1245 | /** | |
| 1246 | * Process submitted values against the original form. | |
| 1247 | * TODO: This is where we would bolt on any validation. | |
| 1248 | */ | |
| 1249 | 1 | function processFieldValue(field, value, next) { |
| 1250 | ||
| 1251 | // Process each field | |
| 1252 | 0 | if(field.type === 'checkbox') { |
| 1253 | ||
| 1254 | 0 | if(typeof value === 'object') { |
| 1255 | // The value has come in as ['off','on'] or [false,true] | |
| 1256 | // So we always take the last value | |
| 1257 | 0 | value = value[value.length - 1]; |
| 1258 | } | |
| 1259 | ||
| 1260 | // Deal with on off | |
| 1261 | 0 | if(value === 'on') value = true; |
| 1262 | 0 | if(value === 'off') value = false; |
| 1263 | ||
| 1264 | } | |
| 1265 | ||
| 1266 | 0 | if(field.type === 'select') { |
| 1267 | ||
| 1268 | // Deal with Yes / No > Boolean | |
| 1269 | 0 | if(value === 'Yes') value = true; |
| 1270 | 0 | if(value === 'No') value = false; |
| 1271 | ||
| 1272 | } | |
| 1273 | ||
| 1274 | 0 | if(field.type === 'datetime') { |
| 1275 | ||
| 1276 | 0 | if(value.hasOwnProperty('date') && value.hasOwnProperty('time')) { |
| 1277 | 0 | value = new Date( |
| 1278 | value.date + " " + value.time | |
| 1279 | ); | |
| 1280 | } | |
| 1281 | ||
| 1282 | 0 | if(value.hasOwnProperty('date') && value.hasOwnProperty('hours')) { |
| 1283 | 0 | value = new Date( |
| 1284 | value.date + " " + value.hours + ":" + value.minutes + ":00" | |
| 1285 | ); | |
| 1286 | } | |
| 1287 | ||
| 1288 | 0 | if(value.hasOwnProperty('year') && value.hasOwnProperty('hours')) { |
| 1289 | ||
| 1290 | 0 | var now = new Date(); |
| 1291 | ||
| 1292 | 0 | value = new Date( |
| 1293 | (value.year || now.getFullYear()), | |
| 1294 | (value.month || now.getMonth()), | |
| 1295 | (value.day || now.getDate()), | |
| 1296 | (value.hours || now.getHours()), | |
| 1297 | (value.minutes || now.getMinutes()), | |
| 1298 | (value.seconds || now.getSeconds()) | |
| 1299 | ); | |
| 1300 | ||
| 1301 | } | |
| 1302 | ||
| 1303 | } | |
| 1304 | ||
| 1305 | 0 | return next(null, value); |
| 1306 | ||
| 1307 | } | |
| 1308 | ||
| 1309 | /** | |
| 1310 | * Export an instance of our form object | |
| 1311 | */ | |
| 1312 | 1 | module.exports = f; |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso Core Library | |
| 3 | * | |
| 4 | * Copyright(c) 2011 Clifton Cunningham <clifton.cunningham@gmail.com> | |
| 5 | * MIT Licensed | |
| 6 | * | |
| 7 | * Dynamic helpers for insertion into the templating engine | |
| 8 | * They all need to take in a req,res pair, these are then | |
| 9 | * interpreted during the request stack and passed into the | |
| 10 | * view engine (so for example 'request' is accessible). | |
| 11 | */ | |
| 12 | ||
| 13 | /** | |
| 14 | * removes any trailing query string or hash values | |
| 15 | * @method stripUrlToConvert | |
| 16 | * @param url {string} The url to convert | |
| 17 | * @return {String} Converted url, if applicable | |
| 18 | */ | |
| 19 | ||
| 20 | 1 | function stripUrlToConvert(url) { |
| 21 | 8 | var qs = url.search(/\?|#/); |
| 22 | 8 | if (qs > -1) { |
| 23 | 0 | url = url.substring(0, qs); |
| 24 | } | |
| 25 | 8 | return url; |
| 26 | } | |
| 27 | ||
| 28 | /** | |
| 29 | * Exports | |
| 30 | */ | |
| 31 | 1 | exports = module.exports = { |
| 32 | ||
| 33 | // Attach view Helpers to the request | |
| 34 | getDynamicHelpers: function(req, res, calipso) { | |
| 35 | 4 | var self = this; |
| 36 | 4 | req.helpers = {}; |
| 37 | 4 | for (var helper in self.helpers) { |
| 38 | 84 | req.helpers[helper] = self.helpers[helper](req, res, calipso); |
| 39 | } | |
| 40 | }, | |
| 41 | ||
| 42 | // Add a new helper (e.g. so modules can add them) | |
| 43 | addHelper: function(name, fn) { | |
| 44 | 0 | var self = this; |
| 45 | 0 | self.helpers[name] = fn; |
| 46 | }, | |
| 47 | ||
| 48 | helpers: { | |
| 49 | /** | |
| 50 | * Request shortcut | |
| 51 | */ | |
| 52 | request: function(req, res, calipso) { | |
| 53 | 4 | return req; |
| 54 | }, | |
| 55 | ||
| 56 | /** | |
| 57 | * Config shortcut | |
| 58 | */ | |
| 59 | config: function(req, res, calipso) { | |
| 60 | 4 | return calipso.config; |
| 61 | }, | |
| 62 | ||
| 63 | /** | |
| 64 | * Translation shortcut | |
| 65 | */ | |
| 66 | t: function(req, res, calipso) { | |
| 67 | 4 | return req.t; |
| 68 | }, | |
| 69 | ||
| 70 | /** | |
| 71 | * User shortcut | |
| 72 | */ | |
| 73 | user: function(req, res, calipso) { | |
| 74 | 4 | return req.session && req.session.user || { |
| 75 | username: '', | |
| 76 | anonymous: true | |
| 77 | }; | |
| 78 | }, | |
| 79 | ||
| 80 | /** | |
| 81 | * Pretty date helper | |
| 82 | */ | |
| 83 | prettyDate: function(req, res, calipso) { | |
| 84 | ||
| 85 | 4 | var prettyFn = calipso.lib.prettyDate.prettyDate; |
| 86 | 4 | return prettyFn; |
| 87 | ||
| 88 | }, | |
| 89 | ||
| 90 | /** | |
| 91 | * Pretty size helper | |
| 92 | */ | |
| 93 | prettySize: function(req, res, calipso) { | |
| 94 | ||
| 95 | 4 | var prettyFn = calipso.lib.prettySize.prettySize; |
| 96 | 4 | return prettyFn; |
| 97 | }, | |
| 98 | ||
| 99 | /** | |
| 100 | * Hot date helper | |
| 101 | */ | |
| 102 | hotDate: function(req, res, calipso) { | |
| 103 | ||
| 104 | 4 | var hotFn = calipso.lib.prettyDate.hotDate; |
| 105 | 4 | return hotFn; |
| 106 | ||
| 107 | }, | |
| 108 | ||
| 109 | /** | |
| 110 | * Get block data not included preloaded in the theme configuration (in blockData) | |
| 111 | */ | |
| 112 | getBlock: function(req, res, calipso) { | |
| 113 | ||
| 114 | 4 | return function(block, next) { |
| 115 | ||
| 116 | // TODO : Allow block to be passed as a regex (e.g. to include all scripts.* blocks) | |
| 117 | 8 | var output = ""; |
| 118 | 8 | res.renderedBlocks.get(block, function(err, blocks) { |
| 119 | ||
| 120 | 8 | blocks.forEach(function(content) { |
| 121 | 1 | output += content; |
| 122 | }); | |
| 123 | ||
| 124 | 16 | if (typeof next === 'function') next(null, output); |
| 125 | ||
| 126 | }); | |
| 127 | ||
| 128 | }; | |
| 129 | }, | |
| 130 | ||
| 131 | /** | |
| 132 | * Get a menu html, synchronous | |
| 133 | */ | |
| 134 | getMenu: function(req, res, calipso) { | |
| 135 | ||
| 136 | 4 | return function(menu, depth) { |
| 137 | // Render menu | |
| 138 | 12 | if (res.menu[menu]) { |
| 139 | 12 | var output = res.menu[menu].render(req, depth); |
| 140 | 12 | return output; |
| 141 | } else { | |
| 142 | 0 | return 'Menu ' + menu + ' does not exist!'; |
| 143 | } | |
| 144 | ||
| 145 | }; | |
| 146 | }, | |
| 147 | ||
| 148 | /** | |
| 149 | * Directly call an exposed module function (e.g. over ride routing rules and inject it anywhere) | |
| 150 | */ | |
| 151 | getModuleFn: function(req, res, calipso) { | |
| 152 | ||
| 153 | 4 | return function(req, moduleFunction, options, next) { |
| 154 | ||
| 155 | // Call an exposed module function | |
| 156 | // e.g. user.loginForm(req, res, template, block, next) | |
| 157 | // First see if function exists | |
| 158 | 0 | var moduleName = moduleFunction.split(".")[0]; |
| 159 | 0 | var functionName = moduleFunction.split(".")[1]; |
| 160 | ||
| 161 | 0 | if (calipso.modules[moduleName] && calipso.modules[moduleName].enabled && calipso.modules[moduleName].fn[functionName]) { |
| 162 | ||
| 163 | 0 | var fn = calipso.modules[moduleName].fn[functionName]; |
| 164 | ||
| 165 | // Get the template | |
| 166 | 0 | var template; |
| 167 | 0 | if (options.template && calipso.modules[moduleName].templates[options.template]) { |
| 168 | 0 | template = calipso.modules[moduleName].templates[options.template]; |
| 169 | } | |
| 170 | ||
| 171 | // Call the fn | |
| 172 | 0 | try { |
| 173 | 0 | fn(req, res, template, null, next); |
| 174 | } catch (ex) { | |
| 175 | 0 | next(ex); |
| 176 | } | |
| 177 | ||
| 178 | } else { | |
| 179 | 0 | next(null, "<div class='error'>Function " + moduleFunction + " requested via getModuleFn does not exist or module is not enabled.</div>"); |
| 180 | } | |
| 181 | ||
| 182 | }; | |
| 183 | ||
| 184 | }, | |
| 185 | ||
| 186 | /** | |
| 187 | * Retrieves the params parsed during module routing | |
| 188 | */ | |
| 189 | getParams: function(req, res, calipso) { | |
| 190 | 4 | return function() { |
| 191 | 0 | return res.params; |
| 192 | }; | |
| 193 | }, | |
| 194 | ||
| 195 | /** | |
| 196 | * Constructs individual classes based on the url request | |
| 197 | */ | |
| 198 | getPageClasses: function(req, res, calipso) { | |
| 199 | 4 | var url = stripUrlToConvert(req.url); |
| 200 | 4 | return url.split('/').join(' '); |
| 201 | }, | |
| 202 | ||
| 203 | /** | |
| 204 | * Constructs a single id based on the url request | |
| 205 | */ | |
| 206 | getPageId: function(req, res, calipso) { | |
| 207 | 4 | var url = stripUrlToConvert(req.url), |
| 208 | urlFrags = url.split('/'); | |
| 209 | 4 | for (var i = 0, len = urlFrags.length; i < len; i++) { |
| 210 | 8 | var frag = urlFrags[i]; |
| 211 | 8 | if (frag === '') { |
| 212 | 4 | urlFrags.splice(i, 1); |
| 213 | } | |
| 214 | } | |
| 215 | 4 | return urlFrags.join('-'); |
| 216 | }, | |
| 217 | ||
| 218 | ||
| 219 | addScript: function(req, res, calipso) { | |
| 220 | 4 | return function(options) { |
| 221 | 0 | res.client.addScript(options); |
| 222 | }; | |
| 223 | }, | |
| 224 | ||
| 225 | getScripts: function(req, res, calipso) { | |
| 226 | 4 | return function(next) { |
| 227 | 0 | res.client.listScripts(next); |
| 228 | }; | |
| 229 | }, | |
| 230 | ||
| 231 | ||
| 232 | addStyle: function(req, res, calipso) { | |
| 233 | 4 | return function(options) { |
| 234 | 0 | res.client.addStyle(options); |
| 235 | }; | |
| 236 | }, | |
| 237 | ||
| 238 | getStyles: function(req, res, calipso) { | |
| 239 | 4 | return function(next) { |
| 240 | 0 | res.client.listStyles(next); |
| 241 | }; | |
| 242 | }, | |
| 243 | ||
| 244 | /** | |
| 245 | * Flash message helpers | |
| 246 | */ | |
| 247 | flashMessages: function(req, res, calipso) { | |
| 248 | 4 | return function() { |
| 249 | 0 | return req.flash(); |
| 250 | }; | |
| 251 | }, | |
| 252 | ||
| 253 | /** | |
| 254 | * HTML helpers - form (formApi), table, link (for now) | |
| 255 | */ | |
| 256 | formApi: function(req, res, calipso) { | |
| 257 | 4 | return function(form) { |
| 258 | 0 | return calipso.form.render(form); |
| 259 | }; | |
| 260 | }, | |
| 261 | table: function(req, res, calipso) { | |
| 262 | 4 | return function(table) { |
| 263 | 0 | return calipso.table.render(table); |
| 264 | }; | |
| 265 | }, | |
| 266 | link: function(req, res, calipso) { | |
| 267 | 4 | return function(link) { |
| 268 | 0 | return calipso.link.render(link); |
| 269 | }; | |
| 270 | } | |
| 271 | } | |
| 272 | ||
| 273 | }; |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso Imports | |
| 3 | * | |
| 4 | * Copyright(c) 2011 Clifton Cunningham <clifton.cunningham@gmail.com> | |
| 5 | * MIT Licensed | |
| 6 | * | |
| 7 | * This library is used to allow a single place to add new 3rd party libraries or utilities that | |
| 8 | * are then automatically accessible via calipso.lib.library in any module. | |
| 9 | * | |
| 10 | */ | |
| 11 | 1 | var rootpath = process.cwd() + '/'; |
| 12 | ||
| 13 | 1 | module.exports = { |
| 14 | fs: require('fs'), | |
| 15 | path: require('path'), | |
| 16 | express: require('express'), | |
| 17 | step: require('step'), | |
| 18 | util: require('util'), | |
| 19 | mongoose: require('mongoose'), | |
| 20 | url: require('url'), | |
| 21 | ejs: require('ejs'), | |
| 22 | pager: require(rootpath + 'utils/pager'), | |
| 23 | prettyDate: require(rootpath + 'utils/prettyDate.js'), | |
| 24 | prettySize: require(rootpath + 'utils/prettySize.js'), | |
| 25 | crypto: require(rootpath + 'utils/crypto.js'), | |
| 26 | connect: require('connect'), | |
| 27 | _: require('underscore'), | |
| 28 | async: require('async') | |
| 29 | }; |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Calipso Link Rendering Library | |
| 4 | * | |
| 5 | * Copyright(c) 2011 Clifton Cunningham <clifton.cunningham@gmail.com> | |
| 6 | * MIT Licensed | |
| 7 | * | |
| 8 | * Loaded into calipso as a plugin, used to simplify rendering of links | |
| 9 | * | |
| 10 | */ | |
| 11 | ||
| 12 | 1 | var rootpath = process.cwd() + '/', |
| 13 | path = require('path'), | |
| 14 | calipso = require(path.join('..', 'calipso')), | |
| 15 | qs = require('qs'); | |
| 16 | ||
| 17 | // Global variable (in this context) for translation function | |
| 18 | 1 | var t; |
| 19 | ||
| 20 | /** | |
| 21 | * The default calipso link object, with default configuration values. | |
| 22 | * Constructor | |
| 23 | */ | |
| 24 | ||
| 25 | 1 | function CalipsoLink() { |
| 26 | ||
| 27 | //TODO Allow over-ride | |
| 28 | } | |
| 29 | ||
| 30 | /** | |
| 31 | * Export an instance of our link object | |
| 32 | */ | |
| 33 | 1 | module.exports = new CalipsoLink(); |
| 34 | ||
| 35 | ||
| 36 | /** | |
| 37 | * Link Renderer, controls the overall creation of the tablle based on a form json object passed | |
| 38 | * in as the first parameter. The structure of this object is as follows: | |
| 39 | * | |
| 40 | * link | |
| 41 | * id : Unique ID that will become the link ID. | |
| 42 | * title : Title to show (hover) | |
| 43 | * target : target window | |
| 44 | * label : label to show in link | |
| 45 | * cls : css class | |
| 46 | * url: the direct url to use, can be function (mandatory) | |
| 47 | * | |
| 48 | * @param item : the json object representing the form | |
| 49 | * @param next : Callback when done, pass markup as return val. | |
| 50 | */ | |
| 51 | 1 | CalipsoLink.prototype.render = function(item) { |
| 52 | ||
| 53 | 0 | return ( |
| 54 | this.render_link(item)); | |
| 55 | ||
| 56 | }; | |
| 57 | ||
| 58 | /** | |
| 59 | * Render link | |
| 60 | * | |
| 61 | * @param link | |
| 62 | * @returns {String} | |
| 63 | */ | |
| 64 | 1 | CalipsoLink.prototype.render_link = function(link) { |
| 65 | ||
| 66 | 0 | var url = ""; |
| 67 | 0 | if (typeof link.url === 'function') { |
| 68 | 0 | url = link.url(link); |
| 69 | } else { | |
| 70 | 0 | url = link.url; |
| 71 | } | |
| 72 | ||
| 73 | 0 | return ('<a' + ' href="' + url + '"' + (link.id ? ' id=' + link.id + '"' : "") + (link.target ? ' target="' + link.target + '"' : "") + (link.title ? ' title="' + link.title + '"' : "") + (link.cls ? ' class="' + link.cls + '"' : "") + '>' + (link.label || "") + '</a>'); |
| 74 | }; |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso Core Logging Library | |
| 3 | * | |
| 4 | * Copyright(c) 2011 Clifton Cunningham <clifton.cunningham@gmail.com> | |
| 5 | * MIT Licensed | |
| 6 | * | |
| 7 | * This module exposes the functions that configures the logging of calipso. | |
| 8 | * This is based entirely on Winston. | |
| 9 | * | |
| 10 | */ | |
| 11 | ||
| 12 | 1 | var app, rootpath = process.cwd(), |
| 13 | path = require('path'), | |
| 14 | winstong = require('winston'), | |
| 15 | calipso = require(path.join('..', 'calipso')); | |
| 16 | ||
| 17 | ||
| 18 | /** | |
| 19 | * Core export | |
| 20 | */ | |
| 21 | 1 | exports = module.exports = { |
| 22 | configureLogging: configureLogging | |
| 23 | }; | |
| 24 | ||
| 25 | /** | |
| 26 | * Configure winston to provide the logging services. | |
| 27 | * | |
| 28 | * TODO : This can be factored out into a module. | |
| 29 | * | |
| 30 | */ | |
| 31 | ||
| 32 | 1 | function configureLogging(options) { |
| 33 | 5 | options = options || calipso.config.get('logging'); |
| 34 | ||
| 35 | //Configure logging | |
| 36 | 5 | var logMsg = "\x1b[36mLogging enabled: \x1b[0m", |
| 37 | winston = require("winston"); | |
| 38 | ||
| 39 | 5 | try { |
| 40 | 5 | winston.remove(winston.transports.File); |
| 41 | } catch (exFile) { | |
| 42 | // Ignore the fault | |
| 43 | } | |
| 44 | ||
| 45 | 5 | if (options.file && options.file.enabled) { |
| 46 | 0 | winston.add(winston.transports.File, { |
| 47 | level: options.console.level, | |
| 48 | timestamp: options.file.timestamp, | |
| 49 | filename: options.file.filepath | |
| 50 | }); | |
| 51 | 0 | logMsg += "File @ " + options.file.filepath + " "; |
| 52 | } | |
| 53 | ||
| 54 | 5 | try { |
| 55 | 5 | winston.remove(winston.transports.Console); |
| 56 | } catch (exConsole) { | |
| 57 | // Ignore the fault | |
| 58 | } | |
| 59 | ||
| 60 | 5 | if (options.console && options.console.enabled) { |
| 61 | 0 | winston.add(winston.transports.Console, { |
| 62 | level: options.console.level, | |
| 63 | timestamp: options.console.timestamp, | |
| 64 | colorize: options.console.colorize | |
| 65 | }); | |
| 66 | 0 | logMsg += "Console "; |
| 67 | } | |
| 68 | ||
| 69 | // Temporary data for form | |
| 70 | 5 | calipso.data.loglevels = []; |
| 71 | 5 | for (var level in winston.config.npm.levels) { |
| 72 | 30 | calipso.data.loglevels.push(level); |
| 73 | } | |
| 74 | ||
| 75 | // Shortcuts to Default | |
| 76 | 5 | calipso.log = winston.info; // Default function |
| 77 | // Shortcuts to NPM levels | |
| 78 | 5 | calipso.silly = winston.silly; |
| 79 | 5 | calipso.verbose = winston.verbose; |
| 80 | 5 | calipso.info = winston.info; |
| 81 | 5 | calipso.warn = winston.warn; |
| 82 | 5 | calipso.debug = winston.debug; |
| 83 | 5 | calipso.error = winston.error; |
| 84 | ||
| 85 | } |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso Menu Library | |
| 3 | * Copyright(c) 2011 Clifton Cunningham | |
| 4 | * MIT Licensed | |
| 5 | * | |
| 6 | * This library provides the base functions to manage the creation of menus. | |
| 7 | * A default renderer will be provided in this library, but this is intended to be over-ridden | |
| 8 | * By menu modules (e.g. to export different structures), or even source menus from different locations. | |
| 9 | * | |
| 10 | */ | |
| 11 | ||
| 12 | /** | |
| 13 | * Includes | |
| 14 | */ | |
| 15 | 1 | var sys; |
| 16 | 1 | try { |
| 17 | 1 | sys = require('util'); |
| 18 | } catch (e) { | |
| 19 | 0 | sys = require('sys'); |
| 20 | } | |
| 21 | 1 | var rootpath = process.cwd() + '/', |
| 22 | path = require('path'), | |
| 23 | utils = require('connect').utils, | |
| 24 | merge = utils.merge, | |
| 25 | calipso = require(path.join('..', 'calipso')); | |
| 26 | ||
| 27 | /** | |
| 28 | * The default menu item object, with default configuration values. | |
| 29 | * Constructor | |
| 30 | */ | |
| 31 | ||
| 32 | 1 | function CalipsoMenu(name, sort, type, options) { |
| 33 | ||
| 34 | // Basic menu options, used typically for root menu holder | |
| 35 | 67 | this.name = name || 'default'; // This should be mandatory |
| 36 | 67 | this.type = type || 'root'; |
| 37 | 67 | this.sort = sort || 'name'; |
| 38 | ||
| 39 | // Options for this menu item | |
| 40 | 67 | if (options) { |
| 41 | 51 | this.setOptions(options); |
| 42 | } | |
| 43 | ||
| 44 | // Child menu items | |
| 45 | 67 | this.children = {}; |
| 46 | 67 | this.sortedChildren = []; // Sorted array of prop names for recursion |
| 47 | } | |
| 48 | ||
| 49 | /** | |
| 50 | * Exports | |
| 51 | */ | |
| 52 | 1 | module.exports = CalipsoMenu; |
| 53 | ||
| 54 | /** | |
| 55 | * Wrapper to enable setting of menu options | |
| 56 | */ | |
| 57 | 1 | CalipsoMenu.prototype.setOptions = function(options) { |
| 58 | 52 | merge(this, options); |
| 59 | }; | |
| 60 | ||
| 61 | /** | |
| 62 | * Function to enable addition of a menu item to the menu. | |
| 63 | * | |
| 64 | * Menu Options: | |
| 65 | * name: req.t('Admin') -- Label to display | |
| 66 | * path: admin -- the menu heirarchy path, used for parent child. | |
| 67 | * e.g. path: admin/config -- the menu heirarchy path, used for parent child. | |
| 68 | * instruction: req.t('Administration Menu') -- tooltip label | |
| 69 | * url: '/admin' -- Url to use as link | |
| 70 | * security: [/admin/,"bob"] -- regex based on user role | |
| 71 | */ | |
| 72 | 1 | CalipsoMenu.prototype.addMenuItem = function(req, options) { |
| 73 | ||
| 74 | 30 | var self = this; |
| 75 | ||
| 76 | // The req parameter was added in 0.3.0, if not passed, assuming options only | |
| 77 | 30 | if (options === undefined) calipso.error("Attempting to add menu item with invalid params, please update your module for the 0.3.0 api, path: " + req.path); |
| 78 | ||
| 79 | // Check security | |
| 80 | 30 | if (options.permit) { |
| 81 | ||
| 82 | 28 | var permitFn = new calipso.permission.Filter(options, options.permit), |
| 83 | permit = permitFn.check(req); | |
| 84 | ||
| 85 | 28 | if (typeof permit !== "object") return; |
| 86 | 29 | if (!permit.allow) return; |
| 87 | } | |
| 88 | // Admin security is opposite to default | |
| 89 | 29 | if (self.name === 'admin') { |
| 90 | 1 | var isAdmin = req.session.user && req.session.user.isAdmin; |
| 91 | // Admin by default is not shown unless permitted | |
| 92 | 2 | if (!options.permit && !isAdmin) return; |
| 93 | } | |
| 94 | ||
| 95 | // Split the path, traverse items and add menuItems. | |
| 96 | // If you add a child prior to parent, then create the parent. | |
| 97 | 28 | var newItem = self.createPath(options, options.path.split("/")); |
| 98 | ||
| 99 | }; | |
| 100 | ||
| 101 | /** | |
| 102 | * Ensure that a full path provided is a valid menu tree | |
| 103 | */ | |
| 104 | 1 | CalipsoMenu.prototype.createPath = function(options, path) { |
| 105 | ||
| 106 | 51 | var self = this; |
| 107 | 51 | var currentItem = path[0]; |
| 108 | 51 | var remainingItems = path.splice(1, path.length - 1); |
| 109 | ||
| 110 | 51 | if (self.children[currentItem] && remainingItems.length > 0) { |
| 111 | ||
| 112 | // Recurse | |
| 113 | 18 | self.children[currentItem].createPath(options, remainingItems); |
| 114 | ||
| 115 | } else { | |
| 116 | ||
| 117 | // If the current item does not yet exist | |
| 118 | 33 | if (!self.children[currentItem]) { |
| 119 | ||
| 120 | // Do we have children left, if so, mark this as a temporary node (e.g. we dont actually have its options) | |
| 121 | 31 | if (remainingItems.length > 0) { |
| 122 | 5 | self.children[currentItem] = new CalipsoMenu('Child of ' + currentItem, self.sort, 'temporary', options); |
| 123 | } else { | |
| 124 | 26 | self.children[currentItem] = new CalipsoMenu('Child of ' + currentItem, self.sort, 'child', options); |
| 125 | } | |
| 126 | 31 | self.sortedChildren.push(currentItem); // Add to array for later sorting |
| 127 | } | |
| 128 | ||
| 129 | // Check to see if we need to update a temporary node | |
| 130 | 33 | if (self.children[currentItem] && remainingItems.length === 0 && self.children[currentItem].type === 'temporary') { |
| 131 | 1 | self.children[currentItem].type = 'child'; |
| 132 | 1 | self.children[currentItem].setOptions(options); |
| 133 | } | |
| 134 | ||
| 135 | 33 | if (remainingItems.length > 0) { |
| 136 | // Recurse | |
| 137 | 5 | self.children[currentItem].createPath(options, remainingItems); |
| 138 | } | |
| 139 | ||
| 140 | } | |
| 141 | ||
| 142 | // Sort the sorted array | |
| 143 | 51 | self.sortedChildren.sort(function(a, b) { |
| 144 | ||
| 145 | // a & b are strings, but both objects on the current children | |
| 146 | 9 | var diff; |
| 147 | 9 | if (self.children[a][self.sort] && self.children[b][self.sort]) { |
| 148 | ||
| 149 | 8 | if (typeof self.children[a][self.sort] === "string") { |
| 150 | 7 | diff = self.children[a][self.sort].toLowerCase() > self.children[b][self.sort].toLowerCase(); |
| 151 | } else { | |
| 152 | 1 | diff = self.children[a][self.sort] > self.children[b][self.sort]; |
| 153 | } | |
| 154 | ||
| 155 | } else { | |
| 156 | 1 | diff = self.children[a].name.toLowerCase() > self.children[b].name.toLowerCase(); |
| 157 | } | |
| 158 | ||
| 159 | 9 | return diff; |
| 160 | }); | |
| 161 | ||
| 162 | ||
| 163 | }; | |
| 164 | ||
| 165 | ||
| 166 | /** | |
| 167 | * Render the menu as a html list - this is the default. | |
| 168 | * The idea is that this can be over-ridden (or the sub-function), to control | |
| 169 | * HTML generation. | |
| 170 | */ | |
| 171 | 1 | CalipsoMenu.prototype.render = function(req, depth) { |
| 172 | ||
| 173 | 13 | var self = this; |
| 174 | ||
| 175 | // If the menu is empty, render nothing | |
| 176 | 25 | if (self.sortedChildren.length === 0) return ''; |
| 177 | ||
| 178 | // Get selected items | |
| 179 | 1 | var selected = self.selected(req); |
| 180 | ||
| 181 | 1 | var htmlOutput = ''; |
| 182 | 1 | htmlOutput += self.startTag(); |
| 183 | ||
| 184 | 1 | var renderUp = function(menu) { |
| 185 | 5 | var selectedClass = ''; |
| 186 | 5 | if (contains(selected, menu.path)) { |
| 187 | 2 | selectedClass = '-selected'; |
| 188 | } | |
| 189 | 5 | var html = self.menuStartTag(menu, selectedClass) + self.menuLinkTag(req, menu, selectedClass); |
| 190 | 5 | return html; |
| 191 | }; | |
| 192 | ||
| 193 | 1 | var renderDown = function(menu) { |
| 194 | 5 | var html = self.menuEndTag(menu); |
| 195 | 5 | return html; |
| 196 | }; | |
| 197 | ||
| 198 | 1 | var renderStart = function(menu) { |
| 199 | 3 | var html = self.childrenStartTag(menu); |
| 200 | 3 | return html; |
| 201 | }; | |
| 202 | ||
| 203 | 1 | var renderFinish = function(menu) { |
| 204 | 3 | var html = self.childrenEndTag(menu); |
| 205 | 3 | return html; |
| 206 | }; | |
| 207 | ||
| 208 | 1 | var output = []; |
| 209 | 1 | self.fnRecurse(self, renderUp, renderDown, renderStart, renderFinish, depth, output); |
| 210 | ||
| 211 | 1 | htmlOutput += output.join(""); |
| 212 | 1 | htmlOutput += self.endTag(); |
| 213 | ||
| 214 | 1 | return htmlOutput; |
| 215 | ||
| 216 | }; | |
| 217 | ||
| 218 | /** | |
| 219 | * Specific tag rendering functions | |
| 220 | * Over-write to enable custom menu rendering | |
| 221 | */ | |
| 222 | 1 | CalipsoMenu.prototype.startTag = function() { |
| 223 | 1 | return "<ul id='" + this.name + "-menu' class='menu" + (this.cls ? ' ' + this.cls : '') + "'>"; |
| 224 | }; | |
| 225 | 1 | CalipsoMenu.prototype.endTag = function() { |
| 226 | 1 | return "</ul>"; |
| 227 | }; | |
| 228 | 1 | CalipsoMenu.prototype.menuStartTag = function(menu, selected) { |
| 229 | 5 | var menuItemTagId = menu.path.replace(/\//g, '-') + "-menu-item"; |
| 230 | 5 | return "<li id='" + menuItemTagId + "' class='" + this.name + "-menu-item" + selected + "'>"; |
| 231 | }; | |
| 232 | 1 | CalipsoMenu.prototype.menuLinkTag = function(req, menu, selected) { |
| 233 | 5 | var popup = menu.popup ? 'popupMenu' : ''; |
| 234 | 5 | return "<a href='" + menu.url + "' title='" + req.t(menu.description) + "' class='" + popup + " " + this.name + "-menu-link" + selected + (menu.cls ? " " + menu.cls : "") + "'>" + req.t(menu.name) + "</a>"; |
| 235 | }; | |
| 236 | 1 | CalipsoMenu.prototype.menuEndTag = function(menu) { |
| 237 | 5 | return "</li>"; |
| 238 | }; | |
| 239 | 1 | CalipsoMenu.prototype.childrenStartTag = function() { |
| 240 | 3 | return "<ul>"; |
| 241 | }; | |
| 242 | 1 | CalipsoMenu.prototype.childrenEndTag = function() { |
| 243 | 3 | return "</ul>"; |
| 244 | }; | |
| 245 | ||
| 246 | /** | |
| 247 | * Locate selected paths based on current request | |
| 248 | */ | |
| 249 | 1 | CalipsoMenu.prototype.selected = function(req) { |
| 250 | ||
| 251 | // Based on current url, create a regex string that can be used to test if a menu item | |
| 252 | // Is selected during rendering | |
| 253 | 2 | var self = this; |
| 254 | 2 | var output = []; |
| 255 | ||
| 256 | 2 | var selectedFn = function(menu) { |
| 257 | ||
| 258 | 10 | var menuSplit = menu.url.split("/"); |
| 259 | 10 | var reqSplit = req.url.split("/"); |
| 260 | 10 | var match = true; |
| 261 | ||
| 262 | 10 | menuSplit.forEach(function(value, key) { |
| 263 | 36 | match = match && (value === reqSplit[key]); |
| 264 | }); | |
| 265 | ||
| 266 | // Check if the url matches | |
| 267 | 10 | if (match) { |
| 268 | 4 | return menu.path; |
| 269 | } | |
| 270 | ||
| 271 | }; | |
| 272 | ||
| 273 | 2 | self.fnRecurse(self, selectedFn, output); |
| 274 | ||
| 275 | 2 | return output; |
| 276 | ||
| 277 | }; | |
| 278 | ||
| 279 | /** | |
| 280 | * Helper function that can recurse the menu tree | |
| 281 | * From a start point, execute a function and add the result to an output array | |
| 282 | */ | |
| 283 | 1 | CalipsoMenu.prototype.fnRecurse = function(menu, fnUp, fnDown, fnStart, fnFinish, depth, output) { |
| 284 | ||
| 285 | 24 | var self = this; |
| 286 | 24 | var result; |
| 287 | 24 | if (typeof fnDown != 'function') { |
| 288 | 18 | output = fnDown; |
| 289 | } | |
| 290 | 24 | output = output || []; |
| 291 | ||
| 292 | // Recurse from menu item selected | |
| 293 | 24 | if (menu.type === 'root') { |
| 294 | ||
| 295 | // Functions don't run on root | |
| 296 | 4 | menu.sortedChildren.forEach(function(child) { |
| 297 | 4 | self.fnRecurse(menu.children[child], fnUp, fnDown, fnStart, fnFinish, depth, output); |
| 298 | }); | |
| 299 | ||
| 300 | } else { | |
| 301 | ||
| 302 | // Control depth of recursion | |
| 303 | 20 | depth = depth === undefined ? -1 : depth; |
| 304 | 20 | if (depth > 0) { |
| 305 | 0 | depth = depth - 1; |
| 306 | 20 | } else if (depth === -1) { |
| 307 | // Recures infinitely | |
| 308 | } else { | |
| 309 | 0 | return output; |
| 310 | } | |
| 311 | ||
| 312 | // Count the number of children | |
| 313 | 20 | var childCount = menu.sortedChildren.length; |
| 314 | ||
| 315 | // Execute fn | |
| 316 | 20 | if (typeof fnUp === 'function') { |
| 317 | ||
| 318 | 20 | result = fnUp(menu); |
| 319 | 20 | if (result) { |
| 320 | 14 | output.push(result); |
| 321 | } | |
| 322 | ||
| 323 | 20 | if (childCount > 0) { |
| 324 | 12 | if (typeof fnStart === 'function') { |
| 325 | 3 | result = fnStart(menu); |
| 326 | 3 | if (result) { |
| 327 | 3 | output.push(result); |
| 328 | } | |
| 329 | } | |
| 330 | } | |
| 331 | ||
| 332 | } | |
| 333 | ||
| 334 | // Recurse | |
| 335 | 20 | menu.sortedChildren.forEach(function(child) { |
| 336 | 16 | self.fnRecurse(menu.children[child], fnUp, fnDown, fnStart, fnFinish, depth, output); |
| 337 | }); | |
| 338 | ||
| 339 | // Close | |
| 340 | 20 | if (typeof fnDown === 'function') { |
| 341 | ||
| 342 | 5 | result = fnDown(menu); |
| 343 | 5 | if (result) { |
| 344 | 5 | output.push(result); |
| 345 | } | |
| 346 | ||
| 347 | 5 | if (childCount > 0) { |
| 348 | 3 | if (typeof fnFinish === 'function') { |
| 349 | 3 | result = fnFinish(menu); |
| 350 | 3 | if (result) { |
| 351 | 3 | output.push(result); |
| 352 | } | |
| 353 | } | |
| 354 | } | |
| 355 | } | |
| 356 | ||
| 357 | } | |
| 358 | ||
| 359 | }; | |
| 360 | ||
| 361 | /** | |
| 362 | * Return current menu as a JSON object, used for Ajax style menus. | |
| 363 | * Path : root to return menu from, default is root (entire menu) | |
| 364 | * Depth : How many levels to return menu | |
| 365 | */ | |
| 366 | 1 | CalipsoMenu.prototype.getMenuJson = function(path, depth) { |
| 367 | ||
| 368 | // TODO | |
| 369 | }; | |
| 370 | ||
| 371 | /** | |
| 372 | * Private helper functions | |
| 373 | */ | |
| 374 | ||
| 375 | 1 | function contains(a, obj) { |
| 376 | 5 | var i = a.length; |
| 377 | 5 | while (i--) { |
| 378 | 9 | if (a[i] === obj) { |
| 379 | 2 | return true; |
| 380 | } | |
| 381 | } | |
| 382 | 3 | return false; |
| 383 | } |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | var app, rootpath = process.cwd() + '/', |
| 2 | path = require('path'), | |
| 3 | calipso = require(path.join('..', 'calipso')); | |
| 4 | ||
| 5 | /** | |
| 6 | * Route all of the modules based on the module event model | |
| 7 | * This replaces an earlier version that only executed modules in | |
| 8 | * parallel via step | |
| 9 | */ | |
| 10 | ||
| 11 | 1 | function eventRouteModules(req, res, next) { |
| 12 | ||
| 13 | // Ignore static content | |
| 14 | // TODO : Make this more connect friendly or at least configurable | |
| 15 | // STATIC content may or may not be static. We should route it normally for now. | |
| 16 | //if (req.url.match(/^\/images|^\/js|^\/css|^\/favicon.ico|png$|jpg$|gif$|css$|js$/)) { | |
| 17 | // return next(); | |
| 18 | //} | |
| 19 | ||
| 20 | 4 | req.timeStart = new Date(); |
| 21 | 4 | var end = res.end; |
| 22 | 4 | res.calipsoEndCalled = false; |
| 23 | 4 | res.end = function () { |
| 24 | 0 | res.calipsoEndCalled = true; |
| 25 | 0 | end.apply(res, arguments); |
| 26 | } | |
| 27 | ||
| 28 | // Attach our event listener to this request | |
| 29 | 4 | attachRequestEvents(req, res); |
| 30 | ||
| 31 | // Initialise the response re. matches | |
| 32 | // Later used to decide to show 404 | |
| 33 | 4 | res.routeMatched = false; |
| 34 | ||
| 35 | // Store our callback here | |
| 36 | 4 | req.routeComplete = function(res) { |
| 37 | 8 | if(!res.calipsoEndCalled) next(); |
| 38 | }; | |
| 39 | ||
| 40 | // Route 'first' modules that fire before all others | |
| 41 | // These first modules can stop the routing of all others | |
| 42 | 4 | doFirstModules(req, res, function(err) { |
| 43 | ||
| 44 | 4 | var iterator = function(module, cb) { |
| 45 | 16 | routeModule(req, res, module, false, false, cb); |
| 46 | } | |
| 47 | ||
| 48 | 4 | calipso.lib.async.map(calipso.lib._.keys(calipso.modules), iterator, function(err, result) { |
| 49 | // Not important | |
| 50 | }) | |
| 51 | ||
| 52 | }); | |
| 53 | ||
| 54 | } | |
| 55 | ||
| 56 | /** | |
| 57 | * Attach module event emitters and request event listener | |
| 58 | * to this request instance. | |
| 59 | * This will only last for the context of a current request | |
| 60 | */ | |
| 61 | ||
| 62 | 1 | function attachRequestEvents(req, res) { |
| 63 | ||
| 64 | // Create a request event listener for this request, pass in functions | |
| 65 | // to enable testing. | |
| 66 | 4 | req.event = new calipso.event.RequestEventListener({ |
| 67 | notifyDependencyFn: notifyDependenciesOfRoute, | |
| 68 | registerDependenciesFn: registerDependencies | |
| 69 | }); | |
| 70 | ||
| 71 | // | |
| 72 | 4 | var maxListeners = calipso.config.get('server:events:maxListeners'); |
| 73 | ||
| 74 | // Attach all the modules to it | |
| 75 | 4 | for (var module in calipso.modules) { |
| 76 | 16 | req.event.registerModule(req, res, module, {maxListeners: maxListeners}); |
| 77 | } | |
| 78 | ||
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Helper to register dependent modules that should be checked by a module when | |
| 83 | * routing, the parent module's emitter is passed in. | |
| 84 | */ | |
| 85 | 1 | function registerDependencies(moduleEmitter, moduleName) { |
| 86 | ||
| 87 | // Register depends on parent | |
| 88 | 16 | if (calipso.modules[moduleName].fn && calipso.modules[moduleName].fn.depends) { |
| 89 | 4 | calipso.modules[moduleName].fn.depends.forEach(function(dependentModule) { |
| 90 | 4 | moduleEmitter.modules[moduleName].check[dependentModule] = false; |
| 91 | }); | |
| 92 | } | |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * Route a specific module | |
| 97 | * Called by both the eventRouteModules but also by when dependencies trigger | |
| 98 | * a module to be routed | |
| 99 | * | |
| 100 | * req, res : request/resposne | |
| 101 | * module : the module to route | |
| 102 | * depends : has this route been triggered by an event based on dependencies being met | |
| 103 | * last : final modules, after all others have routed | |
| 104 | * | |
| 105 | */ | |
| 106 | ||
| 107 | 1 | function routeModule(req, res, moduleName, depends, last, next) { |
| 108 | ||
| 109 | 20 | var module = calipso.modules[moduleName]; |
| 110 | ||
| 111 | // If module is enabled and has no dependencies, or if we are explicitly triggering this via depends | |
| 112 | // Ignore modules that are specified as post route only | |
| 113 | 20 | if (module.enabled && (depends || !module.fn.depends) && (last || !module.fn.last) && !module.fn.first) { |
| 114 | ||
| 115 | // Fire event to start | |
| 116 | 8 | req.event.modules[moduleName].route_start(); |
| 117 | ||
| 118 | // Route | |
| 119 | 8 | module.fn.route(req, res, module, calipso.app, function(err, moduleName) { |
| 120 | ||
| 121 | // Gracefully deal with errors | |
| 122 | 8 | if (err) { |
| 123 | 0 | res.statusCode = 500; |
| 124 | 0 | calipso.error(err.message); |
| 125 | 0 | res.errorMessage = "Module " + moduleName + ", error: " + err.message + err.stack; |
| 126 | } | |
| 127 | ||
| 128 | // Expose configuration if module has it | |
| 129 | 8 | if (module.fn && module.fn.config) { |
| 130 | 0 | var modulePermit = calipso.permission.Helper.hasPermission("admin:module:configuration"); |
| 131 | 0 | res.menu.admin.addMenuItem(req, { |
| 132 | name: moduleName, | |
| 133 | path: 'admin/modules/' + moduleName, | |
| 134 | url: '/admin/modules?module=' + moduleName, | |
| 135 | description: 'Manage ' + moduleName + ' settings ...', | |
| 136 | permit: modulePermit | |
| 137 | }); | |
| 138 | } | |
| 139 | ||
| 140 | // Finish event | |
| 141 | 8 | req.event.modules[moduleName].route_finish(); |
| 142 | ||
| 143 | // Check to see if we have completed routing all modules | |
| 144 | 8 | if (!last) { |
| 145 | 8 | checkAllModulesRouted(req, res); |
| 146 | } | |
| 147 | ||
| 148 | 8 | next(); |
| 149 | ||
| 150 | }); | |
| 151 | ||
| 152 | } else { | |
| 153 | ||
| 154 | 12 | checkAllModulesRouted(req, res); |
| 155 | ||
| 156 | 12 | next(); |
| 157 | ||
| 158 | } | |
| 159 | ||
| 160 | } | |
| 161 | ||
| 162 | /** | |
| 163 | * Check that all enabled modules have been initialised | |
| 164 | * Don't check disabled modules or modules that are setup for postRoute only | |
| 165 | */ | |
| 166 | 1 | function checkAllModulesRouted(req, res) { |
| 167 | ||
| 168 | 20 | var allRouted = true; |
| 169 | ||
| 170 | 20 | for (var module in req.event.modules) { |
| 171 | 80 | var moduleRouted = (req.event.modules[module].routed || (calipso.modules[module].enabled && (calipso.modules[module].fn.last || calipso.modules[module].fn.first)) || !calipso.modules[module].enabled); |
| 172 | 80 | allRouted = moduleRouted && allRouted; |
| 173 | } | |
| 174 | ||
| 175 | 20 | if (allRouted && !req.event.routeComplete) { |
| 176 | 4 | req.event.routeComplete = true; |
| 177 | 4 | doLastModules(req, res, function() { |
| 178 | 4 | req.timeFinish = new Date(); |
| 179 | 4 | req.timeDuration = req.timeFinish - req.timeStart; |
| 180 | 4 | calipso.silly("All modules routed in " + req.timeDuration + " ms"); |
| 181 | 4 | doResponse(req, res); |
| 182 | }); | |
| 183 | } | |
| 184 | ||
| 185 | } | |
| 186 | ||
| 187 | ||
| 188 | /** | |
| 189 | * RUn any modules that are defined as first routing modules | |
| 190 | * via first: true, dependencies are ignored for these. | |
| 191 | */ | |
| 192 | 1 | function doFirstModules(req, res, next) { |
| 193 | ||
| 194 | // Get all the postRoute modules | |
| 195 | 4 | var firstModules = []; |
| 196 | 4 | for (var moduleName in calipso.modules) { |
| 197 | 16 | if (calipso.modules[moduleName].enabled && calipso.modules[moduleName].fn.first) { |
| 198 | 4 | firstModules.push(calipso.modules[moduleName]); |
| 199 | } | |
| 200 | } | |
| 201 | ||
| 202 | ||
| 203 | 4 | if(firstModules.length === 0) return next(); |
| 204 | ||
| 205 | // Execute their routing functions | |
| 206 | 4 | calipso.lib.step( |
| 207 | ||
| 208 | function doFirstModules() { | |
| 209 | 4 | var group = this.group(); |
| 210 | 4 | firstModules.forEach(function(module) { |
| 211 | 4 | module.fn.route(req, res, module, calipso.app, group()); |
| 212 | }); | |
| 213 | }, function done(err) { | |
| 214 | ||
| 215 | // Gracefully deal with errors | |
| 216 | 4 | if (err) { |
| 217 | 0 | res.statusCode = 500; |
| 218 | 0 | console.log(err.message); |
| 219 | 0 | res.errorMessage = err.message + err.stack; |
| 220 | } | |
| 221 | ||
| 222 | 4 | next(); |
| 223 | ||
| 224 | }); | |
| 225 | ||
| 226 | } | |
| 227 | ||
| 228 | ||
| 229 | /** | |
| 230 | * RUn any modules that are defined as last routing modules | |
| 231 | * via last: true, dependencies are ignored for these atm. | |
| 232 | */ | |
| 233 | ||
| 234 | 1 | function doLastModules(req, res, next) { |
| 235 | ||
| 236 | // Get all the postRoute modules | |
| 237 | 4 | var lastModules = []; |
| 238 | 4 | for (var moduleName in calipso.modules) { |
| 239 | 16 | if (calipso.modules[moduleName].enabled && calipso.modules[moduleName].fn.last) { |
| 240 | 4 | lastModules.push(calipso.modules[moduleName]); |
| 241 | } | |
| 242 | } | |
| 243 | ||
| 244 | ||
| 245 | 4 | if(lastModules.length === 0) return next(); |
| 246 | ||
| 247 | // Execute their routing functions | |
| 248 | 4 | calipso.lib.step( |
| 249 | ||
| 250 | function doLastModules() { | |
| 251 | 4 | var group = this.group(); |
| 252 | 4 | lastModules.forEach(function(module) { |
| 253 | 4 | module.fn.route(req, res, module, calipso.app, group()); |
| 254 | }); | |
| 255 | }, function done(err) { | |
| 256 | ||
| 257 | // Gracefully deal with errors | |
| 258 | 4 | if (err) { |
| 259 | 0 | res.statusCode = 500; |
| 260 | 0 | console.log(err.message); |
| 261 | 0 | res.errorMessage = err.message + err.stack; |
| 262 | } | |
| 263 | ||
| 264 | 4 | next(); |
| 265 | ||
| 266 | }); | |
| 267 | ||
| 268 | } | |
| 269 | ||
| 270 | /** | |
| 271 | * Standard response to all modules completing their routing | |
| 272 | */ | |
| 273 | ||
| 274 | 1 | function doResponse(req, res, next) { |
| 275 | ||
| 276 | // If we are in install mode, and are not in the installation process, then redirect | |
| 277 | 4 | if (!calipso.config.get('installed') && !req.url.match(/^\/admin\/install/)) { |
| 278 | 0 | calipso.silly("Redirecting to admin/install ..."); |
| 279 | 0 | calipso.app.doingInstall = true; |
| 280 | 0 | res.redirect("/admin/install"); |
| 281 | 0 | return; |
| 282 | } | |
| 283 | ||
| 284 | // If nothing could be matched ... | |
| 285 | 4 | if (!res.routeMatched) { |
| 286 | 1 | calipso.log("No Calipso module routes matched the current URL."); |
| 287 | 1 | res.statusCode = 404; |
| 288 | } | |
| 289 | ||
| 290 | // Render statuscodes dealt with by themeing engine | |
| 291 | // TODO - this is not very clean | |
| 292 | 4 | calipso.silly("Responding with statusCode: " + res.statusCode); |
| 293 | 4 | if (res.statusCode === 404 || res.statusCode === 500 || res.statusCode === 200 || res.statusCode === 403) { |
| 294 | ||
| 295 | 3 | calipso.theme.render(req, res, function(err, content) { |
| 296 | ||
| 297 | 3 | if (err) { |
| 298 | ||
| 299 | // Something went wrong at the layout, cannot use layout to render. | |
| 300 | 0 | res.statusCode = 500; |
| 301 | 0 | res.send(500, "<html><h2>A fatal error occurred!</h2>" + "<p>" + (err.xMessage ? err.xMessage : err.message) + "</p>" + "<pre>" + err.stack + "</pre></html>"); |
| 302 | 0 | req.routeComplete(res); |
| 303 | ||
| 304 | } else { | |
| 305 | ||
| 306 | 3 | res.setHeader('Content-Type', 'text/html'); |
| 307 | // Who am I? | |
| 308 | 3 | res.setHeader('X-Powered-By', 'Calipso'); |
| 309 | ||
| 310 | // render | |
| 311 | 3 | res.send(content); |
| 312 | ||
| 313 | // Callback | |
| 314 | 3 | req.routeComplete(res); |
| 315 | ||
| 316 | } | |
| 317 | ||
| 318 | }); | |
| 319 | ||
| 320 | } else { | |
| 321 | ||
| 322 | // Otherwise, provided we haven't already issued a redirect, then pass back to Express | |
| 323 | 1 | req.routeComplete(res); |
| 324 | ||
| 325 | } | |
| 326 | ||
| 327 | } | |
| 328 | ||
| 329 | ||
| 330 | /** | |
| 331 | * Initialise the modules currently enabled. | |
| 332 | * This iterates through the modules loaded by loadModules (it places them in an array in the calipso object), | |
| 333 | * and calls the 'init' function exposed by each module (in parallel controlled via step). | |
| 334 | */ | |
| 335 | ||
| 336 | 1 | function initModules() { |
| 337 | ||
| 338 | // Reset | |
| 339 | 2 | calipso.initComplete = false; |
| 340 | ||
| 341 | // Create a list of all our enabled modules | |
| 342 | 2 | var enabledModules = []; |
| 343 | 2 | for (var module in calipso.modules) { |
| 344 | 8 | if (calipso.modules[module].enabled) { |
| 345 | 8 | enabledModules.push(module); |
| 346 | } | |
| 347 | } | |
| 348 | ||
| 349 | // Initialise them all | |
| 350 | 2 | enabledModules.forEach(function(module) { |
| 351 | 8 | initModule(module, false); |
| 352 | }); | |
| 353 | ||
| 354 | } | |
| 355 | ||
| 356 | /** | |
| 357 | * Init a specific module, called by event listeners re. dependent modules | |
| 358 | */ | |
| 359 | ||
| 360 | 1 | function initModule(module, depends) { |
| 361 | ||
| 362 | ||
| 363 | // If the module has no dependencies, kick start it | |
| 364 | 10 | if (depends || !calipso.modules[module].fn.depends) { |
| 365 | ||
| 366 | // Init start event | |
| 367 | 8 | calipso.modules[module].event.init_start(); |
| 368 | ||
| 369 | // Next run any init functions | |
| 370 | 8 | calipso.modules[module].fn.init(calipso.modules[module], calipso.app, function(err) { |
| 371 | ||
| 372 | // Init finish event | |
| 373 | 8 | calipso.modules[module].inited = true; |
| 374 | 8 | calipso.modules[module].event.init_finish(); |
| 375 | ||
| 376 | // Now, load any routes to go along with it | |
| 377 | 8 | if (calipso.modules[module].fn.routes && calipso.modules[module].fn.routes.length > 0) { |
| 378 | 2 | calipso.lib.async.map(calipso.modules[module].fn.routes, function(options, next) { |
| 379 | 2 | calipso.modules[module].router.addRoute(options, next); |
| 380 | }, function(err, data) { | |
| 381 | 2 | if (err) calipso.error(err); |
| 382 | 2 | checkAllModulesInited(); |
| 383 | }); | |
| 384 | } else { | |
| 385 | 6 | checkAllModulesInited(); |
| 386 | } | |
| 387 | ||
| 388 | }); | |
| 389 | ||
| 390 | } | |
| 391 | ||
| 392 | } | |
| 393 | ||
| 394 | /** | |
| 395 | * Check that all enabled modules have been initialised | |
| 396 | * If they have been initialised, then call the callback supplied on initialisation | |
| 397 | */ | |
| 398 | ||
| 399 | 1 | function checkAllModulesInited() { |
| 400 | ||
| 401 | 8 | var allLoaded = true; |
| 402 | 8 | for (var module in calipso.modules) { |
| 403 | 32 | allLoaded = (calipso.modules[module].inited || !calipso.modules[module].enabled) && allLoaded; |
| 404 | } | |
| 405 | ||
| 406 | 8 | if (allLoaded && !calipso.initComplete) { |
| 407 | 2 | calipso.initComplete = true; |
| 408 | 2 | calipso.initCallback(); |
| 409 | } | |
| 410 | ||
| 411 | } | |
| 412 | ||
| 413 | /** | |
| 414 | * Load the modules from the file system, into a 'modules' array | |
| 415 | * that can be managed and iterated. | |
| 416 | * | |
| 417 | * The first level folder is the module type (e.g. core, contrib, ui). | |
| 418 | * It doesn't actually change the processing, but that folder structure is | |
| 419 | * now stored as a property of the module (so makes admin easier). | |
| 420 | * | |
| 421 | * It will take in an options object that holds the configuration parameters | |
| 422 | * for the modules (e.g. if they are enabled or not). | |
| 423 | * If they are switching (e.g. enabled > disabled) it will run the disable hook. | |
| 424 | * | |
| 425 | */ | |
| 426 | ||
| 427 | 1 | function loadModules(next) { |
| 428 | ||
| 429 | 2 | var configuredModules = calipso.config.get('modules') || {}; |
| 430 | ||
| 431 | // Run any disable hooks | |
| 432 | 2 | for (var module in calipso.modules) { |
| 433 | // Check to see if the module is currently enabled, if we are disabling it. | |
| 434 | 4 | if (calipso.modules[module].enabled && configuredModules[module].enabled === false && typeof calipso.modules[module].fn.disable === 'function') { |
| 435 | 0 | calipso.modules[module].fn.disable(); |
| 436 | } | |
| 437 | } | |
| 438 | ||
| 439 | // Clear the modules object (not sure if this is required, but was getting strange errors initially) | |
| 440 | 2 | delete calipso.modules; // 'Delete' it. |
| 441 | 2 | calipso.modules = {}; // Always reset it |
| 442 | ||
| 443 | 2 | var moduleBasePath = path.join(rootpath, calipso.config.get('server:modulePath')); |
| 444 | ||
| 445 | // Read the modules in from the file system, sync is fine as we do it once on load. | |
| 446 | 2 | calipso.lib.fs.readdirSync(moduleBasePath).forEach(function(type) { |
| 447 | ||
| 448 | // Check for all files or folder starting with "." so that we can handle ".svn", ".git" and so on without problems. | |
| 449 | ||
| 450 | 2 | if (type != "README" && type[0] != '.') { // Ignore the readme file and .DS_Store file for Macs |
| 451 | 2 | calipso.lib.fs.readdirSync(path.join(moduleBasePath, type)).forEach(function(moduleFolderName) { |
| 452 | ||
| 453 | 8 | if (moduleFolderName != "README" && moduleFolderName[0] != '.') { // Ignore the readme file and .DS_Store file for Macs |
| 454 | ||
| 455 | 8 | var modulePath = path.join(moduleBasePath, type, moduleFolderName); |
| 456 | ||
| 457 | 8 | var module = { |
| 458 | name: moduleFolderName, | |
| 459 | folder: moduleFolderName, | |
| 460 | library: moduleFolderName, | |
| 461 | type: type, | |
| 462 | path: modulePath, | |
| 463 | enabled: false, | |
| 464 | inited: false | |
| 465 | }; | |
| 466 | ||
| 467 | // Add about info to it | |
| 468 | 8 | loadAbout(module, modulePath, 'package.json'); |
| 469 | ||
| 470 | // Set the module name to what is in the package.json, default to folder name | |
| 471 | 8 | module.name = (module.about && module.about.name) ? module.about.name : moduleFolderName; |
| 472 | ||
| 473 | // Now set the module | |
| 474 | 8 | calipso.modules[module.name] = module; |
| 475 | ||
| 476 | // Set if it is enabled or not | |
| 477 | 8 | module.enabled = configuredModules[module.name] ? configuredModules[module.name].enabled : false; |
| 478 | ||
| 479 | 8 | if (module.enabled) { |
| 480 | ||
| 481 | // Load the module itself via require | |
| 482 | 8 | requireModule(calipso.modules[module.name], modulePath); |
| 483 | ||
| 484 | // Load the templates (factored out so it can be recalled by watcher) | |
| 485 | 8 | loadModuleTemplates(calipso.modules[module.name], path.join(modulePath,'templates')); |
| 486 | ||
| 487 | } | |
| 488 | ||
| 489 | } | |
| 490 | ||
| 491 | }); | |
| 492 | } | |
| 493 | ||
| 494 | }); | |
| 495 | ||
| 496 | // Now that all are loaded, attach events & depends | |
| 497 | 2 | attachModuleEventsAndDependencies(); |
| 498 | ||
| 499 | // Save configuration changes (if required) | |
| 500 | 2 | if (calipso.config.dirty) { |
| 501 | 0 | calipso.config.save(next); |
| 502 | } else { | |
| 503 | 2 | return next(); |
| 504 | } | |
| 505 | ||
| 506 | } | |
| 507 | ||
| 508 | /** | |
| 509 | * Load data from package.json or theme.json | |
| 510 | */ | |
| 511 | ||
| 512 | 1 | function loadAbout(obj, fromPath, file) { |
| 513 | ||
| 514 | 11 | var fs = calipso.lib.fs; |
| 515 | ||
| 516 | 11 | var packageFile = calipso.lib.path.join(fromPath, file); |
| 517 | ||
| 518 | 11 | if ((fs.existsSync || path.existsSync)(packageFile)) { |
| 519 | 11 | var json = fs.readFileSync(packageFile); |
| 520 | 11 | try { |
| 521 | 11 | obj.about = JSON.parse(json.toString()); |
| 522 | 11 | if (obj.about && obj.about.name) { |
| 523 | 11 | obj.library = obj.about.name; |
| 524 | } else { | |
| 525 | 0 | obj.library = obj.name; |
| 526 | } | |
| 527 | } catch (ex) { | |
| 528 | 0 | obj.about = { |
| 529 | description: 'Invalid ' + file | |
| 530 | }; | |
| 531 | } | |
| 532 | } | |
| 533 | ||
| 534 | } | |
| 535 | ||
| 536 | /** | |
| 537 | * Connect up events and dependencies | |
| 538 | * Must come after all modules are loaded | |
| 539 | */ | |
| 540 | ||
| 541 | 1 | function attachModuleEventsAndDependencies() { |
| 542 | ||
| 543 | 2 | var options = {maxListeners: calipso.config.get('server:events:maxListeners'), notifyDependencyFn: notifyDependenciesOfInit}; |
| 544 | ||
| 545 | 2 | for (var module in calipso.modules) { |
| 546 | ||
| 547 | // Register dependencies | |
| 548 | 8 | registerModuleDependencies(calipso.modules[module]); |
| 549 | ||
| 550 | // Attach event listener | |
| 551 | 8 | calipso.event.addModuleEventListener(calipso.modules[module], options); |
| 552 | ||
| 553 | } | |
| 554 | ||
| 555 | // Sweep through the dependency tree and make sure any broken dependencies are disabled | |
| 556 | 2 | disableBrokenDependencies(); |
| 557 | ||
| 558 | } | |
| 559 | ||
| 560 | /** | |
| 561 | * Ensure dependencies are mapped and registered against parent and child | |
| 562 | */ | |
| 563 | ||
| 564 | 1 | function registerModuleDependencies(module) { |
| 565 | ||
| 566 | 8 | if (module.fn && module.fn.depends && module.enabled) { |
| 567 | ||
| 568 | // Create object to hold dependent status | |
| 569 | 2 | module.check = {}; |
| 570 | ||
| 571 | // Register depends on parent | |
| 572 | 2 | module.fn.depends.forEach(function(dependentModule) { |
| 573 | ||
| 574 | 2 | module.check[dependentModule] = false; |
| 575 | ||
| 576 | 2 | if (calipso.modules[dependentModule] && calipso.modules[dependentModule].enabled) { |
| 577 | ||
| 578 | // Create a notification array to allow this module to notify modules that depend on it | |
| 579 | 2 | calipso.modules[dependentModule].notify = calipso.modules[dependentModule].notify || []; |
| 580 | 2 | calipso.modules[dependentModule].notify.push(module.name); |
| 581 | ||
| 582 | } else { | |
| 583 | ||
| 584 | 0 | calipso.modules[module.name].error = "Module " + module.name + " depends on " + dependentModule + ", but it does not exist or is disabled - this module will not load."; |
| 585 | 0 | calipso.error(calipso.modules[module.name].error); |
| 586 | 0 | calipso.modules[module.name].enabled = false; |
| 587 | ||
| 588 | } | |
| 589 | ||
| 590 | }); | |
| 591 | ||
| 592 | } | |
| 593 | ||
| 594 | } | |
| 595 | ||
| 596 | ||
| 597 | /** | |
| 598 | * Disable everythign in a broken dependency tree | |
| 599 | */ | |
| 600 | ||
| 601 | 1 | function disableBrokenDependencies() { |
| 602 | ||
| 603 | 2 | var disabled = 0; |
| 604 | 2 | for (var moduleName in calipso.modules) { |
| 605 | 8 | var module = calipso.modules[moduleName]; |
| 606 | 8 | if (module.enabled && module.fn && module.fn.depends) { |
| 607 | 2 | module.fn.depends.forEach(function(dependentModule) { |
| 608 | 2 | if (!calipso.modules[dependentModule].enabled) { |
| 609 | 0 | calipso.modules[module.name].error = "Module " + module.name + " depends on " + dependentModule + ", but it does not exist or is disabled - this module will not load."; |
| 610 | 0 | calipso.error(calipso.modules[module.name].error); |
| 611 | 0 | calipso.modules[module.name].enabled = false; |
| 612 | 0 | disabled = disabled + 1; |
| 613 | } | |
| 614 | }); | |
| 615 | } | |
| 616 | } | |
| 617 | ||
| 618 | // Recursive | |
| 619 | 2 | if (disabled > 0) disableBrokenDependencies(); |
| 620 | ||
| 621 | } | |
| 622 | ||
| 623 | /** | |
| 624 | * Notify dependencies for initialisation | |
| 625 | */ | |
| 626 | ||
| 627 | 1 | function notifyDependenciesOfInit(moduleName, options) { |
| 628 | ||
| 629 | 8 | var module = calipso.modules[moduleName]; |
| 630 | 8 | if (module.notify) { |
| 631 | 2 | module.notify.forEach(function(notifyModuleName) { |
| 632 | 2 | notifyDependencyOfInit(moduleName, notifyModuleName, options); |
| 633 | }); | |
| 634 | } | |
| 635 | ||
| 636 | } | |
| 637 | ||
| 638 | ||
| 639 | /** | |
| 640 | * Notify dependencies for routing | |
| 641 | */ | |
| 642 | ||
| 643 | 1 | function notifyDependenciesOfRoute(req, res, moduleName, reqModules) { |
| 644 | ||
| 645 | 8 | var module = calipso.modules[moduleName]; |
| 646 | 8 | if (module.notify) { |
| 647 | 4 | module.notify.forEach(function(notifyModuleName) { |
| 648 | 4 | notifyDependencyOfRoute(req, res, moduleName, notifyModuleName); |
| 649 | }); | |
| 650 | } | |
| 651 | ||
| 652 | } | |
| 653 | ||
| 654 | /** | |
| 655 | * Notify dependency | |
| 656 | * moduleName - module that has init'd | |
| 657 | * notifyModuleName - module to tell | |
| 658 | */ | |
| 659 | ||
| 660 | 1 | function notifyDependencyOfInit(moduleName, notifyModuleName, options) { |
| 661 | ||
| 662 | // Set it to true | |
| 663 | 2 | var module = calipso.modules[notifyModuleName]; |
| 664 | 2 | module.check[moduleName] = true; |
| 665 | 2 | checkInit(module); |
| 666 | ||
| 667 | } | |
| 668 | ||
| 669 | ||
| 670 | /** | |
| 671 | * Notify dependency | |
| 672 | * req - request | |
| 673 | * res - response | |
| 674 | * moduleName - module that has init'd | |
| 675 | * notifyModuleName - module to tell | |
| 676 | */ | |
| 677 | ||
| 678 | 1 | function notifyDependencyOfRoute(req, res, moduleName, notifyModuleName) { |
| 679 | ||
| 680 | 4 | var module = req.event.modules[notifyModuleName]; |
| 681 | 4 | module.check[moduleName] = true; |
| 682 | 4 | checkRouted(req, res, moduleName, notifyModuleName); |
| 683 | ||
| 684 | } | |
| 685 | ||
| 686 | /** | |
| 687 | * Check if all dependencies are met and we should init the module | |
| 688 | */ | |
| 689 | ||
| 690 | 1 | function checkInit(module, next) { |
| 691 | ||
| 692 | 2 | var doInit = true; |
| 693 | 2 | for (var check in module.check) { |
| 694 | 2 | doInit = doInit & module.check[check]; |
| 695 | } | |
| 696 | 2 | if (doInit) { |
| 697 | // Initiate the module, no req for callback | |
| 698 | 2 | initModule(module.name, true, function() {}); |
| 699 | } | |
| 700 | ||
| 701 | } | |
| 702 | ||
| 703 | /** | |
| 704 | * Check if all dependencies are met and we should route the module | |
| 705 | */ | |
| 706 | ||
| 707 | 1 | function checkRouted(req, res, moduleName, notifyModuleName) { |
| 708 | ||
| 709 | 4 | var doRoute = true; |
| 710 | ||
| 711 | 4 | for (var check in req.event.modules[notifyModuleName].check) { |
| 712 | 4 | doRoute = doRoute && req.event.modules[notifyModuleName].check[check]; |
| 713 | } | |
| 714 | ||
| 715 | 4 | if (doRoute) { |
| 716 | // Initiate the module, no req for callback | |
| 717 | // initModule(module.name,true,function() {}); | |
| 718 | 4 | routeModule(req, res, notifyModuleName, true, false, function() {}); |
| 719 | } | |
| 720 | ||
| 721 | } | |
| 722 | ||
| 723 | /** | |
| 724 | * Load the module itself, refactored out to enable watch / reload | |
| 725 | * Note, while it was refactored out, you can't currently reload | |
| 726 | * a module, will patch in node-supervisor to watch the js files and restart | |
| 727 | * the whole server (only option :()) | |
| 728 | */ | |
| 729 | ||
| 730 | 1 | function requireModule(module, modulePath, reload, next) { |
| 731 | ||
| 732 | 8 | var fs = calipso.lib.fs; |
| 733 | 8 | var moduleFile = path.join(modulePath + '/' + module.name); |
| 734 | ||
| 735 | 8 | try { |
| 736 | ||
| 737 | // Require the module | |
| 738 | 8 | module.fn = require(moduleFile); |
| 739 | ||
| 740 | // Attach a router - legacy check for default routes | |
| 741 | 8 | module.router = new calipso.router(module.name, modulePath); |
| 742 | ||
| 743 | // Load the routes if specified as either array or function | |
| 744 | 8 | if (typeof module.fn.routes === "function") module.fn.routes = module.fn.routes(); |
| 745 | 8 | module.fn.routes = module.fn.routes || []; |
| 746 | ||
| 747 | // Ensure the defaultConfig exists (e.g. if it hasn't been required before) | |
| 748 | // This is saved in the wider loadModules loop to ensure only one config save action (if required) | |
| 749 | 8 | if (module.fn.config && !calipso.config.getModuleConfig(module.name, '')) { |
| 750 | 0 | calipso.config.setDefaultModuleConfig(module.name, module.fn.config); |
| 751 | } | |
| 752 | ||
| 753 | } catch (ex) { | |
| 754 | ||
| 755 | 0 | calipso.error("Module " + module.name + " has been disabled because " + ex.message); |
| 756 | 0 | calipso.modules[module.name].enabled = false; |
| 757 | ||
| 758 | } | |
| 759 | ||
| 760 | } | |
| 761 | ||
| 762 | /** | |
| 763 | * Pre load all the templates in a module, synch, but only happens on app start up and config reload | |
| 764 | * This is attached to the templates attribute so used later. | |
| 765 | * | |
| 766 | * @param calipso | |
| 767 | * @param moduleTemplatePath | |
| 768 | * @returns template object | |
| 769 | */ | |
| 770 | ||
| 771 | 1 | function loadModuleTemplates(module, moduleTemplatePath) { |
| 772 | ||
| 773 | 8 | var templates = {}; |
| 774 | ||
| 775 | // Default the template to any loaded in the theme (overrides) | |
| 776 | 8 | var fs = calipso.lib.fs; |
| 777 | ||
| 778 | 8 | if (!(fs.existsSync || calipso.lib.path.existsSync)(moduleTemplatePath)) { |
| 779 | 6 | return null; |
| 780 | } | |
| 781 | ||
| 782 | 2 | fs.readdirSync(moduleTemplatePath).forEach(function(name) { |
| 783 | ||
| 784 | // Template paths and functions | |
| 785 | 2 | var templatePath = moduleTemplatePath + "/" + name; |
| 786 | 2 | var templateExtension = templatePath.match(/([^\.]+)$/)[0]; |
| 787 | 2 | var template = fs.readFileSync(templatePath, 'utf8'); |
| 788 | 2 | var templateName = name.replace(/\.([^\.]+)$/, ''); |
| 789 | ||
| 790 | // Load the template - only if not already loaded by theme (e.g. overriden) | |
| 791 | 2 | var hasTemplate = calipso.utils.hasProperty('theme.cache.modules.' + module.name + '.templates.' + templateName, calipso); |
| 792 | ||
| 793 | 2 | if (hasTemplate) { |
| 794 | ||
| 795 | // Use the theme version | |
| 796 | 0 | templates[templateName] = calipso.theme.cache.modules[module.name].templates[templateName]; |
| 797 | ||
| 798 | } else { | |
| 799 | ||
| 800 | // Else load it | |
| 801 | 2 | if (template) { |
| 802 | // calipso.theme.compileTemplate => ./Theme.js | |
| 803 | 2 | templates[templateName] = calipso.theme.compileTemplate(template, templatePath, templateExtension); |
| 804 | ||
| 805 | // Watch / unwatch files - always unwatch (e.g. around config changes) | |
| 806 | 2 | if (calipso.config.get('performance:watchFiles')) { |
| 807 | ||
| 808 | 2 | fs.unwatchFile(templatePath); // Always unwatch first due to recursive behaviour |
| 809 | 2 | fs.watchFile(templatePath, { |
| 810 | persistent: true, | |
| 811 | interval: 200 | |
| 812 | }, function(curr, prev) { | |
| 813 | 0 | loadModuleTemplates(module, moduleTemplatePath); |
| 814 | 0 | calipso.silly("Module " + module.name + " template " + name + " reloaded."); |
| 815 | }); | |
| 816 | ||
| 817 | } | |
| 818 | ||
| 819 | } | |
| 820 | } | |
| 821 | }); | |
| 822 | ||
| 823 | 2 | module.templates = templates; |
| 824 | ||
| 825 | } | |
| 826 | ||
| 827 | /** | |
| 828 | * Exports | |
| 829 | */ | |
| 830 | 1 | module.exports = { |
| 831 | loadModules: loadModules, | |
| 832 | initModules: initModules, | |
| 833 | eventRouteModules: eventRouteModules, | |
| 834 | notifyDependenciesOfInit: notifyDependenciesOfInit, | |
| 835 | notifyDependenciesOfRoute: notifyDependenciesOfRoute, | |
| 836 | registerDependencies: registerDependencies, | |
| 837 | loadAbout: loadAbout | |
| 838 | }; |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso Permissions Class | |
| 3 | * Copyright(c) 2011 Clifton Cunningham | |
| 4 | * MIT Licensed | |
| 5 | * | |
| 6 | * This library adds a permissions class to the router, defining functions that are used by the router to control access. | |
| 7 | * | |
| 8 | */ | |
| 9 | ||
| 10 | 1 | var rootpath = process.cwd() + '/', |
| 11 | path = require('path'), | |
| 12 | calipso = require(path.join('..', 'calipso')); | |
| 13 | ||
| 14 | /** | |
| 15 | * A set of helper functions to simplify the application of filters, as well as store | |
| 16 | * the in memory map of roles to permissions (in memory for performance reasons) | |
| 17 | */ | |
| 18 | 1 | var PermissionHelpers = { |
| 19 | ||
| 20 | // Holder of defined permissions | |
| 21 | permissions: {}, | |
| 22 | sortedPermissions: [], | |
| 23 | structuredPermissions: {}, | |
| 24 | ||
| 25 | // Clear all oaded permissions | |
| 26 | clearPermissionRoles: function() { | |
| 27 | ||
| 28 | 0 | var self = this; |
| 29 | 0 | for (var perm in self.permissions) { |
| 30 | 0 | delete self.permissions[perm].roles; |
| 31 | 0 | self.permissions[perm].roles = []; |
| 32 | } | |
| 33 | ||
| 34 | }, | |
| 35 | ||
| 36 | // Add a permission | |
| 37 | addPermission: function(permission, description, isCrud) { | |
| 38 | ||
| 39 | 3 | var self = this; |
| 40 | ||
| 41 | // if Crud, automatically add level below | |
| 42 | 3 | if (isCrud) { |
| 43 | 0 | calipso.lib._.map(["view", "create", "update", "delete"], function(crudAction) { |
| 44 | 0 | var crudPermission = permission + ":" + crudAction; |
| 45 | 0 | self.permissions[crudPermission] = { |
| 46 | roles: [], | |
| 47 | queries: [], | |
| 48 | description: crudAction + " " + description | |
| 49 | }; | |
| 50 | 0 | self.sortedPermissions.push(crudPermission); |
| 51 | }); | |
| 52 | } else { | |
| 53 | ||
| 54 | // Add Permission always resets it if it already exists | |
| 55 | 3 | self.permissions[permission] = { |
| 56 | roles: [], | |
| 57 | queries: [], | |
| 58 | description: description | |
| 59 | }; | |
| 60 | 3 | self.sortedPermissions.push(permission); |
| 61 | ||
| 62 | } | |
| 63 | ||
| 64 | }, | |
| 65 | ||
| 66 | structureAndSort: function() { | |
| 67 | ||
| 68 | 0 | var self = this; |
| 69 | ||
| 70 | // This could be done by the permissions module | |
| 71 | 0 | self.sortedPermissions.sort(function(a, b) { |
| 72 | 0 | return a < b; |
| 73 | }); | |
| 74 | ||
| 75 | // Now we need to create our permissions object structure | |
| 76 | 0 | self.sortedPermissions.forEach(function(value) { |
| 77 | ||
| 78 | 0 | var path = value.split(":"), |
| 79 | target = self.structuredPermissions, | |
| 80 | counter = 0; | |
| 81 | ||
| 82 | 0 | while (path.length > 1) { |
| 83 | 0 | key = path.shift(); |
| 84 | 0 | if (!target[key] || typeof target[key] !== 'object') { |
| 85 | 0 | target[key] = {}; |
| 86 | } | |
| 87 | 0 | target = target[key]; |
| 88 | } | |
| 89 | ||
| 90 | // Set the specified value in the nested JSON structure | |
| 91 | 0 | key = path.shift(); |
| 92 | 0 | if (typeof target[key] !== "object") { |
| 93 | 0 | target[key] = self.permissions[value].roles; |
| 94 | } | |
| 95 | ||
| 96 | }); | |
| 97 | ||
| 98 | }, | |
| 99 | ||
| 100 | // Add a map between role / permission (this is loaded via the user module) | |
| 101 | addPermissionRole: function(permission, role) { | |
| 102 | ||
| 103 | 3 | var self = this; |
| 104 | ||
| 105 | // Store this as a simple in memory map | |
| 106 | 3 | if (self.permissions[permission]) { |
| 107 | 3 | self.permissions[permission].roles.push(role); |
| 108 | 3 | return true; |
| 109 | } else { | |
| 110 | 0 | calipso.warn("Attempted to map role: " + role + " to a permission: " + permission + " that does not exist (perhaps related to a disabled module?)."); |
| 111 | 0 | return false; |
| 112 | } | |
| 113 | ||
| 114 | }, | |
| 115 | ||
| 116 | // Does a user have a role | |
| 117 | hasRole: function(role) { | |
| 118 | // Curried filter | |
| 119 | 0 | return function(user) { |
| 120 | 0 | var isAllowed = user.roles.indexOf(role) >= 0 ? true : false, |
| 121 | message = isAllowed ? ('User has role ' + role) : 'You dont have the appropriate roles to view that page!'; | |
| 122 | 0 | return { |
| 123 | allow: isAllowed, | |
| 124 | msg: message | |
| 125 | }; | |
| 126 | }; | |
| 127 | }, | |
| 128 | ||
| 129 | // Does a user have a permission | |
| 130 | hasPermission: function(permission) { | |
| 131 | ||
| 132 | 5 | var self = this; |
| 133 | ||
| 134 | // Curried filter | |
| 135 | 5 | return function(user) { |
| 136 | ||
| 137 | // Check if the user has a role that maps to the permission | |
| 138 | 26 | var userRoles = user.roles, |
| 139 | permissionRoles = self.permissions[permission] ? self.permissions[permission].roles : []; | |
| 140 | ||
| 141 | // Check if allowed based on intersection of user roles and roles that have permission | |
| 142 | 26 | var isAllowed = calipso.lib._.intersect(permissionRoles, userRoles).length > 0, |
| 143 | message = isAllowed ? ('User has permission ' + permission) : 'You do not have any of the roles required to perform that action.'; | |
| 144 | ||
| 145 | ||
| 146 | 26 | return { |
| 147 | allow: isAllowed, | |
| 148 | msg: message | |
| 149 | }; | |
| 150 | ||
| 151 | }; | |
| 152 | ||
| 153 | } | |
| 154 | ||
| 155 | }; | |
| 156 | ||
| 157 | ||
| 158 | /** | |
| 159 | * The default calipso permission filter, this is attached to every route, and processed as part of the route matching. | |
| 160 | */ | |
| 161 | 1 | function PermissionFilter(options, permit) { |
| 162 | ||
| 163 | // Store the options | |
| 164 | 36 | var self = this; |
| 165 | 36 | self.options = options; |
| 166 | ||
| 167 | 36 | if(permit) { |
| 168 | 30 | if(typeof permit === 'function') { |
| 169 | // permit is already a fn created by a helper | |
| 170 | 28 | self.permit = permit; |
| 171 | } else { | |
| 172 | // permit is a string - e.g. 'admin:core:configuration' | |
| 173 | 2 | self.permit = calipso.permission.Helper.hasPermission(permit); |
| 174 | } | |
| 175 | } | |
| 176 | ||
| 177 | } | |
| 178 | ||
| 179 | 1 | PermissionFilter.prototype.check = function(req) { |
| 180 | ||
| 181 | 30 | var self = this; |
| 182 | 30 | if (!self.permit && self.options.permit) self.permit = self.options.permit; |
| 183 | 30 | if (self.permit) { |
| 184 | ||
| 185 | 30 | var user = req.session.user; |
| 186 | 30 | var isAdmin = req.session.user && req.session.user.isAdmin; |
| 187 | ||
| 188 | 32 | if (isAdmin) return { |
| 189 | allow: true | |
| 190 | }; // Admins always access everything | |
| 191 | // Else check for a specific permission | |
| 192 | 28 | if (user) { |
| 193 | 26 | return self.permit(user); |
| 194 | } else { | |
| 195 | 2 | return { |
| 196 | allow: false, | |
| 197 | msg: 'You must be a logged in user to view that page' | |
| 198 | }; | |
| 199 | } | |
| 200 | ||
| 201 | } else { | |
| 202 | 0 | return { |
| 203 | allow: true | |
| 204 | }; | |
| 205 | } | |
| 206 | ||
| 207 | }; | |
| 208 | ||
| 209 | ||
| 210 | /** | |
| 211 | * Export an instance of our object | |
| 212 | */ | |
| 213 | 1 | exports.Filter = PermissionFilter; |
| 214 | 1 | exports.Helper = PermissionHelpers; |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso Core Library | |
| 3 | * Copyright(c) 2011 Clifton Cunningham | |
| 4 | * MIT Licensed | |
| 5 | * | |
| 6 | * The Calipso Router provides a router object to each module that enables each module to register | |
| 7 | * its own functions to respond to URL patterns (as per the typical Express approach). Note | |
| 8 | * that Calipso itself does not respond to any URL outside of those exposed by a module, if all are disabled | |
| 9 | * the application will do nothing. | |
| 10 | * | |
| 11 | * Borrowed liberally from Connect / ExpressJS itself for this, thanks for the great work! | |
| 12 | */ | |
| 13 | ||
| 14 | /** | |
| 15 | * Includes | |
| 16 | */ | |
| 17 | 1 | var rootpath = process.cwd() + '/', |
| 18 | path = require('path'), | |
| 19 | calipso = require(path.join('..', 'calipso')), | |
| 20 | url = require('url'), | |
| 21 | fs = require('fs'), | |
| 22 | PermissionFilter = require('./Permission').Filter, | |
| 23 | PermissionHelper = require('./Permission').Helper, | |
| 24 | blocks = require('./Blocks'); | |
| 25 | ||
| 26 | /** | |
| 27 | * Core router object, use the return model to ensure | |
| 28 | * that we always return a new instance when called. | |
| 29 | * | |
| 30 | * A Router is attached to each module, and allows each module to effectively | |
| 31 | * act as its own controller in a mini MVC model. | |
| 32 | * | |
| 33 | * This class exposes: | |
| 34 | * | |
| 35 | * addRoutes: function, to add Routes to a module. | |
| 36 | * route: iterate through the routes, match, and then call the matched function in the module. | |
| 37 | * | |
| 38 | */ | |
| 39 | 1 | var Router = function(moduleName, modulePath) { |
| 40 | ||
| 41 | 8 | return { |
| 42 | ||
| 43 | moduleName: moduleName, | |
| 44 | modulePath: modulePath, | |
| 45 | routes: [], | |
| 46 | ||
| 47 | /** | |
| 48 | * A route is defined by three parameters: | |
| 49 | * | |
| 50 | * path: a string in the form 'GET /url' where the first piece is the HTTP method to respond to. | |
| 51 | * OR | |
| 52 | * a regex function (it matches only on GET requests). | |
| 53 | * fn: the function in the module to call if the route matches. | |
| 54 | * options: additional configuration options, specifically: | |
| 55 | * end - deprecated. TODO CLEANUP | |
| 56 | * admin - is the route an administrative route (user must have isAdmin = true). | |
| 57 | */ | |
| 58 | addRoute: function(options, next, legacy_options, legacy_next) { | |
| 59 | ||
| 60 | // Default options | |
| 61 | 8 | var self = this, |
| 62 | defaults = { | |
| 63 | end: true, | |
| 64 | admin: false, | |
| 65 | user: false, | |
| 66 | cache: false, | |
| 67 | permit: null | |
| 68 | }; | |
| 69 | ||
| 70 | // Deal with legacy, this will eventually just be options, next to enable simpler invocation | |
| 71 | // And to make it more extensible | |
| 72 | 8 | if (typeof legacy_next === "function") { |
| 73 | ||
| 74 | 6 | var routePath = options, |
| 75 | fn = next; | |
| 76 | ||
| 77 | // Set the variables | |
| 78 | 6 | options = legacy_options || {}; |
| 79 | 6 | options.path = routePath; |
| 80 | 6 | options.fn = fn; |
| 81 | ||
| 82 | 6 | next = legacy_next; |
| 83 | ||
| 84 | } | |
| 85 | ||
| 86 | // Default options | |
| 87 | 8 | options = calipso.lib._.extend(defaults, options); |
| 88 | 8 | options.permitFn = new PermissionFilter(options, options.permit); |
| 89 | ||
| 90 | 8 | self.routes.push(options); |
| 91 | ||
| 92 | 8 | next(); |
| 93 | ||
| 94 | }, | |
| 95 | ||
| 96 | /** | |
| 97 | * Module routing function, iterates through the configured routes and trys to match. | |
| 98 | * This has been borrowed from the Express core routing module and refactored slightly | |
| 99 | * to deal with the fact this is much more specific. | |
| 100 | */ | |
| 101 | route: function(req, res, next) { | |
| 102 | ||
| 103 | 16 | var self = this, |
| 104 | requestUrl = url.parse(req.url, true), | |
| 105 | routes = this.routes; | |
| 106 | ||
| 107 | // Use step to enable parallel execution | |
| 108 | 16 | calipso.lib.step( |
| 109 | ||
| 110 | function matchRoutes() { | |
| 111 | ||
| 112 | // Emit event to indicate starting | |
| 113 | 16 | var i, l, group = this.group(); |
| 114 | ||
| 115 | 16 | for (i = 0, l = routes.length; i < l; i = i + 1) { |
| 116 | ||
| 117 | 16 | var keys = [], |
| 118 | route = routes[i], | |
| 119 | templateFn = null, | |
| 120 | block = "", | |
| 121 | routeMethod = "", | |
| 122 | routeRegEx, j, paramLen, param, allPages = false; | |
| 123 | ||
| 124 | 16 | if (typeof route.path === "string") { |
| 125 | 8 | routeMethod = route.path.split(" ")[0]; |
| 126 | 8 | routeRegEx = normalizePath(route.path.split(" ")[1], keys); |
| 127 | } else { | |
| 128 | 8 | routeRegEx = route.path; |
| 129 | 8 | allPages = true; // Is a regex |
| 130 | } | |
| 131 | ||
| 132 | 16 | var captures = requestUrl.pathname.match(routeRegEx); |
| 133 | ||
| 134 | 16 | if (captures && (!routeMethod || req.method === routeMethod)) { |
| 135 | ||
| 136 | // Check to see if we matched a non /.*/ route to flag a 404 later | |
| 137 | 11 | res.routeMatched = !allPages || res.routeMatched; |
| 138 | ||
| 139 | // If admin, then set the route | |
| 140 | 11 | if (route.admin) { |
| 141 | 2 | res.layout = "admin"; |
| 142 | 2 | if(!route.permit){ |
| 143 | 0 | calipso.debug("Route has admin only but no permit is defined!"); |
| 144 | 0 | route.permit = calipso.permission.Helper.hasPermission("admin"); |
| 145 | } | |
| 146 | } | |
| 147 | ||
| 148 | // TODO | |
| 149 | 11 | var isAdmin = req.session.user && req.session.user.isAdmin; |
| 150 | ||
| 151 | // Check to see if it requires logged in user access | |
| 152 | 11 | if (route.permit) { |
| 153 | ||
| 154 | 2 | var permit = route.permitFn.check(req); |
| 155 | ||
| 156 | 2 | if (typeof permit !== "object") permit = { |
| 157 | allow: false, | |
| 158 | msg: 'You don\'t have the appropriate permissions to view that page.' | |
| 159 | }; | |
| 160 | 2 | if (!permit.allow) { |
| 161 | 1 | if (!allPages) { |
| 162 | 1 | if (!req.cookies.logout) { |
| 163 | 1 | req.flash('error', req.t(permit.msg)); |
| 164 | 1 | res.statusCode = 401; |
| 165 | } | |
| 166 | 1 | res.redirect("/"); |
| 167 | 1 | return group()(); |
| 168 | } else { | |
| 169 | // Simply ignore silently | |
| 170 | 0 | return group()(); |
| 171 | } | |
| 172 | } | |
| 173 | } | |
| 174 | ||
| 175 | // Debugging - only when logged in as admin user | |
| 176 | // calipso.silly("Module " + router.moduleName + " matched route: " + requestUrl.pathname + " / " + routeRegEx.toString() + " [" + res.routeMatched + "]"); | |
| 177 | // Lookup the template for this route | |
| 178 | 10 | if (route.template) { |
| 179 | 1 | templateFn = calipso.modules[self.moduleName].templates[route.template]; |
| 180 | 1 | if (!templateFn && route.template) { |
| 181 | 0 | var err = new Error("The specified template: " + route.template + " does not exist in the module: " + self.modulePath); |
| 182 | 0 | return group()(err); |
| 183 | } else { | |
| 184 | 1 | calipso.silly("Using template: " + route.template + " for module: " + self.modulePath); |
| 185 | } | |
| 186 | 1 | route.templateFn = templateFn; |
| 187 | } | |
| 188 | ||
| 189 | // Set the object to hold the rendered blocks if it hasn't been created already | |
| 190 | 10 | if (!res.renderedBlocks) { |
| 191 | 1 | res.renderedBlocks = new blocks.RenderedBlocks(calipso.cacheService); |
| 192 | } | |
| 193 | ||
| 194 | // Copy over any params that make sense from the url | |
| 195 | 10 | req.moduleParams = {}; |
| 196 | 10 | for (j = 1, paramLen = captures.length; j < paramLen; j = j + 1) { |
| 197 | 0 | var key = keys[j - 1], |
| 198 | val = typeof captures[j] === 'string' ? decodeURIComponent(captures[j]) : captures[j]; | |
| 199 | 0 | if (key) { |
| 200 | 0 | req.moduleParams[key] = val; |
| 201 | } else { | |
| 202 | // Comes from custom regex, no key | |
| 203 | // req.moduleParams["regex"] = val; | |
| 204 | } | |
| 205 | } | |
| 206 | ||
| 207 | // Convert any url parameters if we are not a .* match | |
| 208 | 10 | if (requestUrl.query && !allPages) { |
| 209 | 2 | for (param in requestUrl.query) { |
| 210 | 0 | if (requestUrl.query.hasOwnProperty(param)) { |
| 211 | 0 | req.moduleParams[param] = requestUrl.query[param]; |
| 212 | } | |
| 213 | } | |
| 214 | } | |
| 215 | ||
| 216 | // Store the params for use outside the router | |
| 217 | 10 | res.params = res.params || {}; |
| 218 | 10 | calipso.lib._.extend(res.params, req.moduleParams); |
| 219 | ||
| 220 | // Set if we should cache this block - do not cache by default, do not cache admins | |
| 221 | 10 | var cacheBlock = res.renderedBlocks.contentCache[block] = route.cache && !isAdmin; |
| 222 | 10 | var cacheEnabled = calipso.config.get('performance:cache:enabled'); |
| 223 | ||
| 224 | 10 | if (route.block && cacheBlock && cacheEnabled) { |
| 225 | ||
| 226 | 0 | var cacheKey = calipso.cacheService.getCacheKey(['block', route.block], res.params); |
| 227 | ||
| 228 | 0 | calipso.cacheService.check(cacheKey, function(err, isCached) { |
| 229 | 0 | if (isCached) { |
| 230 | // Set the block from the cache, set layout if needed | |
| 231 | 0 | res.renderedBlocks.getCache(cacheKey, route.block, function(err, layout) { |
| 232 | 0 | if (layout) res.layout = layout; |
| 233 | 0 | group()(err); |
| 234 | }); | |
| 235 | } else { | |
| 236 | ||
| 237 | // Execute the module route function and cache the result | |
| 238 | 0 | self.routeFn(req, res, route, group()); |
| 239 | ||
| 240 | } | |
| 241 | }); | |
| 242 | ||
| 243 | } else { | |
| 244 | ||
| 245 | 10 | self.routeFn(req, res, route, group()); |
| 246 | ||
| 247 | } | |
| 248 | ||
| 249 | } | |
| 250 | ||
| 251 | } | |
| 252 | ||
| 253 | }, | |
| 254 | ||
| 255 | function allMatched(err) { | |
| 256 | ||
| 257 | // Once all functions have been called, log the error and pass it back up the tree. | |
| 258 | 16 | if (err) { |
| 259 | // Enrich the error message with info on the module | |
| 260 | // calipso.error("Error in module " + this.moduleName + ", of " + err.message); | |
| 261 | 0 | err.message = err.message + " Calipso Module: " + self.moduleName; |
| 262 | } | |
| 263 | ||
| 264 | // Emit routed event | |
| 265 | 16 | next(err, self.moduleName); |
| 266 | ||
| 267 | }); | |
| 268 | ||
| 269 | }, | |
| 270 | ||
| 271 | // Wrapper for router | |
| 272 | // This deals with legacy modules pre 0.3.0 (will remove later) | |
| 273 | routeFn: function(req, res, route, next) { | |
| 274 | ||
| 275 | 10 | if (typeof route.fn !== "function") console.dir(route); |
| 276 | ||
| 277 | 10 | var legacyRouteFn = route.fn.length === 5 ? true : false; |
| 278 | 10 | if (legacyRouteFn) { |
| 279 | 0 | route.fn(req, res, route.templateFn, route.block, next); |
| 280 | } else { | |
| 281 | 10 | route.fn(req, res, route, next); |
| 282 | } | |
| 283 | ||
| 284 | } | |
| 285 | ||
| 286 | }; | |
| 287 | ||
| 288 | }; | |
| 289 | ||
| 290 | ||
| 291 | /** | |
| 292 | * Normalize the given path string, | |
| 293 | * returning a regular expression. | |
| 294 | * | |
| 295 | * An empty array should be passed, | |
| 296 | * which will contain the placeholder | |
| 297 | * key names. For example "/user/:id" will | |
| 298 | * then contain ["id"]. | |
| 299 | * | |
| 300 | * BORROWED FROM Connect | |
| 301 | * | |
| 302 | * @param {String} path | |
| 303 | * @param {Array} keys | |
| 304 | * @return {RegExp} | |
| 305 | * @api private | |
| 306 | */ | |
| 307 | ||
| 308 | 1 | function normalizePath(path, keys) { |
| 309 | 8 | path = path.concat('/?').replace(/\/\(/g, '(?:/').replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) { |
| 310 | 0 | keys.push(key); |
| 311 | 0 | slash = slash || ''; |
| 312 | 0 | return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || '([^/]+?)') + ')' + (optional || ''); |
| 313 | }).replace(/([\/.])/g, '\\$1').replace(/\*/g, '(.+)'); | |
| 314 | ||
| 315 | 8 | return new RegExp('^' + path + '$', 'i'); |
| 316 | } | |
| 317 | ||
| 318 | /** | |
| 319 | * Exports | |
| 320 | */ | |
| 321 | 1 | module.exports = Router; |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso MongoDB Storage Library | |
| 3 | * Copyright(c) 2011 Clifton Cunningham | |
| 4 | * MIT Licensed | |
| 5 | * | |
| 6 | * This library provides a few simple functions that can be used to help manage MongoDB and Mongoose. | |
| 7 | */ | |
| 8 | ||
| 9 | 1 | var rootpath = process.cwd(), |
| 10 | path = require('path'), | |
| 11 | events = require('events'), | |
| 12 | mongoStore = require('connect-mongodb'), | |
| 13 | mongoose = require('mongoose'), | |
| 14 | calipso = require(path.join('..', 'calipso')); | |
| 15 | ||
| 16 | 1 | function Storage() { |
| 17 | // Store running map reduce functions | |
| 18 | 1 | this.mr = {}; |
| 19 | } | |
| 20 | ||
| 21 | /** | |
| 22 | * Check that the mongodb instance specified in the configuration is valid. | |
| 23 | */ | |
| 24 | 1 | Storage.prototype.mongoConnect = function(dbUri, checkInstalling, next) { |
| 25 | ||
| 26 | // Test the mongodb configuration | |
| 27 | 2 | var isInstalled = calipso.config.get('installed'); |
| 28 | ||
| 29 | // If first option is callback, ste dbUri to config value | |
| 30 | 2 | if (typeof dbUri === "function") { |
| 31 | 0 | next = dbUri; |
| 32 | 0 | dbUri = calipso.config.get('database:uri'); |
| 33 | 0 | checkInstalling = false; |
| 34 | } | |
| 35 | ||
| 36 | // Check we are installing ... | |
| 37 | 2 | if (checkInstalling) { |
| 38 | 0 | var db = mongoose.createConnection(dbUri, function(err) { |
| 39 | 0 | next(err, false); |
| 40 | }); | |
| 41 | 0 | return; |
| 42 | } | |
| 43 | ||
| 44 | 2 | if (isInstalled) { |
| 45 | ||
| 46 | // Always disconnect first just in case any left overs from installation | |
| 47 | 2 | mongoose.disconnect(function() { |
| 48 | ||
| 49 | // TODO - what the hell is going on with mongoose? | |
| 50 | 2 | calipso.db = mongoose.createConnection(dbUri, function(err) { |
| 51 | ||
| 52 | 2 | if (err) { |
| 53 | ||
| 54 | 0 | calipso.error("Unable to connect to the specified database ".red + dbUri + ", the problem was: ".red + err.message); |
| 55 | 0 | mongoose.disconnect(function() { |
| 56 | 0 | return next(err, false); |
| 57 | }); | |
| 58 | ||
| 59 | } else { | |
| 60 | ||
| 61 | 2 | calipso.silly("Database connection to " + dbUri + " was successful."); |
| 62 | ||
| 63 | // Replace the inmemory session with mongodb backed one | |
| 64 | 2 | var foundMiddleware = false, mw; |
| 65 | ||
| 66 | 2 | calipso.app.stack.forEach(function(middleware, key) { |
| 67 | 6 | if (middleware.handle.tag === 'session') { |
| 68 | 2 | foundMiddleware = true; |
| 69 | 2 | var maxAge = calipso.config.get('session:maxAge'); |
| 70 | 2 | if (maxAge) { |
| 71 | 0 | try { |
| 72 | 0 | maxAge = Number(maxAge) * 1000; |
| 73 | } | |
| 74 | catch (e) { | |
| 75 | 0 | calipso.error('MaxAge value ' + maxAge + ' is not a numeric string'); |
| 76 | 0 | maxAge = undefined; |
| 77 | } | |
| 78 | } | |
| 79 | 2 | mw = calipso.lib.express.session({ |
| 80 | secret: calipso.config.get('session:secret'), | |
| 81 | store: calipso.app.sessionStore = new mongoStore({ | |
| 82 | db: calipso.db.db | |
| 83 | }), | |
| 84 | cookie: { maxAge: maxAge } | |
| 85 | }); | |
| 86 | 2 | mw.tag = 'session'; |
| 87 | 2 | calipso.app.stack[key].handle = mw; |
| 88 | } | |
| 89 | }); | |
| 90 | ||
| 91 | 2 | if (!foundMiddleware) { |
| 92 | 0 | return next(new Error("Unable to load the MongoDB backed session, please check your session and db configuration"), false); |
| 93 | } | |
| 94 | ||
| 95 | 2 | return next(null, true); |
| 96 | ||
| 97 | } | |
| 98 | }); | |
| 99 | }); | |
| 100 | ||
| 101 | } else { | |
| 102 | ||
| 103 | 0 | calipso.silly("Database connection not attempted to " + dbUri + " as in installation mode."); |
| 104 | ||
| 105 | // Create a dummy connection to enable models to be defined | |
| 106 | 0 | calipso.db = mongoose.createConnection(''); |
| 107 | ||
| 108 | 0 | next(null, false); |
| 109 | ||
| 110 | } | |
| 111 | ||
| 112 | }; | |
| 113 | ||
| 114 | 1 | module.exports = new Storage(); |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Calipso Table Rendering Library | |
| 4 | * | |
| 5 | * Copyright(c) 2011 Clifton Cunningham <clifton.cunningham@gmail.com> | |
| 6 | * MIT Licensed | |
| 7 | * | |
| 8 | * Loaded into calipso as a plugin, used to simplify the rendering of tabular data. | |
| 9 | * Including things such as rendering table sorting elements etc. | |
| 10 | * TODO: validation, redisplay of submitted values | |
| 11 | * | |
| 12 | */ | |
| 13 | ||
| 14 | 1 | var rootpath = process.cwd() + '/', |
| 15 | path = require('path'), | |
| 16 | calipso = require(path.join('..', 'calipso')), | |
| 17 | qs = require('qs'), | |
| 18 | pager = require(path.join(rootpath, 'utils/pager')), | |
| 19 | merge = require('connect').utils.merge; | |
| 20 | ||
| 21 | // Global variable (in this context) for translation function | |
| 22 | 1 | var t; |
| 23 | ||
| 24 | /** | |
| 25 | * The default calipso table object, with default configuration values. | |
| 26 | * Constructor | |
| 27 | */ | |
| 28 | ||
| 29 | 1 | function CalipsoTable() { |
| 30 | ||
| 31 | //TODO Allow over-ride | |
| 32 | } | |
| 33 | ||
| 34 | /** | |
| 35 | * Export an instance of our table object | |
| 36 | */ | |
| 37 | 1 | module.exports = new CalipsoTable(); |
| 38 | ||
| 39 | ||
| 40 | /** | |
| 41 | * Table Renderer, controls the overall creation of the tablle based on a form json object passed | |
| 42 | * in as the first parameter. The structure of this object is as follows: | |
| 43 | * | |
| 44 | * table | |
| 45 | * id : Unique ID that will become the form ID. | |
| 46 | * title : Title to show at the top of the form. | |
| 47 | * cls : css class | |
| 48 | * columns [*] : Form fields array - can be in form or section. | |
| 49 | * label : Label for form field. | |
| 50 | * name : Name of form element to be passed back with the value. | |
| 51 | * type : Type of element, based on the form functions defined below. | |
| 52 | * sortable : true / false | |
| 53 | * fn : Function to apply to the row | |
| 54 | * data [*] : Array of buttons to be rendered at the bottom of the form. | |
| 55 | * view : COntrols display of this form | |
| 56 | * pager : show pager | |
| 57 | * from : from page | |
| 58 | * to : to page | |
| 59 | * url : base url for links | |
| 60 | * sort : {} of sort field name:dir (asc|desc) | |
| 61 | * | |
| 62 | * This is synchronous so that it can be called from views. | |
| 63 | * | |
| 64 | * @param item : the json object representing the table | |
| 65 | * @param req : The request object | |
| 66 | */ | |
| 67 | 1 | CalipsoTable.prototype.render = function(req, item) { |
| 68 | ||
| 69 | // Store local reference to the request for use during translation | |
| 70 | 1 | t = req.t; |
| 71 | ||
| 72 | 1 | return ( |
| 73 | this.start_table(item) + this.render_headers(item) + this.render_data(item, req) + this.end_table(item) + this.render_pager(item, item.view.url)); | |
| 74 | ||
| 75 | }; | |
| 76 | ||
| 77 | /** | |
| 78 | * Render the initial table tag | |
| 79 | * | |
| 80 | * @param form | |
| 81 | * @returns {String} | |
| 82 | */ | |
| 83 | 1 | CalipsoTable.prototype.start_table = function(table) { |
| 84 | 1 | return ('<table id="' + table.id + '"' + (table.cls ? ' class="' + table.cls + '"' : "") + '>'); |
| 85 | }; | |
| 86 | ||
| 87 | /** | |
| 88 | * Close the table | |
| 89 | * @param table | |
| 90 | * @returns {String} | |
| 91 | */ | |
| 92 | 1 | CalipsoTable.prototype.end_table = function(table) { |
| 93 | 1 | return '</table>'; |
| 94 | }; | |
| 95 | ||
| 96 | ||
| 97 | /** | |
| 98 | * Render headers | |
| 99 | * @param table | |
| 100 | * @returns {String} | |
| 101 | */ | |
| 102 | 1 | CalipsoTable.prototype.render_headers = function(table) { |
| 103 | ||
| 104 | // If there are no columns, return | |
| 105 | 1 | if (table.columns.length === 0) throw new Error("You must define columns to render a table."); |
| 106 | ||
| 107 | // Test | |
| 108 | 1 | var output = "<thead><tr>"; |
| 109 | ||
| 110 | // Iterate | |
| 111 | 1 | table.columns.forEach(function(column, key) { |
| 112 | ||
| 113 | // set the class | |
| 114 | // Check to see if we are sorting by this column | |
| 115 | 3 | var cls = getHeaderClass(table, column); |
| 116 | ||
| 117 | 3 | output += "<th" + (' class="' + cls + '"') + (column.sort ? ' name="' + column.sort + '"' : (column.name ? ' name="' + column.name + '"' : "")) + ">"; |
| 118 | 3 | output += column.label; |
| 119 | 3 | output += "</th>"; |
| 120 | ||
| 121 | }); | |
| 122 | ||
| 123 | 1 | output += "</tr></thead>"; |
| 124 | ||
| 125 | 1 | return output; |
| 126 | ||
| 127 | }; | |
| 128 | ||
| 129 | /** | |
| 130 | * Helper function to determine column header sort class | |
| 131 | */ | |
| 132 | ||
| 133 | 1 | function getHeaderClass(table, column) { |
| 134 | ||
| 135 | // Default class | |
| 136 | 3 | var cls = column.cls || ''; |
| 137 | // Sortable | |
| 138 | 3 | cls += column.sortable === false ? '' : 'sortable'; |
| 139 | ||
| 140 | 3 | if (table.view && table.view.sort && (table.view.sort[column.name] || table.view.sort[column.sort])) { |
| 141 | 0 | cls += ' sorted-' + (table.view.sort[column.sort] || table.view.sort[column.name]); |
| 142 | } else { | |
| 143 | // Leave as is | |
| 144 | } | |
| 145 | 3 | return cls; |
| 146 | ||
| 147 | } | |
| 148 | ||
| 149 | /** | |
| 150 | * Convert a sortBy parameter into mongo sort queries | |
| 151 | */ | |
| 152 | 1 | CalipsoTable.prototype.sortQuery = function(qry, sortBy) { |
| 153 | ||
| 154 | 0 | if (typeof sortBy === 'string') sortBy = [sortBy]; |
| 155 | 0 | if (!sortBy || sortBy.length === 0) return qry; |
| 156 | ||
| 157 | 0 | sortBy.forEach(function(sort) { |
| 158 | 0 | var sortArr = sort.split(","); |
| 159 | 0 | if (sortArr.length === 2) { |
| 160 | 0 | var dir = sortArr[1] === 'asc' ? 1 : (sortArr[1] === 'desc' ? -1 : 0); |
| 161 | 0 | qry = qry.sort(sortArr[0], dir); |
| 162 | } | |
| 163 | }); | |
| 164 | ||
| 165 | 0 | return qry; |
| 166 | }; | |
| 167 | ||
| 168 | ||
| 169 | /** | |
| 170 | * Convert a sort by form param into a view sort object | |
| 171 | */ | |
| 172 | 1 | CalipsoTable.prototype.parseSort = function(sortBy) { |
| 173 | ||
| 174 | 0 | var options = {}; |
| 175 | ||
| 176 | 0 | if (typeof sortBy === 'string') sortBy = [sortBy]; |
| 177 | 0 | if (!sortBy || sortBy.length === 0) return options; |
| 178 | ||
| 179 | 0 | sortBy.forEach(function(sort) { |
| 180 | 0 | var sortArr = sort.split(","); |
| 181 | 0 | if (sortArr.length === 2) { |
| 182 | 0 | options[sortArr[0]] = sortArr[1]; |
| 183 | } | |
| 184 | }); | |
| 185 | ||
| 186 | 0 | return options; |
| 187 | }; | |
| 188 | ||
| 189 | ||
| 190 | /** | |
| 191 | * Render headers | |
| 192 | * @param table | |
| 193 | * @returns {String} | |
| 194 | */ | |
| 195 | 1 | CalipsoTable.prototype.render_data = function(table, req) { |
| 196 | ||
| 197 | // If there are no columns, return | |
| 198 | 1 | if (table.columns.length === 0) throw new Error("You must define columns to render a table."); |
| 199 | ||
| 200 | // Test | |
| 201 | 1 | var output = "<tbody>"; |
| 202 | ||
| 203 | // Iterate | |
| 204 | 1 | table.data.forEach(function(row) { |
| 205 | 2 | output += "<tr>"; |
| 206 | // Iterate over the columns | |
| 207 | 2 | table.columns.forEach(function(column) { |
| 208 | 6 | output += "<td>"; |
| 209 | 6 | if (column.name in row) { |
| 210 | 6 | if (typeof column.fn === "function") { |
| 211 | 0 | output += column.fn(req, row); |
| 212 | } else { | |
| 213 | 6 | output += row[column.name]; |
| 214 | } | |
| 215 | } else { | |
| 216 | 0 | output += "Invalid: " + column.name; |
| 217 | } | |
| 218 | 6 | output += "</td>"; |
| 219 | }); | |
| 220 | 2 | output += "</tr>"; |
| 221 | }); | |
| 222 | ||
| 223 | 1 | return output + "</tbody>"; |
| 224 | ||
| 225 | }; | |
| 226 | ||
| 227 | /** | |
| 228 | * Render headers | |
| 229 | * @param table | |
| 230 | * @returns {String} | |
| 231 | */ | |
| 232 | 1 | CalipsoTable.prototype.render_pager = function(table, url) { |
| 233 | ||
| 234 | // Test | |
| 235 | 1 | var output = ""; |
| 236 | ||
| 237 | 1 | if (table.view && table.view.pager) { |
| 238 | 1 | output += pager.render(table.view.from, table.view.limit, table.view.total, url); |
| 239 | } | |
| 240 | ||
| 241 | 1 | return output; |
| 242 | ||
| 243 | }; |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * Calipso theme library | |
| 3 | * | |
| 4 | * Copyright(c) 2011 Clifton Cunningham <clifton.cunningham@gmail.com> | |
| 5 | * MIT Licensed | |
| 6 | * | |
| 7 | * This library provides all of the template loading, caching and rendering functions used by Calipso. | |
| 8 | * | |
| 9 | * The idea is that modules only ever provide generic, unstyled html (or json), where all layout and styling is completely | |
| 10 | * controlled by the theme. Themes should be able to be reloaded on configuration change, and the theme engine | |
| 11 | * will watch for changes to templates (based on config) to speed up development. | |
| 12 | * | |
| 13 | * Additionally, configuration of layouts has been extracted out into a theme configuration file, enabling control of | |
| 14 | * the 'wiring' to an extent. | |
| 15 | * | |
| 16 | * Decision was made to not use the default express view renderers as it didn't give enough control over caching templates, | |
| 17 | * Interacting with the view libraries directly, | |
| 18 | * | |
| 19 | * Borrowed liberally from Connect / ExpressJS itself for this, thanks for the great work! | |
| 20 | * | |
| 21 | */ | |
| 22 | ||
| 23 | /** | |
| 24 | * Includes | |
| 25 | */ | |
| 26 | ||
| 27 | 1 | var rootpath = process.cwd() + '/', |
| 28 | path = require('path'), | |
| 29 | calipso = require(path.join('..', 'calipso')), | |
| 30 | fs = require('fs'), | |
| 31 | utils = require('connect').utils, | |
| 32 | merge = utils.merge; | |
| 33 | ||
| 34 | /** | |
| 35 | * The theme object itself, instantiated within calipso | |
| 36 | */ | |
| 37 | 1 | module.exports.Theme = function(theme, next) { |
| 38 | ||
| 39 | // Defaults | |
| 40 | 2 | var themeName = theme.name; |
| 41 | 2 | var themePath = theme.path; |
| 42 | ||
| 43 | /** | |
| 44 | * Load a theme | |
| 45 | */ | |
| 46 | 2 | loadTheme(themeName, themePath, function(err, themeConfig) { |
| 47 | ||
| 48 | 2 | if (err) { |
| 49 | 0 | next(err); |
| 50 | 0 | return; |
| 51 | } | |
| 52 | ||
| 53 | 2 | cacheTheme(themeConfig, themePath, function(err, themeCache) { |
| 54 | ||
| 55 | 2 | if (err) { |
| 56 | 0 | next(err); |
| 57 | 0 | return; |
| 58 | } | |
| 59 | ||
| 60 | // Load the theme configuration file. | |
| 61 | 2 | var theme = { |
| 62 | theme: themeName, | |
| 63 | cache: themeCache, | |
| 64 | config: themeConfig, | |
| 65 | compileTemplate: function(data, templatePath, templateExtension) { | |
| 66 | // expose private function for module to use | |
| 67 | 2 | return compileTemplate(data, templatePath, templateExtension); |
| 68 | }, | |
| 69 | ||
| 70 | // Render a module | |
| 71 | // Changed in 0.1.1 to be asynch | |
| 72 | renderItem: function(req, res, template, block, options, next) { | |
| 73 | ||
| 74 | 1 | var output = ""; |
| 75 | ||
| 76 | 1 | if (template) { |
| 77 | ||
| 78 | 1 | var themeOptions = createOptions(req, res, options); |
| 79 | ||
| 80 | 1 | if (typeof template === 'function') { |
| 81 | 1 | try { |
| 82 | 1 | output = template.call({}, themeOptions); |
| 83 | } catch (ex) { | |
| 84 | 0 | output = "Block: " + block + " failed to render because " + ex.message + ex.stack; |
| 85 | } | |
| 86 | ||
| 87 | } else { | |
| 88 | // Assume template is processed HTML | |
| 89 | 0 | output = template; |
| 90 | } | |
| 91 | ||
| 92 | 1 | if (block) { |
| 93 | // Store the block and layout | |
| 94 | 1 | res.renderedBlocks.set(block, output, res.layout, res.params, next); |
| 95 | } else { | |
| 96 | // Just return back to the calling function | |
| 97 | 0 | next(null, output); |
| 98 | } | |
| 99 | ||
| 100 | } | |
| 101 | ||
| 102 | }, | |
| 103 | render: function(req, res, next) { | |
| 104 | ||
| 105 | 3 | var cache = this.cache, theme = this, layout = res.layout ? res.layout : "default", content, themeOptions, err; |
| 106 | ||
| 107 | 3 | calipso.silly("Using layout " + layout); |
| 108 | ||
| 109 | 3 | if (!theme.config.layouts[layout]) { |
| 110 | 0 | layout = "default"; |
| 111 | 0 | if (!theme.config.layouts[layout]) { |
| 112 | 0 | calipso.error("Default layout is not defined within the current theme, exiting."); |
| 113 | 0 | res.send(""); |
| 114 | 0 | return; |
| 115 | } | |
| 116 | } | |
| 117 | ||
| 118 | 3 | processTheme(req, res, layout, theme, function(err) { |
| 119 | ||
| 120 | // If something went wrong ... | |
| 121 | 3 | if (err) { |
| 122 | 0 | next(err, null); |
| 123 | 0 | return; |
| 124 | } | |
| 125 | ||
| 126 | // Now, process the layout template itself | |
| 127 | 3 | themeOptions = createOptions(req, res, res.bufferedOutput); |
| 128 | ||
| 129 | 3 | try { |
| 130 | 3 | content = theme.cache[layout].template.call({}, themeOptions); |
| 131 | } catch (ex) { | |
| 132 | 0 | err = ex; |
| 133 | } | |
| 134 | ||
| 135 | 3 | return next(err, content); |
| 136 | ||
| 137 | ||
| 138 | }); | |
| 139 | ||
| 140 | }, | |
| 141 | getLayoutsArray: function() { | |
| 142 | ||
| 143 | 0 | var theme = this; |
| 144 | 0 | var layouts = []; |
| 145 | 0 | for (var layout in theme.config.layouts) { |
| 146 | 0 | layouts.push(layout); |
| 147 | } | |
| 148 | 0 | return layouts; |
| 149 | ||
| 150 | } | |
| 151 | ||
| 152 | }; | |
| 153 | ||
| 154 | 2 | next(null, theme); |
| 155 | ||
| 156 | }); | |
| 157 | ||
| 158 | ||
| 159 | }); | |
| 160 | ||
| 161 | }; | |
| 162 | ||
| 163 | /** | |
| 164 | *Process a theme section | |
| 165 | */ | |
| 166 | ||
| 167 | 1 | function processSection(req, res, section, sectionPath, layoutConfig, theme, next) { |
| 168 | ||
| 169 | 18 | var themeOptions, sectionCache = theme.cache[sectionPath]; |
| 170 | ||
| 171 | // Check the theme cache | |
| 172 | 18 | if (!sectionCache) { |
| 173 | 0 | calipso.error("Unable to find template for " + sectionPath); |
| 174 | 0 | next(); |
| 175 | 0 | return; |
| 176 | } | |
| 177 | ||
| 178 | 18 | var blockData = ""; |
| 179 | ||
| 180 | 18 | if (!sectionCache.template) { |
| 181 | // Use the default | |
| 182 | 0 | sectionPath = "default." + section; |
| 183 | 0 | sectionCache = theme.cache[sectionPath]; |
| 184 | } | |
| 185 | ||
| 186 | // should there be more than just these two error codes? | |
| 187 | // if more than just these two, then this would have to happen later on: | |
| 188 | // templates.push({name:"500", templatePath:"templates/500.html"}); | |
| 189 | // Override with a 404 (not found) page | |
| 190 | 18 | if (section === "body" && res.statusCode === 404) { |
| 191 | 1 | if (!theme.cache.hasOwnProperty("404")) { |
| 192 | 0 | localNext(new Error("You must define a 404 template in the error folder e.g. error/404.html")); |
| 193 | 0 | return; |
| 194 | } | |
| 195 | 1 | sectionCache = theme.cache["404"]; |
| 196 | } | |
| 197 | ||
| 198 | // Override with a 403 (no permissions) page | |
| 199 | 18 | if(section === "body" && res.statusCode === 403) { |
| 200 | 0 | if(!theme.cache.hasOwnProperty("403")) { |
| 201 | 0 | localNext(new Error("You must define a 403 template in the error folder e.g. error/403.html")); |
| 202 | 0 | return; |
| 203 | } | |
| 204 | 0 | sectionCache = theme.cache["403"]; |
| 205 | } | |
| 206 | ||
| 207 | // Override with a 500 (error) page | |
| 208 | 18 | if (section === "body" && res.statusCode === 500) { |
| 209 | 0 | if (!theme.cache.hasOwnProperty("500")) { |
| 210 | 0 | localNext(new Error("You must define a 500 template in the error folder e.g. error/500.html")); |
| 211 | 0 | return; |
| 212 | } | |
| 213 | 0 | sectionCache = theme.cache["500"]; |
| 214 | 0 | blockData = res.errorMessage ? res.errorMessage : ""; |
| 215 | } | |
| 216 | ||
| 217 | // Retrieve any backing function | |
| 218 | 18 | var sectionCacheFn = sectionCache.fn; |
| 219 | ||
| 220 | // Clear any buffered output for this section | |
| 221 | 18 | res.bufferedOutput[section] = ""; |
| 222 | ||
| 223 | // Get the basic theme options | |
| 224 | 18 | themeOptions = createOptions(req, res, { |
| 225 | blockData: blockData | |
| 226 | }); | |
| 227 | ||
| 228 | // Add any custom functions | |
| 229 | 18 | if (typeof sectionCacheFn === "function") { |
| 230 | ||
| 231 | 8 | sectionCacheFn(req, themeOptions, function(err, fnOptions) { |
| 232 | ||
| 233 | 8 | if (err) { |
| 234 | 0 | err.xMessage = "Issue executing the theme function for section " + section + ", check " + sectionPath.replace(".", "/") + ".js"; |
| 235 | 0 | localNext(err); |
| 236 | 0 | return; |
| 237 | } | |
| 238 | ||
| 239 | 8 | themeOptions = merge(themeOptions, fnOptions); |
| 240 | 8 | try { |
| 241 | 8 | res.bufferedOutput[section] += sectionCache.template.call({}, themeOptions); |
| 242 | 8 | localNext(); |
| 243 | } catch (ex) { | |
| 244 | // Augment the exception | |
| 245 | 0 | ex.xMessage = "Issue processing theme section " + section + ", path: " + sectionPath; |
| 246 | 0 | localNext(ex); |
| 247 | } | |
| 248 | ||
| 249 | }); | |
| 250 | ||
| 251 | } else { | |
| 252 | 10 | try { |
| 253 | 10 | res.bufferedOutput[section] += sectionCache.template.call({}, themeOptions); |
| 254 | 10 | localNext(); |
| 255 | } catch (ex) { | |
| 256 | 0 | ex.xMessage = "Issue processing theme section: " + section + ", theme: " + sectionPath; |
| 257 | 0 | localNext(ex); |
| 258 | } | |
| 259 | ||
| 260 | } | |
| 261 | ||
| 262 | // Local next function to enable proxying of callback | |
| 263 | ||
| 264 | 18 | function localNext(err) { |
| 265 | 18 | next(err); |
| 266 | } | |
| 267 | ||
| 268 | ||
| 269 | } | |
| 270 | ||
| 271 | /** | |
| 272 | * Copy the current block data over to options to render | |
| 273 | * @param res | |
| 274 | * @param config | |
| 275 | */ | |
| 276 | ||
| 277 | 1 | function processTheme(req, res, layout, theme, next) { |
| 278 | ||
| 279 | 3 | var layoutConfig, copyConfig, copySection, sectionExists, disable, sections = [], |
| 280 | section; | |
| 281 | ||
| 282 | 3 | delete res.bufferedOutput; |
| 283 | 3 | res.bufferedOutput = {}; |
| 284 | ||
| 285 | // Scan through each layout | |
| 286 | 3 | try { |
| 287 | 3 | layoutConfig = theme.config.layouts[layout].layout; |
| 288 | } catch (ex) { | |
| 289 | 0 | next(ex.message); |
| 290 | 0 | return; |
| 291 | } | |
| 292 | ||
| 293 | // Check to see if this layout copies default | |
| 294 | 3 | if (layoutConfig.copyFrom && layout != "default") { |
| 295 | ||
| 296 | 1 | copyConfig = theme.config.layouts[layoutConfig.copyFrom].layout; |
| 297 | 1 | layoutConfig.sections = layoutConfig.sections || {}; |
| 298 | ||
| 299 | // Copy over any missing sections from default | |
| 300 | 1 | for (copySection in copyConfig.sections) { |
| 301 | ||
| 302 | 6 | sectionExists = layoutConfig.sections && layoutConfig.sections[copySection]; |
| 303 | 6 | disable = layoutConfig.sections && layoutConfig.sections[copySection] && layoutConfig.sections[copySection].disable; |
| 304 | 6 | if (!sectionExists && !disable) { |
| 305 | 6 | layoutConfig.sections[copySection] = copyConfig.sections[copySection]; |
| 306 | 6 | layoutConfig.sections[copySection].layout = "default"; // Flag override as below |
| 307 | } | |
| 308 | ||
| 309 | } | |
| 310 | ||
| 311 | } | |
| 312 | ||
| 313 | // Create a section array | |
| 314 | 3 | for (section in layoutConfig.sections) { |
| 315 | 18 | disable = layoutConfig.sections[section].disable; |
| 316 | 18 | if (!disable) { |
| 317 | 18 | sections.push(section); |
| 318 | } | |
| 319 | } | |
| 320 | 3 | var totalCount = sections.length; |
| 321 | 3 | var totalDone = 0; |
| 322 | ||
| 323 | // Now, process all the sections | |
| 324 | // This is done via a localNext to give us full control | |
| 325 | // and better ability to debug | |
| 326 | ||
| 327 | 3 | function localNext(err) { |
| 328 | 18 | totalDone += 1; |
| 329 | ||
| 330 | 18 | if (totalDone == totalCount) { |
| 331 | 3 | next(); |
| 332 | } | |
| 333 | ||
| 334 | } | |
| 335 | ||
| 336 | 3 | for (section in layoutConfig.sections) { |
| 337 | ||
| 338 | // Check to see if we are overriding | |
| 339 | 18 | var currentSection = section; |
| 340 | 18 | var layoutOverride = layoutConfig.sections[section].layout; |
| 341 | 18 | var sectionPath = layoutOverride ? layoutOverride + "." + section : layout + "." + section; |
| 342 | 18 | var cache = layoutConfig.sections[section].cache; |
| 343 | 18 | var params = layoutConfig.sections[section].varyParams; |
| 344 | 18 | var cacheEnabled = calipso.config.get('performance:cache:enabled'); |
| 345 | 18 | var isAdmin = req.session.user && req.session.user.isAdmin; |
| 346 | ||
| 347 | 18 | disable = layoutConfig.sections[section].disable; |
| 348 | ||
| 349 | // Sections are cacheable | |
| 350 | 18 | if (!disable) { |
| 351 | 18 | if (cache && cacheEnabled && !isAdmin) { |
| 352 | 0 | var keys = [layout, 'section', currentSection]; |
| 353 | 0 | var cacheKey = calipso.cacheService.getCacheKey(keys, params); |
| 354 | 0 | sectionCache(req, res, cacheKey, section, sectionPath, layoutConfig, theme, localNext); |
| 355 | } else { | |
| 356 | 18 | processSection(req, res, section, sectionPath, layoutConfig, theme, localNext); |
| 357 | } | |
| 358 | } | |
| 359 | ||
| 360 | } | |
| 361 | ||
| 362 | } | |
| 363 | ||
| 364 | /** | |
| 365 | * Interact with sections via the cache | |
| 366 | */ | |
| 367 | ||
| 368 | 1 | function sectionCache(req, res, cacheKey, section, templateName, layoutConfig, theme, next) { |
| 369 | ||
| 370 | 0 | calipso.cacheService.check(cacheKey, function(err, isCached) { |
| 371 | 0 | if (isCached) { |
| 372 | 0 | calipso.silly("Cache hit for " + cacheKey + ", section " + section); |
| 373 | 0 | calipso.cacheService.get(cacheKey, function(err, cache) { |
| 374 | 0 | if (!err) { |
| 375 | 0 | res.bufferedOutput[section] = cache.content; |
| 376 | } | |
| 377 | 0 | next(err); |
| 378 | }); | |
| 379 | } else { | |
| 380 | 0 | calipso.silly("Cache miss for " + cacheKey + ", section " + section); |
| 381 | 0 | processSection(req, res, section, templateName, layoutConfig, theme, function(err) { |
| 382 | 0 | if (!err) { |
| 383 | 0 | var content = res.bufferedOutput[section]; |
| 384 | 0 | calipso.cacheService.set(cacheKey, { |
| 385 | content: content | |
| 386 | }, null, next); | |
| 387 | } else { | |
| 388 | 0 | next(err); |
| 389 | } | |
| 390 | }); | |
| 391 | } | |
| 392 | }); | |
| 393 | } | |
| 394 | ||
| 395 | ||
| 396 | /** | |
| 397 | * Load a theme | |
| 398 | */ | |
| 399 | ||
| 400 | 1 | function loadTheme(theme, themePath, next) { |
| 401 | ||
| 402 | 2 | var themeFile = calipso.lib.path.join(themePath, "theme.json"); |
| 403 | ||
| 404 | 2 | (fs.exists || path.exists)(themeFile, function(exists) { |
| 405 | 2 | if(exists) { |
| 406 | 2 | fs.readFile(themeFile, 'utf8', function(err, data) { |
| 407 | 2 | if (!err) { |
| 408 | 2 | var jsonData; |
| 409 | 2 | try { |
| 410 | 2 | jsonData = JSON.parse(data); |
| 411 | 2 | next(null, jsonData); |
| 412 | } catch (ex) { | |
| 413 | 0 | next(new Error("Error parsing theme configuration: " + ex.message + " stack, " + ex.stack)); |
| 414 | } | |
| 415 | } else { | |
| 416 | 0 | next(err); |
| 417 | } | |
| 418 | }); | |
| 419 | } else { | |
| 420 | 0 | next(new Error("Can't find specified theme configuration " + themeFile)); |
| 421 | } | |
| 422 | }); | |
| 423 | } | |
| 424 | ||
| 425 | /** | |
| 426 | * Load all of the theme templates into the theme | |
| 427 | * @param theme | |
| 428 | */ | |
| 429 | ||
| 430 | 1 | function cacheTheme(themeConfig, themePath, next) { |
| 431 | ||
| 432 | 2 | var templates = [], |
| 433 | templateCache = {}, | |
| 434 | layout, layoutConfig, section, template, module, templateFiles, errorCodeTemplates; | |
| 435 | ||
| 436 | // Scan through each layout | |
| 437 | 2 | if (themeConfig) { |
| 438 | ||
| 439 | 2 | for (layout in themeConfig.layouts) { |
| 440 | ||
| 441 | // Scan through each layout | |
| 442 | 6 | layoutConfig = themeConfig.layouts[layout].layout; |
| 443 | ||
| 444 | // Add the layout template | |
| 445 | 6 | templates.push({ |
| 446 | name: layout, | |
| 447 | templatePath: calipso.lib.path.join("templates", layoutConfig.template) | |
| 448 | }); | |
| 449 | ||
| 450 | ||
| 451 | // Add the templates | |
| 452 | 6 | for (section in layoutConfig.sections) { |
| 453 | 14 | template = layoutConfig.sections[section].template; |
| 454 | 14 | if (template) { |
| 455 | 14 | templates.push({ |
| 456 | name: layout + "." + section, | |
| 457 | templatePath: calipso.lib.path.join("templates", layout, template) | |
| 458 | }); | |
| 459 | } | |
| 460 | } | |
| 461 | ||
| 462 | // Check to see if the theme overrides any module templates | |
| 463 | 6 | if (layoutConfig.modules) { |
| 464 | 0 | for (module in layoutConfig.modules) { |
| 465 | 0 | for (template in layoutConfig.modules[module]) { |
| 466 | 0 | loadModuleOverrideTemplate(templateCache, module, template, path.join(themePath, layoutConfig.modules[module][template])); |
| 467 | } | |
| 468 | } | |
| 469 | } | |
| 470 | } | |
| 471 | ||
| 472 | // Push error message templates | |
| 473 | 2 | templateFiles = calipso.lib.fs.readdirSync(calipso.lib.path.join(themePath, 'templates', 'error')); |
| 474 | 2 | errorCodeTemplates = calipso.lib._.select(templateFiles, function(filename) { |
| 475 | // Select files that start with 3 digits, indicating an error code | |
| 476 | 4 | return filename.match(/^\d{3}./); |
| 477 | }); | |
| 478 | ||
| 479 | 2 | calipso.lib._.each(errorCodeTemplates, function(filename) { |
| 480 | 4 | templates.push({ |
| 481 | name: filename.match(/^\d{3}/)[0], | |
| 482 | templatePath: calipso.lib.path.join("templates", "error", filename) | |
| 483 | }); | |
| 484 | }); | |
| 485 | ||
| 486 | 2 | var templateIterator = function(templateName, cb) { |
| 487 | 24 | loadTemplate(templateCache, templateName, themePath, cb); |
| 488 | }; | |
| 489 | ||
| 490 | 2 | calipso.lib.async.map(templates, templateIterator, function(err, result) { |
| 491 | 2 | if (err) { |
| 492 | // May not be a problem as missing templates default to default | |
| 493 | 0 | calipso.error("Error loading templates, msg: " + err.message + ", stack: " + err.stack); |
| 494 | 0 | next(err); |
| 495 | } else { | |
| 496 | 2 | next(null, templateCache); |
| 497 | } | |
| 498 | }); | |
| 499 | ||
| 500 | } | |
| 501 | ||
| 502 | } | |
| 503 | ||
| 504 | /** | |
| 505 | * Load a template that overrides a module template | |
| 506 | * fired from cacheTheme(), | |
| 507 | */ | |
| 508 | ||
| 509 | 1 | function loadModuleOverrideTemplate(templateCache, module, template, path) { |
| 510 | ||
| 511 | 0 | var templatePath = path, |
| 512 | templateExtension = templatePath.match(/([^\.]+)$/)[0], | |
| 513 | templateFn = fs.readFileSync(templatePath, 'utf8'), | |
| 514 | templateFnCompiled = compileTemplate(templateFn, templatePath, templateExtension); | |
| 515 | ||
| 516 | // Initialise the objects | |
| 517 | 0 | templateCache.modules = templateCache.modules || {}; |
| 518 | 0 | templateCache.modules[module] = templateCache.modules[module] || {}; |
| 519 | 0 | templateCache.modules[module].templates = templateCache.modules[module].templates || {}; |
| 520 | ||
| 521 | // allow hook for listening for module events? | |
| 522 | // Load the function | |
| 523 | 0 | templateCache.modules[module].templates[template] = templateFnCompiled; |
| 524 | ||
| 525 | } | |
| 526 | ||
| 527 | /** | |
| 528 | * Load a template | |
| 529 | */ | |
| 530 | ||
| 531 | 1 | function loadTemplate(templateCache, template, themePath, next) { |
| 532 | ||
| 533 | // Reset / default | |
| 534 | 48 | if (!templateCache[template.name]) templateCache[template.name] = {}; |
| 535 | ||
| 536 | // Template paths and functions | |
| 537 | 24 | var templatePath = calipso.lib.path.join(themePath, template.templatePath), |
| 538 | templateExtension = template.templatePath.match(/([^\.]+)$/)[0], | |
| 539 | templateFnPath = calipso.lib.path.join(themePath, template.templatePath.replace("." + templateExtension, ".js")); | |
| 540 | ||
| 541 | 24 | (fs.exists || path.exists)(templatePath,function(exists) { |
| 542 | ||
| 543 | 24 | if (exists) { |
| 544 | ||
| 545 | 24 | var templateData = ''; |
| 546 | ||
| 547 | 24 | try { |
| 548 | 24 | templateData = fs.readFileSync(templatePath, 'utf8'); |
| 549 | } catch (err) { | |
| 550 | 0 | calipso.error('Failed to open template ' + templatePath + ' ...'); |
| 551 | } | |
| 552 | ||
| 553 | 24 | if (calipso.config.get('performance:watchFiles')) { |
| 554 | ||
| 555 | 24 | try { |
| 556 | 24 | fs.unwatchFile(templatePath); |
| 557 | 24 | fs.watchFile(templatePath, { |
| 558 | persistent: true, | |
| 559 | interval: 200 | |
| 560 | }, function(curr, prev) { | |
| 561 | 0 | loadTemplate(templateCache, template, themePath, function() { |
| 562 | 0 | calipso.silly("Template " + templatePath + " reloaded ..."); |
| 563 | }); | |
| 564 | }); | |
| 565 | } catch (ex) { | |
| 566 | 0 | calipso.error('Failed to watch template ' + templatePath + ' ...'); |
| 567 | } | |
| 568 | ||
| 569 | } | |
| 570 | ||
| 571 | // Precompile the view into our cache | |
| 572 | 24 | templateCache[template.name].template = compileTemplate(templateData, templatePath, templateExtension); |
| 573 | ||
| 574 | // See if we have a template fn | |
| 575 | 24 | if ((fs.existsSync || path.existsSync)(templateFnPath)) { |
| 576 | ||
| 577 | 8 | if (exists) { |
| 578 | 8 | try { |
| 579 | 8 | templateCache[template.name].fn = require(templateFnPath); |
| 580 | } catch (ex) { | |
| 581 | 0 | calipso.error(ex); |
| 582 | } | |
| 583 | ||
| 584 | } | |
| 585 | ||
| 586 | } | |
| 587 | ||
| 588 | 24 | return next(null, template); |
| 589 | ||
| 590 | } else { | |
| 591 | ||
| 592 | 0 | next(new Error('Path does not exist: ' + templatePath)); |
| 593 | ||
| 594 | } | |
| 595 | ||
| 596 | }); | |
| 597 | ||
| 598 | } | |
| 599 | ||
| 600 | /** | |
| 601 | * Pre-compile a template based on its extension. | |
| 602 | * If the required view engine does not exist, exit gracefully and let | |
| 603 | * them know that this is the case. | |
| 604 | */ | |
| 605 | ||
| 606 | 1 | function compileTemplate(template, templatePath, templateExtension) { |
| 607 | ||
| 608 | 26 | var compiledTemplate = function() {}, |
| 609 | templateEngine; | |
| 610 | 26 | var options = { |
| 611 | filename: templatePath | |
| 612 | }; | |
| 613 | ||
| 614 | // If we get html, replace with ejs | |
| 615 | 52 | if (templateExtension === "html") templateExtension = "ejs"; |
| 616 | ||
| 617 | // Load a template engine based on the extension | |
| 618 | 26 | try { |
| 619 | 26 | templateEngine = require(templateExtension); |
| 620 | } catch (ex) { | |
| 621 | 0 | calipso.warn("No view rendering engine exists that matches: " + templateExtension + ", so using EJS!"); |
| 622 | 0 | templateEngine = require("ejs"); |
| 623 | } | |
| 624 | ||
| 625 | // Return our compiled template | |
| 626 | 26 | try { |
| 627 | 26 | compiledTemplate = templateEngine.compile(template, options); |
| 628 | } catch (ex) { | |
| 629 | 0 | calipso.error("Error compiling template : " + templatePath + ", message: " + ex.message); |
| 630 | } | |
| 631 | ||
| 632 | 26 | return compiledTemplate; |
| 633 | ||
| 634 | } | |
| 635 | ||
| 636 | /** | |
| 637 | * Merge options together | |
| 638 | */ | |
| 639 | ||
| 640 | 1 | function createOptions(req, res, options) { |
| 641 | ||
| 642 | // Merge options with helpers | |
| 643 | 22 | options = merge(options, req.helpers); |
| 644 | ||
| 645 | // Merge options with application data | |
| 646 | 22 | if (calipso.data) { |
| 647 | 22 | options = merge(options, calipso.data); |
| 648 | } | |
| 649 | ||
| 650 | 22 | return options; |
| 651 | ||
| 652 | } |
| Line | Hits | Source |
|---|---|---|
| 1 | /** | |
| 2 | * General utility methods | |
| 3 | */ | |
| 4 | 1 | var _ = require('underscore'); |
| 5 | ||
| 6 | 1 | module.exports = { |
| 7 | /** | |
| 8 | * Basically like getProperty, different return | |
| 9 | * @method hasProperty | |
| 10 | * @param ns {string} A period delimited string of the namespace to find, sans root object | |
| 11 | * @param obj {object} The root object to search | |
| 12 | * @return {boolean} true if property exists, false otherwise | |
| 13 | */ | |
| 14 | hasProperty: function(ns, obj) { | |
| 15 | 2 | if (!ns) { |
| 16 | 0 | return obj; |
| 17 | } | |
| 18 | 2 | var nsArray = ns.split('.'), |
| 19 | nsLen = nsArray.length, | |
| 20 | newNs; | |
| 21 | ||
| 22 | // if nsLen === 0, then obj is just returned | |
| 23 | 2 | while (nsLen > 0) { |
| 24 | 6 | newNs = nsArray.shift(); |
| 25 | 6 | if (obj[newNs]) { |
| 26 | 4 | obj = obj[newNs]; |
| 27 | } else { | |
| 28 | 2 | return false; |
| 29 | } | |
| 30 | 4 | nsLen = nsArray.length; |
| 31 | } | |
| 32 | 0 | return true; |
| 33 | }, | |
| 34 | /** | |
| 35 | * Find a namespaced property | |
| 36 | * @method getProperty | |
| 37 | * @param ns {string} A period delimited string of the namespace to find, sans root object | |
| 38 | * @param obj {object} The root object to search | |
| 39 | * @return {object} the object, either the namespaced obejct or the root object | |
| 40 | */ | |
| 41 | getProperty: function(ns, obj) { | |
| 42 | 0 | if (!ns) { |
| 43 | 0 | return obj; |
| 44 | } | |
| 45 | 0 | var nsArray = ns.split('.'), |
| 46 | nsLen = nsArray.length, | |
| 47 | newNs; | |
| 48 | ||
| 49 | // if nsLen === 0, then obj is just returned | |
| 50 | 0 | while (nsLen > 0) { |
| 51 | 0 | newNs = nsArray.shift(); |
| 52 | 0 | if (obj[newNs]) { |
| 53 | 0 | obj = obj[newNs]; |
| 54 | } | |
| 55 | 0 | nsLen = nsArray.length; |
| 56 | } | |
| 57 | 0 | return obj; |
| 58 | }, | |
| 59 | ||
| 60 | /** | |
| 61 | * Simple mongo object copier, used to do a shallow copy of objects | |
| 62 | */ | |
| 63 | copyMongoObject: function(object, copy, schema) { | |
| 64 | ||
| 65 | 0 | var fields = _.keys(schema.paths); |
| 66 | 0 | _.each(fields, function(key) { |
| 67 | 0 | if (key !== '_id') copy.set(key, object.get(key)); |
| 68 | }); | |
| 69 | ||
| 70 | }, | |
| 71 | escapeHtmlQuotes: function (string) { | |
| 72 | 0 | if (string && string.replace) { |
| 73 | 0 | return string.replace(/\"/g, '"').replace(/\'/g, '''); |
| 74 | } | |
| 75 | else { | |
| 76 | 0 | return string; |
| 77 | } | |
| 78 | } | |
| 79 | }; |
| Line | Hits | Source |
|---|---|---|
| 1 | /** | |
| 2 | * This library provides a wrapper to enable modules to load javascript and styles into an | |
| 3 | * array, that can then be rendered into a theme in the appropriate location. | |
| 4 | * | |
| 5 | * Styles and JS are all indexed by key, so you could write functions that over-wrote them in the theme as the | |
| 6 | * last update will always stick. | |
| 7 | * | |
| 8 | */ | |
| 9 | ||
| 10 | 1 | var rootpath = process.cwd() + '/', |
| 11 | path = require('path'), | |
| 12 | calipso = require(path.join('..', 'calipso')), | |
| 13 | fs = require('fs'); | |
| 14 | ||
| 15 | /** | |
| 16 | * Client Object - handle CSS and JS loading for modules out to themes | |
| 17 | */ | |
| 18 | 1 | var Client = module.exports = function Client(options) { |
| 19 | ||
| 20 | 14 | this.options = options || { |
| 21 | 'minified-script': 'media/calipso-main' | |
| 22 | }; | |
| 23 | ||
| 24 | 14 | this.scripts = []; |
| 25 | 14 | this.styles = []; |
| 26 | ||
| 27 | // Shortcuts to core, must be included somewhere (module or theme) to be rendered | |
| 28 | 14 | this.coreScripts = { |
| 29 | 'jquery': {key:'jquery', url:'jquery-1.8.3.min.js', weight: -100}, | |
| 30 | 'calipso': {key:'calipso', url:'calipso.js', weight: -50} | |
| 31 | } | |
| 32 | ||
| 33 | }; | |
| 34 | ||
| 35 | 1 | Client.prototype.addScript = function(options) { |
| 36 | ||
| 37 | 8 | var self = this; |
| 38 | ||
| 39 | // Convert our options over with flexible defaults | |
| 40 | 8 | if (typeof options === "string") { |
| 41 | 2 | if (this.coreScripts[options]) { |
| 42 | 1 | options = this.coreScripts[options]; |
| 43 | } else { | |
| 44 | 1 | options = { |
| 45 | name: options, | |
| 46 | url: options, | |
| 47 | weight: 0 | |
| 48 | }; | |
| 49 | } | |
| 50 | } | |
| 51 | 12 | if (!options.name) options.name = options.url; |
| 52 | ||
| 53 | // Add the script | |
| 54 | 8 | self._add('scripts', options.name, options); |
| 55 | ||
| 56 | }; | |
| 57 | ||
| 58 | /** | |
| 59 | * Create simple list of all client JS | |
| 60 | */ | |
| 61 | 1 | Client.prototype.listScripts = function(next) { |
| 62 | ||
| 63 | // TODO - this should be updated to use LABjs by default (?) | |
| 64 | 1 | var self = this; |
| 65 | 1 | var output = "<!-- Calipso Module Scripts -->"; |
| 66 | 1 | self.scripts.forEach(function(value) { |
| 67 | 2 | output += '\r\n<script title="' + value.name + '" src="' + value.url + '"></script>'; |
| 68 | }); | |
| 69 | 1 | output += "<!-- End of Calipso Module Scripts -->"; |
| 70 | 1 | next(null, output); |
| 71 | ||
| 72 | }; | |
| 73 | ||
| 74 | 1 | Client.prototype.addStyle = function(options) { |
| 75 | ||
| 76 | 5 | var self = this; |
| 77 | ||
| 78 | // Convert our options over with flexible defaults | |
| 79 | 5 | if (typeof options === "string") { |
| 80 | 1 | options = { |
| 81 | name: options, | |
| 82 | url: options, | |
| 83 | weight: 0 | |
| 84 | }; | |
| 85 | } | |
| 86 | 8 | if (!options.name) options.name = options.url; |
| 87 | ||
| 88 | // Add the script | |
| 89 | 5 | self._add('styles', options.name, options); |
| 90 | ||
| 91 | }; | |
| 92 | ||
| 93 | /** | |
| 94 | * Compile together all of the client side scripts | |
| 95 | */ | |
| 96 | 1 | Client.prototype.listStyles = function(next) { |
| 97 | ||
| 98 | // TODO - this should be updated to use LABjs by default (?) | |
| 99 | 1 | var self = this; |
| 100 | 1 | var output = "<!-- Calipso Module Styles -->"; |
| 101 | ||
| 102 | 1 | self.styles.forEach(function(value) { |
| 103 | 2 | output += '\r\n<link rel="stylesheet" title="' + value.name + '" href="' + value.url + '"/>'; |
| 104 | }); | |
| 105 | 1 | output += "<!-- End of Calipso Module Styles -->"; |
| 106 | 1 | next(null, output); |
| 107 | ||
| 108 | }; | |
| 109 | ||
| 110 | ||
| 111 | /** | |
| 112 | * Helper to add unique elements to an array | |
| 113 | */ | |
| 114 | 1 | Client.prototype._add = function(arrName, name, options) { |
| 115 | ||
| 116 | 13 | var self = this; |
| 117 | 13 | self[arrName] = self[arrName] || []; |
| 118 | ||
| 119 | // Find first match | |
| 120 | 13 | var found = calipso.lib._.find(self[arrName], function(value) { |
| 121 | 3 | return (value.name && value.name === name) ? true : false; |
| 122 | }); | |
| 123 | ||
| 124 | 13 | if (found) { |
| 125 | // Replace - this means we never get duplicates (e.g. of JQuery, JQueryUI) | |
| 126 | 1 | self[arrName].splice(found, 1, options); |
| 127 | } else { | |
| 128 | // Push | |
| 129 | 12 | self[arrName].push(options); |
| 130 | } | |
| 131 | ||
| 132 | // Sort - TODO, this can probably be more efficient by placing the new item smarter | |
| 133 | 13 | self[arrName].sort(function(a, b) { |
| 134 | 2 | return a.weight > b.weight; |
| 135 | }); | |
| 136 | ||
| 137 | }; | |
| 138 | ||
| 139 | ||
| 140 | /** | |
| 141 | * Compile together all of the client side scripts | |
| 142 | * TODO - this is currently not used, needs to be worked on and thought through. | |
| 143 | * | |
| 144 | Client.prototype.compile = function(next) { | |
| 145 | ||
| 146 | var self = this; | |
| 147 | ||
| 148 | try { | |
| 149 | ||
| 150 | var scriptFile = path.join(rootpath,self.options.script), | |
| 151 | scriptStream = fs.createWriteStream(scriptFile, {'flags': 'a'}); | |
| 152 | ||
| 153 | } catch(ex) { | |
| 154 | ||
| 155 | console.dir(ex); | |
| 156 | ||
| 157 | } | |
| 158 | ||
| 159 | var grabFile = function(item, callback) { | |
| 160 | ||
| 161 | // TODO - allow referential | |
| 162 | var filePath = path.join(rootpath, item.url); | |
| 163 | ||
| 164 | // Check to see if the file has changed | |
| 165 | var stat = fs.lstatSync(filePath); | |
| 166 | ||
| 167 | fs.readFile(filePath, 'utf8', function(err, contents) { | |
| 168 | ||
| 169 | if(err) { | |
| 170 | ||
| 171 | return callback(new Error("Unable to locate file for ClientJS creation: " + filePath)); | |
| 172 | ||
| 173 | } else { | |
| 174 | ||
| 175 | var drain; | |
| 176 | drain = scriptStream.write(contents); | |
| 177 | callback(null, stat.mtime); | |
| 178 | ||
| 179 | } | |
| 180 | }); | |
| 181 | ||
| 182 | } | |
| 183 | ||
| 184 | // Callback wrapper to close the streams | |
| 185 | var done = function(err, data) { | |
| 186 | scriptStream.end(); | |
| 187 | next(err, data); | |
| 188 | } | |
| 189 | ||
| 190 | // var contents = fs.readFileSync(config.out, 'utf8'); | |
| 191 | calipso.lib.async.mapSeries(self.scripts, grabFile, function(err, scripts) { | |
| 192 | ||
| 193 | if(err) return done(err); | |
| 194 | ||
| 195 | var reduce = function(context, memo, value, index, list) { | |
| 196 | return (value > memo) ? value : memo; | |
| 197 | }; | |
| 198 | ||
| 199 | var maxmtime = calipso.lib._.reduce(scripts, reduce); | |
| 200 | ||
| 201 | console.dir(maxmtime); | |
| 202 | ||
| 203 | var script = '<!-- ' + maxmtime + ' -->' | |
| 204 | ||
| 205 | done(null, script); | |
| 206 | ||
| 207 | }) | |
| 208 | ||
| 209 | } | |
| 210 | **/ |
| Line | Hits | Source |
|---|---|---|
| 1 | /** | |
| 2 | * Setup the bare minimum required for a fully functioning 'calipso' object | |
| 3 | */ | |
| 4 | 1 | var jsc = require('jscoverage'), |
| 5 | require = jsc.require(module), // rewrite require function | |
| 6 | calipso = require('./require', true)('calipso'), | |
| 7 | path = require('path'), | |
| 8 | fs = require('fs'), | |
| 9 | colors = require('colors'), | |
| 10 | rootpath = process.cwd() + '/', | |
| 11 | Config = require('./require', true)('core/Configuration'), | |
| 12 | http = require('http'), | |
| 13 | mochaConfig = path.join(rootpath,'tmp','mocha.json'); | |
| 14 | ||
| 15 | // Create the tmp folder if it doesnt' exist | |
| 16 | 3 | try { fs.mkdirSync(path.join(rootpath,'tmp')) } catch(ex) {}; |
| 17 | ||
| 18 | /** | |
| 19 | * Mock application object | |
| 20 | */ | |
| 21 | 1 | function MockApp(next) { |
| 22 | ||
| 23 | 1 | var self = this; |
| 24 | ||
| 25 | // Configuration - always start with default | |
| 26 | 1 | var defaultConfig = path.join(rootpath, 'test', 'helpers', 'defaultConfig.json'); |
| 27 | ||
| 28 | 1 | var statusMsg = '\r\nBase path: '.grey + rootpath.cyan + '\r\nUsing config: '.grey + defaultConfig.cyan + '\r\nIn environment: '.grey + (process.env.NODE_ENV || 'development').cyan; |
| 29 | 1 | if(!process.env.CALIPSO_COV) console.log(statusMsg); |
| 30 | ||
| 31 | // Always delete any left over config | |
| 32 | 2 | try { fs.unlinkSync(mochaConfig); } catch(ex) { /** ignore **/ } |
| 33 | ||
| 34 | // Create new | |
| 35 | 1 | self.config = new Config({ |
| 36 | 'env': 'mocha', | |
| 37 | 'path': path.join(rootpath, 'tmp'), | |
| 38 | 'defaultConfig': defaultConfig | |
| 39 | }); | |
| 40 | ||
| 41 | // Middleware helpers | |
| 42 | 1 | self.mwHelpers = { |
| 43 | 0 | staticMiddleware: function() { return {} }, |
| 44 | 0 | stylusMiddleware: function() { return {} } |
| 45 | } | |
| 46 | ||
| 47 | // Pseudo stack - only middleware that is later overloaded | |
| 48 | 1 | self.stack = [{ |
| 49 | handle: { | |
| 50 | name: 'sessionDefault', | |
| 51 | tag: 'session' | |
| 52 | } | |
| 53 | }, { | |
| 54 | handle: { | |
| 55 | name: 'static', | |
| 56 | tag: 'theme.static' | |
| 57 | } | |
| 58 | }, { | |
| 59 | handle: { | |
| 60 | name: 'stylus', | |
| 61 | tag: 'theme.stylus' | |
| 62 | } | |
| 63 | }]; | |
| 64 | ||
| 65 | // Initialise and return | |
| 66 | 1 | self.config.init(function (err) { |
| 67 | ||
| 68 | 1 | if(err) console.log('Config error: '.grey + err.message.red); |
| 69 | 1 | if(!process.env.CALIPSO_COV) console.log('Config loaded: '.grey + self.config.file.cyan); |
| 70 | 1 | next(self); |
| 71 | ||
| 72 | }) | |
| 73 | ||
| 74 | } | |
| 75 | ||
| 76 | /** | |
| 77 | * Test permissions | |
| 78 | */ | |
| 79 | 1 | calipso.permission.Helper.addPermission("test:permission", "Simple permission for testing purposes."); |
| 80 | 1 | calipso.permission.Helper.addPermissionRole("test:permission", "Test"); |
| 81 | ||
| 82 | /** | |
| 83 | * Setup logging | |
| 84 | */ | |
| 85 | 1 | var loggingConfig = { |
| 86 | "console": { | |
| 87 | "enabled": false, | |
| 88 | "level": "error", | |
| 89 | "timestamp": true, | |
| 90 | "colorize": true | |
| 91 | } | |
| 92 | }; | |
| 93 | 1 | calipso.logging.configureLogging(loggingConfig); |
| 94 | ||
| 95 | /** | |
| 96 | * Request | |
| 97 | */ | |
| 98 | 1 | require('express/lib/request'); |
| 99 | 1 | require('express/lib/response'); |
| 100 | ||
| 101 | 1 | var Request = http.IncomingMessage, |
| 102 | Response = http.OutgoingMessage; | |
| 103 | ||
| 104 | 1 | Request.prototype.t = function (str) { |
| 105 | 14 | return str |
| 106 | }; | |
| 107 | ||
| 108 | 1 | function CreateRequest(url, method, session) { |
| 109 | 3 | var req = new Request(); |
| 110 | 3 | req.method = method || 'GET'; |
| 111 | 3 | req.url = url || '/'; |
| 112 | 3 | req.session = session || {}; |
| 113 | 3 | req.flashMsgs = []; |
| 114 | 3 | req.flash = function (type, msg) { |
| 115 | 0 | req.flashMsgs.push({ |
| 116 | type: type, | |
| 117 | msg: msg | |
| 118 | }); | |
| 119 | } | |
| 120 | 3 | return req; |
| 121 | } | |
| 122 | ||
| 123 | ||
| 124 | 1 | function CreateResponse() { |
| 125 | 1 | var res = new Response(); |
| 126 | 1 | res.redirectQueue = []; |
| 127 | 1 | res.redirect = function (url) { |
| 128 | 0 | res.redirectQueue.push(url); |
| 129 | 0 | res.finished = false; |
| 130 | } | |
| 131 | 1 | res.end = function(content, type) { |
| 132 | 0 | res.body = content; |
| 133 | } | |
| 134 | 1 | res.send = function(content) { |
| 135 | 0 | res.body = content; |
| 136 | } | |
| 137 | 1 | return res; |
| 138 | } | |
| 139 | /** | |
| 140 | * Default requests and users | |
| 141 | */ | |
| 142 | 1 | var requests = { |
| 143 | anonUser: CreateRequest('/', 'GET'), | |
| 144 | testUser: CreateRequest('/', 'GET', { | |
| 145 | user: { | |
| 146 | isAdmin: false, | |
| 147 | roles: ['Test'] | |
| 148 | } | |
| 149 | }), | |
| 150 | adminUser: CreateRequest('/secured', 'GET', { | |
| 151 | user: { | |
| 152 | isAdmin: true, | |
| 153 | roles: ['Administrator'] | |
| 154 | } | |
| 155 | }) | |
| 156 | } | |
| 157 | ||
| 158 | /** | |
| 159 | * Initialise everything and then export | |
| 160 | */ | |
| 161 | 1 | new MockApp(function (app) { |
| 162 | 1 | module.exports = { |
| 163 | app: app, | |
| 164 | calipso: calipso, | |
| 165 | testPermit: calipso.permission.Helper.hasPermission("test:permission"), | |
| 166 | requests: requests, | |
| 167 | response: CreateResponse() | |
| 168 | } | |
| 169 | }) |