ticketman
Version:
A simple pull-based job/ticket system contians a centeral ticket dispatcher and distributed workers. This system is written in NodeJS, runing on MongoDB
313 lines (245 loc) • 7.47 kB
text/coffeescript
mongoose = require('mongoose')
Ticket = mongoose.model('Ticket')
crypto = require 'crypto'
STATUS = require "../enums/ticket_status"
MAX_ATTEMPTS_BEFORE_ABANDON = 16
MAX_TIME_ALLOWED_FOR_PROCESSING = 1000 * 60 * 60
debuglog = require("debug")("ticketman:controller:ticket")
# list tickets
# GET /
# GET /tickets
exports.index = (req, res, next)->
debuglog "index"
res.render 'tickets/index',
title: 'All Tickets'
tickets : []
return
exports.list = (req, res, next)->
debuglog "list req.query: %j", req.query
query = Ticket.paginate(req.query || {}, '_id').select('-comments -content')
if req.query.status?
query.where
status : req.query.status
query.execPagination (err, result)->
return next err if err?
result.success = true
console.log "[ticket::list] dump result:"
console.dir result
res.json result
return
exports.count = (req, res, next)->
result = {}
Ticket.count (err, count)->
next err if err?
result.all = count
Ticket.count {status: STATUS.PENDING}, (err, count)->
next err if err?
result[STATUS.PENDING] = count
Ticket.count {status: STATUS.PROCESSING}, (err, count)->
next err if err?
result[STATUS.PROCESSING] = count
Ticket.count {status: STATUS.COMPLETE}, (err, count)->
next err if err?
result[STATUS.COMPLETE] = count
Ticket.count {status: STATUS.ABANDON}, (err, count)->
next err if err?
result[STATUS.ABANDON] = count
res.json result
return
return
return
return
return
return
# GET /tickets/:id
exports.show = (req, res, next)->
debuglog "show"
id = String(req.params.id || '')
return next() unless id?
Ticket.findById id, (err, ticket)->
return next err if err?
res.render 'tickets/show',
title: 'All Tickets'
ticket : ticket
return
return
# POST /api/tickets/new
exports.create = (req, res, next)->
debuglog "create"
title = (req.body||{}).title
#md5 = crypto.createHash('md5')
#md5.update(title||'')
#req.body.token = md5.digest('hex')
req.body.token = crypto.createHash('md5').update(title).digest('hex').toLowerCase()
ticket = new Ticket(req.body)
ticket.save (err)=>
if err?
return res.json
success : false
error : err.toString()
else
return res.json
success : true
ticket : ticket
return
# PUT /api/tickets/assign
exports.assign = (req, res, next)->
debuglog "assign, req.worker:%j", req.worker
req.body.worker = req.worker.name
Ticket.arrangeAssignment req.body, (err, ticket) ->
return next(err) if err?
unless ticket?
return res.json
success : false
error : "no pending ticket of #{req.body.category}"
# clear comments when assign ticket
ticket.comments = []
#console.log "!!!! before assign"
#console.dir ticket
#ticket = JSON.stringify(ticket)
#ticket = JSON.parse(ticket)
return res.json
success : true
ticket : ticket
return
# PUT /api/tickets/:id/comment
exports.comment = (req, res, next)->
id = req.params.id || ''
return next() unless id?
req.body.name = req.worker.name
Ticket.addComment id, req.body, (err, ticket)->
return next(err) if err?
unless ticket?
return res.json
success : false
error : "no commented ticket of #{id}"
return res.json
success : true
ticket : ticket
return
# PUT /api/tickets/:id/complete
exports.complete = (req, res, next)->
id = String(req.params.id || '')
return next() unless id?
req.body.id = id
Ticket.changeStatus req.body, STATUS.COMPLETE, (err, ticket)->
return next(err) if err?
return next() unless ticket?
return res.json ticket
return
# PUT /api/tickets/:id/giveup
exports.giveup = (req, res, next)->
id = String(req.params.id || '')
return next() unless id?
req.body.id = id
comment =
name: req.body.name || req.worker.name
kind: "danger"
content : req.body.reason || "#{req.worker.name} fail to process this ticket"
Ticket.addComment id, comment, (err, ticket)->
return next(err) if err?
unless ticket?
return res.json
success : false
error : "missing ticket of #{id}"
# abandon ticket if exceed max attempts
targetStatus = if ticket.attempts < MAX_ATTEMPTS_BEFORE_ABANDON then STATUS.PENDING else STATUS.ABANDON
Ticket.changeStatus req.body, targetStatus, (err, ticket)->
return next(err) if err?
return next() unless ticket?
ticket.update {$inc: {attempts:1}}, (err, numberAffected)->
return next(err) if err?
ticket.attempts = numberAffected
return res.json ticket
return
return
return
# PUT /tickets/:id/abandon
exports.abandon = (req, res, next)->
id = String(req.params.id || '')
return next() unless id?
Ticket.findById id, (err, ticket)->
return next err if err?
return next() unless ticket?
return next(new Error "only pending ticket could be abandoned") unless ticket.status is STATUS.PENDING
Ticket.changeStatus {id : ticket.id}, STATUS.ABANDON, (err, ticket)->
return next err if err?
return next() unless ticket?
return res.redirect "/tickets"
return
return
# PUT /tickets/:id/comment
exports.adminComment = (req, res, next)->
id = String(req.params.id || '')
return next() unless id?
req.body.content = req.body.content.trim()
console.log "[ticket::==========] req.body.content:#{req.body.content}"
return next(new Error "please say something") unless req.body.content
req.body.kind = "warning"
req.body.name = "admin"
Ticket.addComment id, req.body, (err, ticket)->
return next(err) if err?
return next() unless ticket?
return res.redirect "/tickets/#{id}"
return
#GET /tickets/:token/status
exports.showStatus = (req, res, next) ->
token = req.params.token
unless token
res.json
"success":false
"error": "missing param token"
return
query = {
token: token
}
Ticket.findOne query, (err, ticket) ->
if err?
res.json
"success":false
"error": "#{err}"
return
data =
"success": true
if ticket?
data['result'] =
id: ticket.id
status: ticket.status
else
data['result'] = null
return res.json data
return
# routine: clean up overtime processing tickets
setInterval ()->
#debuglog "clean up overtime processing tickets"
query =
$and: [
{status : STATUS.PROCESSING}
{updated_at : $lt : Date.now() - MAX_TIME_ALLOWED_FOR_PROCESSING}
]
Ticket.findOne query, (err, ticket)->
if err?
console.error "ERROR [ticket::interval::cleanup] error:#{err}"
return
unless ticket?
#debuglog "no ticket"
return
#debuglog "[interval::cleanup] ticket:"
#console.dir ticket
if ticket.attempts < MAX_ATTEMPTS_BEFORE_ABANDON
content = "ticket processing overtime, set back to retry."
targetStatus = STATUS.PENDING
else
content = "ticket exceeds max attemption, so abandon"
targetStatus = STATUS.ABANDON
ticket.comments.push
name : "admin"
kind : "danger"
content : content
date : Date.now()
ticket.status = targetStatus
ticket.save (err)->
#debuglog "change to ticket applied"
return
return
, 2000