mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-02 11:58:10 -05:00
be1c1323ac
* Partially revert GET /db/:collection/:handle/patches, as it returned limited results for versioned docs * Fix POST /db/:collection/:handle/patch to: * normalize diffs based on latest doc version * handle empty deltas * not swallow thrown errors due to _.curry * Set 'target.original' correctly for versioned collections
170 lines
6.6 KiB
CoffeeScript
170 lines
6.6 KiB
CoffeeScript
utils = require '../lib/utils'
|
|
errors = require '../commons/errors'
|
|
wrap = require 'co-express'
|
|
Promise = require 'bluebird'
|
|
Patch = require '../models/Patch'
|
|
User = require '../models/User'
|
|
mongoose = require 'mongoose'
|
|
database = require '../commons/database'
|
|
parse = require '../commons/parse'
|
|
slack = require '../slack'
|
|
{ isJustFillingTranslations } = require '../commons/deltas'
|
|
{ updateI18NCoverage } = require '../commons/i18n'
|
|
{ initNewVersion, saveNewVersion } = require '../middleware/versions'
|
|
{ notifyChangesMadeToDoc } = require '../commons/notify'
|
|
|
|
module.exports =
|
|
|
|
patches: (Model, options={}) -> wrap (req, res) ->
|
|
dbq = Patch.find()
|
|
dbq.limit(parse.getLimitFromReq(req))
|
|
dbq.skip(parse.getSkipFromReq(req))
|
|
dbq.select(parse.getProjectFromReq(req))
|
|
|
|
id = req.params.handle
|
|
if not database.isID(id)
|
|
# handle slug
|
|
doc = yield database.getDocFromHandle(req, Model, {_id: 1})
|
|
if not doc
|
|
throw new errors.NotFound('Patchable document not found')
|
|
id = (doc.get('original') or doc.id) + ''
|
|
|
|
query =
|
|
$or: [
|
|
{'target.original': id+''}
|
|
{'target.original': mongoose.Types.ObjectId(id)}
|
|
]
|
|
if req.query.status
|
|
query.status = req.query.status
|
|
if req.user and req.query.creator is req.user.id
|
|
query.creator = req.user._id
|
|
|
|
patches = yield dbq.find(query).sort('-created')
|
|
res.status(200).send(patches)
|
|
|
|
|
|
joinWatchers: (Model, options={}) -> wrap (req, res) ->
|
|
doc = yield database.getDocFromHandle(req, Model)
|
|
if not doc
|
|
throw new errors.NotFound('Document not found.')
|
|
if not database.hasAccessToDocument(req, doc, 'get')
|
|
throw new errors.Forbidden()
|
|
updateResult = yield doc.update({ $addToSet: { watchers: req.user.get('_id') }})
|
|
if updateResult.nModified
|
|
watchers = doc.get('watchers')
|
|
watchers.push(req.user.get('_id'))
|
|
doc.set('watchers', watchers)
|
|
res.status(200).send(doc)
|
|
|
|
|
|
leaveWatchers: (Model, options={}) -> wrap (req, res) ->
|
|
doc = yield database.getDocFromHandle(req, Model)
|
|
if not doc
|
|
throw new errors.NotFound('Document not found.')
|
|
updateResult = yield doc.update({ $pull: { watchers: req.user.get('_id') }})
|
|
if updateResult.nModified
|
|
watchers = doc.get('watchers')
|
|
watchers = _.filter watchers, (id) -> not id.equals(req.user._id)
|
|
doc.set('watchers', watchers)
|
|
res.status(200).send(doc)
|
|
|
|
|
|
postPatch: (Model, collectionName) -> wrap (req, res) ->
|
|
# handle either "POST /db/<collection>/:handle/patch" or "POST /db/patch" with target included in body
|
|
# Tried currying the function, but it didn't play nice with the generator function.
|
|
if req.params.handle
|
|
target = yield database.getDocFromHandle(req, Model, {getLatest:true})
|
|
else if req.body.target?.id
|
|
target = yield Model.findById(req.body.target.id)
|
|
if Model.schema.uses_coco_versions and target and not target.get('version.isLatestMajor')
|
|
original = target.get('original')
|
|
target = yield Model.findOne({original}).sort({ 'version.major': -1, 'version.minor': -1 })
|
|
if not target
|
|
throw new errors.NotFound('Target not found.')
|
|
|
|
# normalize the delta because in tests, changes to patches would sneak in and cause false positives
|
|
# TODO: Figure out a better system. Perhaps submit a series of paths? I18N Edit Views already use them for their rows.
|
|
originalDelta = req.body.delta
|
|
originalTarget = target.toObject()
|
|
# _.cloneDeep can't handle ObjectIds, and be careful with Dates, too.
|
|
newTargetAttrs = _.cloneDeep(target.toObject(), (value) ->
|
|
return value if value instanceof mongoose.Types.ObjectId
|
|
return value if value instanceof Date
|
|
return undefined
|
|
)
|
|
if _.isEmpty(originalDelta)
|
|
throw new errors.UnprocessableEntity('Delta given is empty.')
|
|
jsondiffpatch.patch(newTargetAttrs, originalDelta)
|
|
normalizedDelta = jsondiffpatch.diff(originalTarget, newTargetAttrs)
|
|
normalizedDelta = _.pick(normalizedDelta, _.keys(originalDelta))
|
|
if _.isEmpty(normalizedDelta)
|
|
throw new errors.UnprocessableEntity('Normalized delta is empty.')
|
|
|
|
# decide whether the patch should be auto-accepted, or left 'pending' for an admin or artisan to review
|
|
reasonNotAutoAccepted = undefined
|
|
validation = tv4.validateMultiple(newTargetAttrs, Model.jsonSchema)
|
|
if not validation.valid
|
|
reasonNotAutoAccepted = 'Did not pass json schema.'
|
|
else if not isJustFillingTranslations(normalizedDelta)
|
|
reasonNotAutoAccepted = 'Adding to existing translations.'
|
|
|
|
else
|
|
# save changes directly to the target, whether versioned or not, and send out notifications
|
|
if Model.schema.uses_coco_versions
|
|
newVersion = database.initDoc(req, Model)
|
|
initNewVersion(newVersion, target)
|
|
newVersionAttrs = newVersion.toObject()
|
|
jsondiffpatch.patch(newVersionAttrs, normalizedDelta)
|
|
newVersion.set(newVersionAttrs)
|
|
newVersion.set('commitMessage', req.body.commitMessage)
|
|
major = target.get('version.major')
|
|
updateI18NCoverage(newVersion)
|
|
yield saveNewVersion(newVersion, major)
|
|
target = newVersion
|
|
else
|
|
target.set(newTargetAttrs)
|
|
updateI18NCoverage(target)
|
|
yield target.save()
|
|
notifyChangesMadeToDoc(req, target)
|
|
|
|
# create, validate and save the patch
|
|
if Model.schema.uses_coco_versions
|
|
patchTarget = {
|
|
collection: collectionName
|
|
id: target._id
|
|
original: target.get('original')
|
|
version: _.pick(target.get('version'), 'major', 'minor')
|
|
}
|
|
else
|
|
patchTarget = {
|
|
collection: collectionName
|
|
id: target._id
|
|
original: target._id
|
|
}
|
|
|
|
patch = new Patch()
|
|
patch.set({
|
|
delta: normalizedDelta
|
|
commitMessage: req.body.commitMessage
|
|
target: patchTarget
|
|
creator: req.user._id
|
|
status: if reasonNotAutoAccepted then 'pending' else 'accepted'
|
|
created: new Date().toISOString()
|
|
reasonNotAutoAccepted: reasonNotAutoAccepted
|
|
})
|
|
database.validateDoc(patch)
|
|
|
|
# add this patch to the denormalized list of patches on the target
|
|
if reasonNotAutoAccepted
|
|
yield target.update({ $addToSet: { patches: patch._id }})
|
|
|
|
yield patch.save()
|
|
|
|
User.incrementStat req.user.id, 'stats.patchesSubmitted'
|
|
|
|
res.status(201).send(patch.toObject({req: req}))
|
|
|
|
if reasonNotAutoAccepted
|
|
docLink = "https://codecombat.com/editor/#{collectionName}/#{target.id}"
|
|
message = "#{req.user.get('name')} submitted a patch to #{target.get('name')}: #{patch.get('commitMessage')} #{docLink}"
|
|
slack.sendSlackMessage message, ['artisans']
|