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