# 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 |  | })