UNPKG

10.6 kBtext/coffeescriptView Raw
1# Copyright (c) 2011 Leif Johnson <leif@leifjohnson.net>
2#
3# Permission is hereby granted, free of charge, to any person obtaining a copy
4# of this software and associated documentation files (the "Software"), to deal
5# in the Software without restriction, including without limitation the rights
6# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7# copies of the Software, and to permit persons to whom the Software is
8# furnished to do so, subject to the following conditions:
9#
10# The above copyright notice and this permission notice shall be included in all
11# copies or substantial portions of the Software.
12#
13# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19# SOFTWARE.
20
21_ = require 'underscore'
22crypto = require 'crypto'
23express = require 'express'
24fs = require 'fs'
25mongoose = require 'mongoose'
26nib = require 'nib'
27stitch = require 'stitch'
28stylus = require 'stylus'
29uglify = require 'uglify-js'
30
31
32module.exports.middleware = (options) ->
33 desktop = stitch.createPackage
34 paths: ["#{__dirname}/desktop"]
35 dependencies: [
36 "#{__dirname}/public/js/jquery.js"
37 "#{__dirname}/public/js/jquery-ui.js"
38 "#{__dirname}/public/js/underscore.js"
39 "#{__dirname}/public/js/backbone.js"
40 ]
41
42 desktop.compile (err, source) ->
43 {gen_code, ast_squeeze, ast_mangle} = uglify.uglify
44 minified = gen_code ast_squeeze ast_mangle uglify.parser.parse source
45 fs.writeFile "#{__dirname}/public/tomato-desktop.js", minified, (err) ->
46 throw err if err
47 console.log "compiled #{__dirname}/public/tomato-desktop.js"
48
49 # set up a private string to detect values that aren't present during POSTs
50 ALPHA = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-'
51 rc = -> ALPHA[Math.floor Math.random() * ALPHA.length]
52 NOTPRESENT = (rc() for x in [0...100]).join ''
53
54 TIMERS = _.extend { workSec: 25 * 60, breakSec: 5 * 60 }, options?.timers
55
56 mongoose.connect options?.db or 'mongodb://localhost/tomato'
57
58 # tasks are individual line items in a tomato -- something to be accomplished
59 TaskSchema = new mongoose.Schema
60 id: String
61 name: String
62 order:
63 type: Number
64 default: 0
65 min: -1e100
66 max: 1e100
67 createdAt: Date
68 updatedAt: Date
69 finishedAt: Date
70 tomatoes: [
71 startedAt: Date
72 finishedAt: Date
73 ]
74
75 # breaks are just labeled segments of time
76 BreakSchema = new mongoose.Schema
77 startedAt: Date
78 finishedAt: Date
79 flavor: String
80
81 # a tomato is pretty much a list of tasks and a list of breaks -- a todo list
82 TomatoSchema = new mongoose.Schema
83 slug:
84 type: String
85 unique: true
86 trim: true
87 match: /^[^\/]+$/
88 workSec:
89 type: Number
90 min: 1
91 breakSec:
92 type: Number
93 min: 1
94 createdAt: Date
95 updatedAt: Date
96 tasks: [ TaskSchema ]
97 breaks: [ BreakSchema ]
98
99 Task = mongoose.model 'Task', TaskSchema
100 Break = mongoose.model 'Break', BreakSchema
101 Tomato = mongoose.model 'Tomato', TomatoSchema
102
103 app = express.createServer()
104
105 app.configure 'development', ->
106 app.use express.errorHandler dumpExceptions: true, showStack: true
107
108 app.configure 'production', ->
109 app.use express.errorHandler()
110
111 app.configure ->
112 app.set 'views', __dirname
113 app.set 'view options', layout: false
114 app.set 'view engine', 'jade'
115 app.use express.logger 'short'
116 app.use stylus.middleware
117 src: "#{__dirname}/public"
118 compile: (str, path) ->
119 stylus(str).set('filename', path).set('compress', true).use(nib())
120 app.use express.methodOverride()
121 app.use express.bodyParser()
122 app.use express.static "#{__dirname}/public"
123 app.use app.router
124
125 app.get '/desktop.js', desktop.createServer()
126
127 # GET / -- create a new tomato
128 app.get '/', (req, res) ->
129 id = (rc() for x in [0...16]).join ''
130 now = Date.now()
131 tomato = slug: id, createdAt: now, updatedAt: now, tasks: []
132 new Tomato(_.extend tomato, TIMERS).save (err) ->
133 return res.send(err, 500) if err
134 res.redirect "/#{id}"
135
136 # TOMATO routes
137
138 fetchTomato = (req, res, next) ->
139 conditions = slug: req.param 'tomato'
140 fields = ['slug', 'workSec', 'breakSec', 'updatedAt']
141 Tomato.findOne conditions, fields, (err, tomato) ->
142 return res.send(err, 500) if err
143 return res.send(404) unless tomato
144 res.locals tomato: tomato
145 next()
146
147 # GET /:tomato -- return the html to drive the client-side tomato app
148 app.get '/:tomato', fetchTomato, (req, res) ->
149 ctx =
150 analytics: options?.analytics
151 basepath: (app.settings.basepath or '').replace /\/$/, ''
152 res.render 'main', ctx
153
154 # PUT /:tomato -- update the id, name, etc. of a tomato
155 app.put '/:tomato', fetchTomato, (req, res) ->
156 tomato = res.local 'tomato'
157 now = Date.now()
158 for key in ['slug', 'workSec', 'breakSec']
159 value = req.param key, NOTPRESENT
160 if value isnt NOTPRESENT
161 tomato.set key, value
162 tomato.set 'updatedAt', now
163 tomato.save (err) ->
164 return res.send(err, 500) if err
165 res.send 200
166
167 # DELETE /:tomato -- delete a tomato and all tasks and breaks
168 app.del '/:tomato', (req, res) ->
169 Tomato.remove { slug: req.param 'tomato' }, (err) ->
170 return res.send(err, 500) if err
171 res.send 200
172
173 # TASK routes
174
175 fetchTasks = (req, res, next) ->
176 conditions = slug: req.param 'tomato'
177 fields = ['slug', 'updatedAt', 'tasks']
178 Tomato.findOne conditions, fields, (err, tomato) ->
179 return res.send(err, 500) if err
180 return res.send(404) unless tomato
181 res.locals tomato: tomato
182 next()
183
184 fetchTask = (req, res, next) ->
185 id = req.param 'task'
186 conditions = slug: req.param 'tomato'
187 fields = ['slug', 'updatedAt', 'tasks']
188 Tomato.findOne conditions, fields, (err, tomato) ->
189 return res.send(err, 500) if err
190 return res.send(404) unless tomato
191 task = _.find tomato.tasks, (t) -> t.id is id
192 return res.send(404) unless task?
193 res.locals tomato: tomato, task: task
194 next()
195
196 # GET /:tomato/tasks -- get all tasks for a tomato
197 app.get '/:tomato/tasks', fetchTasks, (req, res) ->
198 res.send res.local('tomato').tasks
199
200 # POST /:tomato/tasks -- create a new task for a tomato
201 app.post '/:tomato/tasks', fetchTasks, (req, res) ->
202 hash = (s) ->
203 h = crypto.createHash 'md5'
204 h.update s
205 return h.digest 'hex'
206
207 tomato = res.local 'tomato'
208 name = req.param 'name'
209 now = Date.now()
210 task =
211 id: hash "#{now}:#{name}"
212 name: name
213 order: req.param('order')
214 createdAt: now
215 updatedAt: now
216 finishedAt: null
217 tomato.tasks.push task
218 tomato.set 'updatedAt', now
219 tomato.save (err) ->
220 return res.send(err, 500) if err
221 res.send task
222
223 # GET /:tomato/tasks/:task -- return data for a task
224 app.get '/:tomato/tasks/:task', fetchTask, (req, res) ->
225 res.send res.local 'task'
226
227 # PUT /:tomato/tasks/:task -- update data for a task
228 app.put '/:tomato/tasks/:task', fetchTask, (req, res) ->
229 tomato = res.local 'tomato'
230 task = res.local 'task'
231 now = Date.now()
232 for key in ['name', 'order', 'finishedAt', 'tomatoes']
233 value = req.param key, NOTPRESENT
234 if value isnt NOTPRESENT
235 task.set key, value
236 task.set 'updatedAt', now
237 tomato.set 'updatedAt', now
238 tomato.save (err) ->
239 return res.send(err, 500) if err
240 res.send 200
241
242 # DELETE /:tomato/tasks/:task -- remove a task
243 app.del '/:tomato/tasks/:task', fetchTasks, (req, res) ->
244 tomato = res.local 'tomato'
245 now = Date.now()
246 index = _.indexOf _.pluck(tomato.tasks, 'id'), req.param 'task'
247 tomato.tasks.splice index, 1
248 tomato.set 'updatedAt', now
249 tomato.save (err) ->
250 return res.send(err, 500) if err
251 res.send 200
252
253 # BREAK routes
254
255 fetchBreaks = (req, res, next) ->
256 conditions = slug: req.param 'tomato'
257 fields = ['slug', 'updatedAt', 'breaks']
258 Tomato.findOne conditions, fields, (err, tomato) ->
259 return res.send(err, 500) if err
260 return res.send(404) unless tomato
261 res.locals tomato: tomato
262 next()
263
264 fetchBreak = (req, res, next) ->
265 id = req.param 'break'
266 conditions = slug: req.param 'tomato'
267 fields = ['slug', 'updatedAt', 'breaks']
268 Tomato.findOne conditions, fields, (err, tomato) ->
269 return res.send(err, 500) if err
270 return res.send(404) unless tomato
271 brake_ = _.find tomato.breaks, (t) -> t.id is id
272 return res.send(404) unless brake
273 res.locals tomato: tomato, break: brake
274 next()
275
276 # GET /:tomato/breaks -- get all breaks for a tomato
277 app.get '/:tomato/breaks', fetchBreaks, (req, res) ->
278 res.send res.local('tomato').breaks
279
280 # POST /:tomato/breaks -- create a new break for a tomato
281 app.post '/:tomato/breaks', fetchBreaks, (req, res) ->
282 hash = (s) ->
283 h = crypto.createHash 'md5'
284 h.update s
285 return h.digest 'hex'
286
287 # GET /:tomato/breaks/:break -- return data for a break
288 app.get '/:tomato/breaks/:break', fetchBreak, (req, res) ->
289 res.send res.local 'break'
290
291 # PUT /:tomato/breaks/:break -- update data for a break
292 app.put '/:tomato/breaks/:break', fetchBreak, (req, res) ->
293 tomato = res.local 'tomato'
294 brake = res.local 'break'
295 now = Date.now()
296 for key in ['name', 'order', 'finishedAt', 'tomatoes']
297 value = req.param key, NOTPRESENT
298 if value isnt NOTPRESENT
299 brake.set key, value
300 brake.set 'updatedAt', now
301 tomato.set 'updatedAt', now
302 tomato.save (err) ->
303 return res.send(err, 500) if err
304 res.send 200
305
306 # DELETE /:tomato/breaks/:break -- remove a break
307 app.del '/:tomato/breaks/:break', fetchBreaks, (req, res) ->
308 tomato = res.local 'tomato'
309 now = Date.now()
310 index = _.indexOf _.pluck(tomato.breaks, 'id'), req.param 'break'
311 tomato.breaks.splice index, 1
312 tomato.set 'updatedAt', now
313 tomato.save (err) ->
314 return res.send(err, 500) if err
315 res.send 200
316
317 return app
318
319
320unless module.parent
321 exports.middleware().listen 4000
322 console.log 'tomato server listening on http://localhost:4000'