Coverage

63%
1966
1242
724

core/Configuration.js

96%
58
56
2
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;

/Users/andy/calipso/test/helpers/require.js

66%
6
4
2
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 }

calipso.js

81%
92
75
17
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 }

core/Blocks.js

70%
30
21
9
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;

core/Cache.js

100%
7
7
0
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 }

core/cacheAdapters/memory.js

30%
39
12
27
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 };

core/cacheAdapters/store.js

75%
16
12
4
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 }

core/Date.js

13%
149
20
129
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();

core/Event.js

95%
132
126
6
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 }

core/Form.js

13%
302
40
262
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;

core/Helpers.js

66%
71
47
24
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 };

core/Lib.js

100%
2
2
0
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 };

core/Link.js

50%
12
6
6
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 };

core/Logging.js

84%
25
21
4
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 }

core/Menu.js

97%
148
145
3
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 }

core/Module.js

85%
262
225
37
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 };

core/Permission.js

55%
61
34
27
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;

core/Router.js

75%
90
68
22
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;

core/Storage.js

58%
41
24
17
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();

core/Table.js

70%
65
46
19
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 };

core/Themes.js

65%
238
155
83
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 }

core/Utils.js

37%
27
10
17
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, '&quot;').replace(/\'/g, '&apos;');
74 }
75 else {
76 0 return string;
77 }
78 }
79 };

client/Client.js

100%
44
44
0
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 **/

/Users/andy/calipso/test/helpers/calipsoHelper.js

85%
49
42
7
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 })