2014-01-03 13:32:13 -05:00
async = require ' async '
2014-06-30 22:16:26 -04:00
mongoose = require ' mongoose '
2014-01-03 13:32:13 -05:00
Grid = require ' gridfs-stream '
2014-01-22 17:57:41 -05:00
errors = require ' ./errors '
2014-03-28 14:02:23 -04:00
log = require ' winston '
2014-04-08 22:26:19 -04:00
Patch = require ' ../patches/Patch '
2014-04-17 20:30:55 -04:00
User = require ' ../users/User '
sendwithus = require ' ../sendwithus '
2014-08-28 22:39:46 -04:00
hipchat = require ' ../hipchat '
2014-11-28 20:49:41 -05:00
deltasLib = require ' ../../app/core/deltas '
2014-03-28 14:02:23 -04:00
2014-08-28 13:50:20 -04:00
PROJECT = { original: 1 , name: 1 , version: 1 , description: 1 , slug: 1 , kind: 1 , created: 1 , permissions: 1 }
2014-11-20 14:37:10 -05:00
FETCH_LIMIT = 1000 # So many ThangTypes
2014-01-03 13:32:13 -05:00
module.exports = class Handler
# subclasses should override these properties
modelClass: null
2014-07-22 14:07:00 -04:00
privateProperties: [ ]
2014-01-03 13:32:13 -05:00
editableProperties: [ ]
postEditableProperties: [ ]
jsonSchema: { }
waterfallFunctions: [ ]
2014-06-09 11:28:35 -04:00
allowedMethods: [ ' GET ' , ' POST ' , ' PUT ' , ' PATCH ' ]
2014-01-03 13:32:13 -05:00
2014-07-22 14:07:00 -04:00
constructor: ->
2014-12-15 13:02:05 -05:00
# TODO The second 'or' is for backward compatibility only
2014-12-02 23:01:35 -05:00
@privateProperties = @ modelClass ? . privateProperties or @ privateProperties or [ ]
@editableProperties = @ modelClass ? . editableProperties or @ editableProperties or [ ]
@postEditableProperties = @ modelClass ? . postEditableProperties or @ postEditableProperties or [ ]
@jsonSchema = @ modelClass ? . jsonSchema or @ jsonSchema or { }
2014-07-22 14:07:00 -04:00
2014-01-03 13:32:13 -05:00
# subclasses should override these methods
hasAccess: (req) -> true
hasAccessToDocument: (req, document, method=null) ->
2014-02-24 23:27:38 -05:00
return true if req . user ? . isAdmin ( )
2014-10-28 01:54:50 -04:00
2015-02-05 14:47:27 -05:00
if @ modelClass . schema . uses_coco_translation_coverage and ( method or req . method ) . toLowerCase ( ) in [ ' post ' , ' put ' ]
2014-10-27 20:11:48 -04:00
return true if @ isJustFillingTranslations ( req , document )
2014-10-28 01:54:50 -04:00
2014-01-03 13:32:13 -05:00
if @ modelClass . schema . uses_coco_permissions
2014-04-13 17:06:27 -04:00
return document . hasPermissionsForMethod ? ( req . user , method or req . method )
2014-01-03 13:32:13 -05:00
return true
2014-10-28 01:54:50 -04:00
2014-10-27 20:11:48 -04:00
isJustFillingTranslations: (req, document) ->
differ = deltasLib . makeJSONDiffer ( )
omissions = [ ' original ' ] . concat ( deltasLib . DOC_SKIP_PATHS )
delta = differ . diff ( _ . omit ( document . toObject ( ) , omissions ) , _ . omit ( req . body , omissions ) )
flattened = deltasLib . flattenDelta ( delta )
2014-10-30 16:07:04 -04:00
_ . all flattened , (delta) ->
2014-10-27 20:11:48 -04:00
# sometimes coverage gets moved around... allow other changes to happen to i18nCoverage
2014-11-25 19:20:41 -05:00
return false unless _ . isArray ( delta . o )
return true if ' i18nCoverage ' in delta . dataPath
return false unless delta . o . length is 1
index = delta . deltaPath . indexOf ( ' i18n ' )
return false if index is - 1
2015-02-17 12:29:08 -05:00
return false if delta . deltaPath [ index + 1 ] in [ ' en ' , ' en-US ' , ' en-GB ' ] # English speakers are most likely just spamming, so always treat those as patches, not saves.
2014-11-25 19:20:41 -05:00
return true
2014-01-03 13:32:13 -05:00
formatEntity: (req, document) -> document ? . toObject ( )
getEditableProperties: (req, document) ->
props = @ editableProperties . slice ( )
isBrandNew = req . method is ' POST ' and not req . body . original
2014-04-08 22:26:19 -04:00
props = props . concat @ postEditableProperties if isBrandNew
2014-01-03 13:32:13 -05:00
2014-11-22 21:40:28 -05:00
if @ modelClass . schema . uses_coco_permissions and req . user
2014-01-03 13:32:13 -05:00
# can only edit permissions if this is a brand new property,
# or you are an owner of the old one
isOwner = document . getAccessForUserObjectId ( req . user . _id ) is ' owner '
2014-02-24 23:27:38 -05:00
if isBrandNew or isOwner or req . user ? . isAdmin ( )
2014-01-03 13:32:13 -05:00
props . push ' permissions '
2014-04-08 22:26:19 -04:00
props . push ' commitMessage ' if @ modelClass . schema . uses_coco_versions
props . push ' allowPatches ' if @ modelClass . schema . is_patchable
2014-01-03 13:32:13 -05:00
props
# sending functions
2015-09-03 14:04:40 -04:00
sendUnauthorizedError: (res) -> errors . unauthorized ( res )
2014-06-24 12:14:26 -04:00
sendForbiddenError: (res) -> errors . forbidden ( res )
2014-07-29 06:45:47 -04:00
sendNotFoundError: (res, message) -> errors . notFound ( res , message )
2014-08-08 11:14:57 -04:00
sendMethodNotAllowed: (res, message) -> errors . badMethod ( res , @ allowedMethods , message )
2014-01-14 17:13:47 -05:00
sendBadInputError: (res, message) -> errors . badInput ( res , message )
2014-12-03 16:32:04 -05:00
sendPaymentRequiredError: (res, message) -> errors . paymentRequired ( res , message )
2014-03-31 16:56:13 -04:00
sendDatabaseError: (res, err) ->
2015-04-20 12:09:42 -04:00
return @ sendError ( res , err . code , err . response ) if err ? . response and err ? . code
2014-03-31 16:56:13 -04:00
log . error " Database error, #{ err } "
errors . serverError ( res , ' Database error, ' + err )
2014-01-03 13:32:13 -05:00
sendError: (res, code, message) ->
2014-01-14 17:13:47 -05:00
errors . custom ( res , code , message )
2014-01-03 13:32:13 -05:00
2014-12-12 18:27:58 -05:00
sendSuccess: (res, message='{}') ->
res . send 200 , message
2014-01-03 13:32:13 -05:00
res . end ( )
2014-12-12 18:27:58 -05:00
sendCreated: (res, message='{}') ->
2014-06-24 12:14:26 -04:00
res . send 201 , message
res . end ( )
2014-12-12 18:27:58 -05:00
sendAccepted: (res, message='{}') ->
2014-06-24 12:14:26 -04:00
res . send 202 , message
res . end ( )
2014-08-08 11:14:57 -04:00
sendNoContent: (res) ->
res . send 204
2014-06-24 12:14:26 -04:00
res . end ( )
2014-01-03 13:32:13 -05:00
# generic handlers
get: (req, res) ->
2014-12-17 00:45:17 -05:00
return @ sendForbiddenError ( res ) if not @ hasAccess ( req )
2014-06-09 10:18:26 -04:00
2014-10-28 12:47:49 -04:00
specialParameters = [ ' term ' , ' project ' , ' conditions ' ]
2014-10-27 20:11:48 -04:00
if @ modelClass . schema . uses_coco_translation_coverage and req . query . view is ' i18n-coverage '
# TODO: generalize view, project, limit and skip query parameters
projection = { }
if req . query . project
projection [ field ] = 1 for field in req . query . project . split ( ' , ' )
query = { slug: { $exists: true } , i18nCoverage: { $exists: true } }
q = @ modelClass . find ( query , projection )
skip = parseInt ( req . query . skip )
if skip ? and skip < 1000000
q . skip ( skip )
limit = parseInt ( req . query . limit )
if limit ? and limit < 1000
q . limit ( limit )
q . exec (err, documents) =>
return @ sendDatabaseError ( res , err ) if err
documents = ( @ formatEntity ( req , doc ) for doc in documents )
@ sendSuccess ( res , documents )
2014-06-09 10:18:26 -04:00
# If the model uses coco search it's probably a text search
2014-10-28 12:47:49 -04:00
else if @ modelClass . schema . uses_coco_search
2014-06-09 10:18:26 -04:00
term = req . query . term
matchedObjects = [ ]
filters = if @ modelClass . schema . uses_coco_versions or @ modelClass . schema . uses_coco_permissions then [ filter: { index: true } ] else [ filter: { } ]
2014-11-19 17:55:01 -05:00
skip = parseInt ( req . query . skip )
limit = parseInt ( req . query . limit )
2014-06-09 10:18:26 -04:00
if @ modelClass . schema . uses_coco_permissions and req . user
filters . push { filter: { index: req . user . get ( ' id ' ) } }
projection = null
if req . query . project is ' true '
projection = PROJECT
else if req . query . project
if @ modelClass . className is ' User '
projection = PROJECT
2014-06-30 22:16:26 -04:00
log . warn ' Whoa, we haven \' t yet thought about public properties for User projection yet. '
2014-06-09 10:18:26 -04:00
else
projection = { }
projection [ field ] = 1 for field in req . query . project . split ( ' , ' )
for filter in filters
callback = (err, results) =>
return @ sendDatabaseError ( res , err ) if err
for r in results . results ? results
obj = r . obj ? r
continue if obj in matchedObjects # TODO: probably need a better equality check
matchedObjects . push obj
filters . pop ( ) # doesn't matter which one
unless filters . length
res . send matchedObjects
res . end ( )
if term
2015-02-21 17:22:43 -05:00
filter.filter.$text = $search: term
2015-11-03 12:32:59 -05:00
else if filters . length is 1 and filters [ 0 ] . filter ? . index is true
# All we are doing is an empty text search, but that doesn't hit the index, so we'll just look for the slug.
filter.filter = slug: { $exists: true }
2015-02-21 17:22:43 -05:00
args = [ filter . filter ]
args . push projection if projection
q = @ modelClass . find ( args . . . )
if skip ? and skip < 1000000
q . skip ( skip )
if limit ? and limit < FETCH_LIMIT
q . limit ( limit )
2014-06-09 10:18:26 -04:00
else
2015-02-21 17:22:43 -05:00
q . limit ( FETCH_LIMIT )
q . exec callback
2014-06-09 10:18:26 -04:00
# if it's not a text search but the user is an admin, let him try stuff anyway
else if req . user ? . isAdmin ( )
# admins can send any sort of query down the wire
2015-10-09 17:24:59 -04:00
# Example URL: http://localhost:3000/db/user?filter[anonymous]=true
2014-06-09 10:18:26 -04:00
filter = { }
2015-10-09 17:24:59 -04:00
filter [ key ] = JSON . parse ( val ) for own key , val of req . query . filter when key not in specialParameters if ' filter ' of req . query
2014-06-09 10:18:26 -04:00
query = @ modelClass . find ( filter )
# Conditions are chained query functions, for example: query.find().limit(20).sort('-dateCreated')
2015-10-09 17:24:59 -04:00
# Example URL: http://localhost:3000/db/user?conditions[limit]=20&conditions[sort]=-dateCreated
2015-02-18 17:12:23 -05:00
hasLimit = false
2014-06-09 10:18:26 -04:00
try
2015-10-09 17:24:59 -04:00
for own key , val of req . query . conditions
query = query [ key ] ( val )
hasLimit || = key is ' limit '
2014-06-09 10:18:26 -04:00
catch e
return @ sendError ( res , 422 , ' Badly formed conditions. ' )
2015-02-18 17:12:23 -05:00
query . limit ( 2000 ) unless hasLimit
2014-06-09 10:18:26 -04:00
query . exec (err, documents) =>
return @ sendDatabaseError ( res , err ) if err
documents = ( @ formatEntity ( req , doc ) for doc in documents )
@ sendSuccess ( res , documents )
# regular users are only allowed text searches for now, without any additional filters or sorting
else
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res )
2014-06-09 10:18:26 -04:00
2014-01-03 13:32:13 -05:00
getById: (req, res, id) ->
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccess ( req )
2015-04-08 15:00:12 -04:00
if req . query . project
projection = { }
projection [ field ] = 1 for field in req . query . project . split ( ' , ' )
@ getDocumentForIdOrSlug id , projection , (err, document) =>
2014-01-03 13:32:13 -05:00
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless document ?
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccessToDocument ( req , document )
2015-04-07 22:03:35 -04:00
res . setHeader ' Cache-Control ' , ' no-cache ' unless Handler . isID ( id + ' ' ) # Don't cache if it's a slug instead of an ID
2014-01-03 13:32:13 -05:00
@ sendSuccess ( res , @ formatEntity ( req , document ) )
getByRelationship: (req, res, args...) ->
# this handler should be overwritten by subclasses
2014-04-08 22:26:19 -04:00
if @ modelClass . schema . is_patchable
return @ getPatchesFor ( req , res , args [ 0 ] ) if req . route . method is ' get ' and args [ 1 ] is ' patches '
2014-04-15 18:09:36 -04:00
return @ setWatching ( req , res , args [ 0 ] ) if req . route . method is ' put ' and args [ 1 ] is ' watch '
2014-01-03 13:32:13 -05:00
return @ sendNotFoundError ( res )
2014-04-28 17:31:11 -04:00
getNamesByIDs: (req, res) ->
ids = req . query . ids or req . body . ids
if @ modelClass . schema . uses_coco_versions
return @ getNamesByOriginals ( req , res )
2014-06-30 22:16:26 -04:00
@ getPropertiesFromMultipleDocuments res , User , ' name ' , ids
2014-04-28 17:31:11 -04:00
2015-03-28 16:54:44 -04:00
getNamesByOriginals: (req, res, nonVersioned=false) ->
2014-04-28 17:31:11 -04:00
ids = req . query . ids or req . body . ids
ids = ids . split ( ' , ' ) if _ . isString ids
ids = _ . uniq ids
2015-02-12 11:50:45 -05:00
# Hack: levels loading thang types need the components returned as well.
2014-08-07 21:27:47 -04:00
# Need a way to specify a projection for a query.
2015-10-13 19:43:56 -04:00
project = { name: 1 , original: 1 , kind: 1 , components: 1 , prerenderedSpriteSheetData: 1 }
2015-03-28 16:54:44 -04:00
sort = if nonVersioned then { } else { ' version.major ' : - 1 , ' version.minor ' : - 1 }
2014-04-28 17:31:11 -04:00
makeFunc = (id) =>
(callback) =>
2015-03-28 16:54:44 -04:00
criteria = { }
criteria [ if nonVersioned then ' _id ' else ' original ' ] = mongoose . Types . ObjectId ( id )
2014-04-28 17:31:11 -04:00
@ modelClass . findOne ( criteria , project ) . sort ( sort ) . exec (err, document) ->
return done ( err ) if err
2014-04-28 17:58:58 -04:00
callback ( null , document ? . toObject ( ) or null )
2014-05-19 17:59:43 -04:00
2014-04-28 17:31:11 -04:00
funcs = { }
for id in ids
return errors . badInput ( res , " Given an invalid id: #{ id } " ) unless Handler . isID ( id )
funcs [ id ] = makeFunc ( id )
async . parallel funcs , (err, results) ->
return errors . serverError err if err
2014-04-28 17:58:58 -04:00
res . send ( d for d in _ . values ( results ) when d )
2014-04-28 17:31:11 -04:00
res . end ( )
2014-04-08 22:26:19 -04:00
getPatchesFor: (req, res, id) ->
2015-02-16 20:38:25 -05:00
query =
$or: [
{ ' target.original ' : id + ' ' }
{ ' target.original ' : mongoose . Types . ObjectId ( id ) }
]
status: req . query . status or ' pending '
2014-04-08 22:26:19 -04:00
Patch . find ( query ) . sort ( ' -created ' ) . exec (err, patches) =>
return @ sendDatabaseError ( res , err ) if err
2014-04-19 20:52:17 -04:00
patches = ( patch . toObject ( ) for patch in patches )
2014-04-08 22:26:19 -04:00
@ sendSuccess ( res , patches )
2014-04-15 18:09:36 -04:00
setWatching: (req, res, id) ->
2014-04-08 22:26:19 -04:00
@ getDocumentForIdOrSlug id , (err, document) =>
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccessToDocument ( req , document , ' get ' )
2014-04-08 22:26:19 -04:00
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless document ?
2014-04-15 18:09:36 -04:00
watchers = document . get ( ' watchers ' ) or [ ]
2014-04-08 22:26:19 -04:00
me = req . user . get ( ' _id ' )
2014-04-15 18:09:36 -04:00
watchers = ( l for l in watchers when not l . equals ( me ) )
watchers . push me if req . body . on and req . body . on isnt ' false '
document . set ' watchers ' , watchers
2014-04-08 22:26:19 -04:00
document . save (err, document) =>
return @ sendDatabaseError ( res , err ) if err
@ sendSuccess ( res , @ formatEntity ( req , document ) )
2014-01-03 13:32:13 -05:00
versions: (req, res, id) ->
# TODO: a flexible system for doing GAE-like cursors for these sort of paginating queries
2014-02-17 12:22:12 -05:00
# Keeping it simple for now and just allowing access to the first FETCH_LIMIT results.
2014-01-03 13:32:13 -05:00
query = { ' original ' : mongoose . Types . ObjectId ( id ) }
sort = { ' created ' : - 1 }
2014-06-24 16:41:55 -04:00
selectString = ' slug name version commitMessage created creator permissions '
2014-03-28 16:51:52 -04:00
aggregate = $match: query
@ modelClass . aggregate ( aggregate ) . project ( selectString ) . limit ( FETCH_LIMIT ) . sort ( sort ) . exec (err, results) =>
2014-03-10 23:22:25 -04:00
return @ sendDatabaseError ( res , err ) if err
2014-01-03 13:32:13 -05:00
res . send ( results )
res . end ( )
files: (req, res, id) ->
module = req . path [ 4 . . ] . split ( ' / ' ) [ 0 ]
query = { ' metadata.path ' : " db/ #{ module } / #{ id } " }
Grid . gfs . collection ( ' media ' ) . find query , (err, cursor) ->
return @ sendDatabaseError ( res , err ) if err
results = cursor . toArray (err, results) ->
return @ sendDatabaseError ( res , err ) if err
res . send ( results )
res . end ( )
getLatestVersion: (req, res, original, version) ->
# can get latest overall version, latest of a major version, or a specific version
2014-11-29 14:11:40 -05:00
return @ sendBadInputError ( res , ' Invalid MongoDB id: ' + original ) if not Handler . isID ( original )
2014-12-14 16:27:39 -05:00
2014-01-03 13:32:13 -05:00
query = { ' original ' : mongoose . Types . ObjectId ( original ) }
if version ?
version = version . split ( ' . ' )
majorVersion = parseInt ( version [ 0 ] )
minorVersion = parseInt ( version [ 1 ] )
query [ ' version.major ' ] = majorVersion unless _ . isNaN ( majorVersion )
query [ ' version.minor ' ] = minorVersion unless _ . isNaN ( minorVersion )
sort = { ' version.major ' : - 1 , ' version.minor ' : - 1 }
2014-02-15 17:09:30 -05:00
args = [ query ]
2014-07-17 19:28:52 -04:00
if req . query . project
projection = { }
fields = if req . query . project is ' true ' then _ . keys ( PROJECT ) else req . query . project . split ( ' , ' )
projection [ field ] = 1 for field in fields
2014-12-28 16:25:20 -05:00
# Make sure that permissions and version are fetched, but not sent back if they didn't ask for them.
extraProjectionProps = [ ]
extraProjectionProps . push ' permissions ' unless projection . permissions
extraProjectionProps . push ' version ' unless projection . version
projection.permissions = 1
projection.version = 1
2014-07-17 19:28:52 -04:00
args . push projection
2014-02-15 17:09:30 -05:00
@ modelClass . findOne ( args . . . ) . sort ( sort ) . exec (err, doc) =>
2014-01-03 13:32:13 -05:00
return @ sendNotFoundError ( res ) unless doc ?
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccessToDocument ( req , doc )
2014-12-28 16:25:20 -05:00
doc = _ . omit doc , extraProjectionProps if extraProjectionProps ?
2014-01-03 13:32:13 -05:00
res . send ( doc )
res . end ( )
patch: ->
2014-10-27 19:09:52 -04:00
console . warn ' Received unexpected PATCH request '
2014-01-03 13:32:13 -05:00
@ put ( arguments . . . )
put: (req, res, id) ->
2014-10-27 19:09:52 -04:00
# Client expects PATCH behavior for PUTs
# Real PATCHs return incorrect HTTP responses in some environments (e.g. Browserstack, schools)
2014-12-29 12:14:43 -05:00
return @ sendForbiddenError ( res ) if @ modelClass . schema . uses_coco_versions and not req . user . isAdmin ( ) # Campaign editor just saves over things.
2014-01-03 13:32:13 -05:00
return @ sendBadInputError ( res , ' No input. ' ) if _ . isEmpty ( req . body )
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccess ( req )
2014-01-03 13:32:13 -05:00
@ getDocumentForIdOrSlug req . body . _id or id , (err, document) =>
return @ sendBadInputError ( res , ' Bad id. ' ) if err and err . name is ' CastError '
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless document ?
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccessToDocument ( req , document )
2014-01-03 13:32:13 -05:00
@ doWaterfallChecks req , document , (err, document) =>
2014-12-02 23:01:35 -05:00
return if err is true
2014-01-03 13:32:13 -05:00
return @ sendError ( res , err . code , err . res ) if err
@ saveChangesToDocument req , document , (err) =>
return @ sendBadInputError ( res , err . errors ) if err ? . valid is false
return @ sendDatabaseError ( res , err ) if err
@ sendSuccess ( res , @ formatEntity ( req , document ) )
2014-12-28 16:25:20 -05:00
@ onPutSuccess ( req , document )
2014-01-03 13:32:13 -05:00
post: (req, res) ->
if @ modelClass . schema . uses_coco_versions
if req . body . original
return @ postNewVersion ( req , res )
else
return @ postFirstVersion ( req , res )
return @ sendBadInputError ( res , ' No input. ' ) if _ . isEmpty ( req . body )
return @ sendBadInputError ( res , ' id should not be included. ' ) if req . body . _id
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccess ( req )
2014-01-03 13:32:13 -05:00
document = @ makeNewInstance ( req )
@ saveChangesToDocument req , document , (err) =>
2014-04-08 17:10:50 -04:00
return @ sendBadInputError ( res , err . errors ) if err ? . valid is false
2014-01-03 13:32:13 -05:00
return @ sendDatabaseError ( res , err ) if err
@ sendSuccess ( res , @ formatEntity ( req , document ) )
2014-04-17 20:09:01 -04:00
@ onPostSuccess ( req , document )
onPostSuccess: (req, doc) ->
2014-12-28 16:25:20 -05:00
onPutSuccess: (req, doc) ->
2014-01-03 13:32:13 -05:00
2014-01-08 14:27:13 -05:00
# ##
TODO: think about pulling some common stuff out of postFirstVersion / postNewVersion
into a postVersion if we can figure out the breakpoints ?
. . . . . actually , probably better would be to do the returns with throws instead
and have a handler which turns them into status codes and messages
# ##
2014-01-03 13:32:13 -05:00
postFirstVersion: (req, res) ->
return @ sendBadInputError ( res , ' No input. ' ) if _ . isEmpty ( req . body )
return @ sendBadInputError ( res , ' id should not be included. ' ) if req . body . _id
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccess ( req )
2014-01-03 13:32:13 -05:00
document = @ makeNewInstance ( req )
document . set ( ' original ' , document . _id )
document . set ( ' creator ' , req . user . _id )
@ saveChangesToDocument req , document , (err) =>
2014-04-08 17:10:50 -04:00
return @ sendBadInputError ( res , err . errors ) if err ? . valid is false
2014-01-03 13:32:13 -05:00
return @ sendDatabaseError ( res , err ) if err
@ sendSuccess ( res , @ formatEntity ( req , document ) )
postNewVersion: (req, res) ->
"""
To the client , posting new versions look like this :
POST / db / modelname
With the input being just the altered structure of the old version ,
leaving the _id property intact even .
No version object means it ' s a new major version.
A version object with a major value means a new minor version .
All other properties in version are ignored .
"""
return @ sendBadInputError ( res , ' This entity is not versioned ' ) unless @ modelClass . schema . uses_coco_versions
return @ sendBadInputError ( res , ' No input. ' ) if _ . isEmpty ( req . body )
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccess ( req )
2014-01-03 13:32:13 -05:00
@ getDocumentForIdOrSlug req . body . _id , (err, parentDocument) =>
return @ sendBadInputError ( res , ' Bad id. ' ) if err and err . name is ' CastError '
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless parentDocument ?
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccessToDocument ( req , parentDocument )
2014-04-01 15:13:11 -04:00
editableProperties = @ getEditableProperties req , parentDocument
2014-01-03 13:32:13 -05:00
updatedObject = parentDocument . toObject ( )
2014-04-01 15:13:11 -04:00
for prop in editableProperties
if ( val = req . body [ prop ] ) ?
updatedObject [ prop ] = val
else if updatedObject [ prop ] ?
delete updatedObject [ prop ]
2014-01-03 13:32:13 -05:00
delete updatedObject . _id
major = req . body . version ? . major
2014-04-08 17:10:50 -04:00
validation = @ validateDocumentInput ( updatedObject )
return @ sendBadInputError ( res , validation . errors ) unless validation . valid
2014-01-03 13:32:13 -05:00
done = (err, newDocument) =>
return @ sendDatabaseError ( res , err ) if err
newDocument . set ( ' creator ' , req . user . _id )
newDocument . save (err) =>
return @ sendDatabaseError ( res , err ) if err
@ sendSuccess ( res , @ formatEntity ( req , newDocument ) )
2014-05-30 17:41:41 -04:00
if @ modelClass . schema . is_patchable
@ notifyWatchersOfChange ( req . user , newDocument , req . headers [ ' x-current-path ' ] )
2014-01-03 13:32:13 -05:00
if major ?
parentDocument . makeNewMinorVersion ( updatedObject , major , done )
else
parentDocument . makeNewMajorVersion ( updatedObject , done )
2014-04-17 20:30:55 -04:00
notifyWatchersOfChange: (editor, changedDocument, editPath) ->
2014-08-28 22:39:46 -04:00
docLink = " http://codecombat.com #{ editPath } "
@ sendChangedHipChatMessage creator: editor , target: changedDocument , docLink: docLink
2014-04-17 20:30:55 -04:00
watchers = changedDocument . get ( ' watchers ' ) or [ ]
2015-02-25 14:16:57 -05:00
# Don't send these emails to the person who submitted the patch, or to Nick, George, or Scott.
watchers = ( w for w in watchers when not w . equals ( editor . get ( ' _id ' ) ) and not ( w + ' ' in [ ' 512ef4805a67a8c507000001 ' , ' 5162fab9c92b4c751e000274 ' , ' 51538fdb812dd9af02000001 ' ] ) )
2014-04-17 20:30:55 -04:00
return unless watchers . length
User . find ( { _id : { $in : watchers } } ) . select ( { email : 1 , name : 1 } ) . exec (err, watchers) =>
for watcher in watchers
@ notifyWatcherOfChange editor , watcher , changedDocument , editPath
notifyWatcherOfChange: (editor, watcher, changedDocument, editPath) ->
context =
email_id: sendwithus . templates . change_made_notify_watcher
recipient:
address: watcher . get ( ' email ' )
name: watcher . get ( ' name ' )
email_data:
doc_name: changedDocument . get ( ' name ' ) or ' ??? '
submitter_name: editor . get ( ' name ' ) or ' ??? '
doc_link: if editPath then " http://codecombat.com #{ editPath } " else null
commit_message: changedDocument . get ( ' commitMessage ' )
sendwithus . api . send context , (err, result) ->
2014-08-28 22:39:46 -04:00
sendChangedHipChatMessage: (options) ->
2015-02-04 19:17:53 -05:00
message = " #{ options . creator . get ( ' name ' ) } saved a change to <a href= \" #{ options . docLink } \" > #{ options . target . get ( ' name ' ) } </a>: #{ options . target . get ( ' commitMessage ' ) or ' (no commit message) ' } "
rooms = if /Diplomat submission/ . test ( message ) then [ ' main ' ] else [ ' main ' , ' artisans ' ]
hipchat . sendHipChatMessage message , rooms
2014-08-28 22:39:46 -04:00
2014-01-03 13:32:13 -05:00
makeNewInstance: (req) ->
2014-04-17 20:09:01 -04:00
model = new @ modelClass ( { } )
2014-08-28 22:39:46 -04:00
if @ modelClass . schema . is_patchable
watchers = [ req . user . get ( ' _id ' ) ]
if req . user . isAdmin ( ) # https://github.com/codecombat/codecombat/issues/1105
nick = mongoose . Types . ObjectId ( ' 512ef4805a67a8c507000001 ' )
watchers . push nick unless _ . find watchers , (id) -> id . equals nick
model . set ' watchers ' , watchers
2014-04-17 20:09:01 -04:00
model
2014-01-03 13:32:13 -05:00
validateDocumentInput: (input) ->
tv4 = require ( ' tv4 ' ) . tv4
res = tv4 . validateMultiple ( input , @ jsonSchema )
res
2014-03-03 19:19:35 -05:00
2014-04-24 14:08:42 -04:00
@isID: (id) -> _ . isString ( id ) and id . length is 24 and id . match ( /[a-f0-9]/gi ) ? . length is 24
2014-01-03 13:32:13 -05:00
2015-04-08 15:00:12 -04:00
getDocumentForIdOrSlug: (idOrSlug, projection, done) ->
unless done
done = projection # projection is optional argument
projection = null
2014-01-03 13:32:13 -05:00
idOrSlug = idOrSlug + ' '
2014-02-17 18:19:19 -05:00
if Handler . isID ( idOrSlug )
2015-04-08 15:00:12 -04:00
query = @ modelClass . findById ( idOrSlug )
2014-01-20 12:13:18 -05:00
else
2015-04-08 15:00:12 -04:00
query = @ modelClass . findOne { slug: idOrSlug }
query . select projection if projection
query . exec (err, document) ->
done ( err , document )
2014-01-03 13:32:13 -05:00
doWaterfallChecks: (req, document, done) ->
return done ( null , document ) unless @ waterfallFunctions . length
# waterfall doesn't let you pass an initial argument
# so wrap the first waterfall function to pass in the document
funcs = ( f for f in @ waterfallFunctions )
firstFunc = funcs [ 0 ]
wrapped = (func, r, doc) -> (callback) -> func ( r , doc , callback )
funcs [ 0 ] = wrapped ( firstFunc , req , document )
async . waterfall funcs , (err, rrr, document) ->
done ( err , document )
saveChangesToDocument: (req, document, done) ->
2014-04-01 15:13:11 -04:00
for prop in @ getEditableProperties req , document
if ( val = req . body [ prop ] ) ?
document . set prop , val
2014-04-02 02:16:50 -04:00
# Hold on, gotta think about that one
#else if document.get(prop)? and req.method isnt 'PATCH'
# document.set prop, 'undefined'
2014-01-03 13:32:13 -05:00
obj = document . toObject ( )
# Hack to get saving of Users to work. Probably should replace these props with strings
# so that validation doesn't get hung up on Date objects in the documents.
delete obj . dateCreated
validation = @ validateDocumentInput ( obj )
return done ( validation ) unless validation . valid
document . save (err) -> done ( err )
2014-04-18 14:17:13 -04:00
getPropertiesFromMultipleDocuments: (res, model, properties, ids) ->
query = model . find ( )
ids = ids . split ( ' , ' ) if _ . isString ids
ids = _ . uniq ids
for id in ids
return errors . badInput ( res , " Given an invalid id: #{ id } " ) unless Handler . isID ( id )
query . where ( { ' _id ' : { $in: ids } } )
query . select ( properties ) . exec (err, documents) ->
dict = { }
_ . each documents , (document) ->
dict [ document . id ] = document
res . send dict
res . end ( )
2014-06-09 11:28:35 -04:00
2014-08-08 11:14:57 -04:00
delete : (req, res) -> @ sendMethodNotAllowed res , ' DELETE not allowed. '
2014-06-09 11:28:35 -04:00
2014-08-08 11:14:57 -04:00
head: (req, res) -> @ sendMethodNotAllowed res , ' HEAD not allowed. '
2014-07-22 14:07:00 -04:00
# This is not a Mongoose user
projectionForUser: (req, model, ownerID) ->
2014-11-22 21:40:28 -05:00
return { } if ' privateProperties ' not of model or req . user ? . _id + ' ' is ownerID + ' ' or req . user . isAdmin ( )
2014-07-22 14:07:00 -04:00
projection = { }
projection [ field ] = 0 for field in model . privateProperties
projection