2015-12-16 20:09:22 -05:00
|
|
|
utils = require '../lib/utils'
|
|
|
|
errors = require '../commons/errors'
|
2016-04-06 13:56:06 -04:00
|
|
|
User = require '../models/User'
|
2015-12-16 20:09:22 -05:00
|
|
|
sendwithus = require '../sendwithus'
|
2016-03-18 20:05:21 -04:00
|
|
|
slack = require '../slack'
|
2015-12-16 20:09:22 -05:00
|
|
|
_ = require 'lodash'
|
|
|
|
wrap = require 'co-express'
|
|
|
|
mongoose = require 'mongoose'
|
2016-02-22 18:08:58 -05:00
|
|
|
database = require '../commons/database'
|
|
|
|
parse = require '../commons/parse'
|
2015-12-16 20:09:22 -05:00
|
|
|
|
2016-08-24 18:46:35 -04:00
|
|
|
# More info on database versioning: https://github.com/codecombat/codecombat/wiki/Versioning
|
|
|
|
|
2015-12-16 20:09:22 -05:00
|
|
|
module.exports =
|
|
|
|
postNewVersion: (Model, options={}) -> wrap (req, res) ->
|
2016-08-24 18:46:35 -04:00
|
|
|
# Find the document which is getting a new version
|
2016-02-22 18:08:58 -05:00
|
|
|
parent = yield database.getDocFromHandle(req, Model)
|
2015-12-16 20:09:22 -05:00
|
|
|
if not parent
|
|
|
|
throw new errors.NotFound('Parent not found.')
|
|
|
|
|
2016-08-24 18:46:35 -04:00
|
|
|
# Check permissions
|
|
|
|
# TODO: Figure out an encapsulated way to do this; it's more permissions than versioning
|
2015-12-16 20:09:22 -05:00
|
|
|
if options.hasPermissionsOrTranslations
|
|
|
|
permissions = options.hasPermissionsOrTranslations
|
|
|
|
permissions = [permissions] if _.isString(permissions)
|
|
|
|
permissions = ['admin'] if not _.isArray(permissions)
|
|
|
|
hasPermission = _.any(req.user?.hasPermission(permission) for permission in permissions)
|
2016-08-24 18:46:35 -04:00
|
|
|
if Model.schema.uses_coco_permissions and not hasPermission
|
|
|
|
hasPermission = parent.hasPermissionsForMethod(req.user, req.method)
|
2016-02-22 18:08:58 -05:00
|
|
|
if not (hasPermission or database.isJustFillingTranslations(req, parent))
|
2015-12-16 20:09:22 -05:00
|
|
|
throw new errors.Forbidden()
|
|
|
|
|
2016-08-24 18:46:35 -04:00
|
|
|
# Create the new version, a clone of the parent with POST data applied
|
2016-02-22 18:08:58 -05:00
|
|
|
doc = database.initDoc(req, Model)
|
2015-12-16 20:09:22 -05:00
|
|
|
ATTRIBUTES_NOT_INHERITED = ['_id', 'version', 'created', 'creator']
|
|
|
|
doc.set(_.omit(parent.toObject(), ATTRIBUTES_NOT_INHERITED))
|
2016-02-22 18:08:58 -05:00
|
|
|
database.assignBody(req, doc, { unsetMissing: true })
|
2015-12-16 20:09:22 -05:00
|
|
|
|
2016-08-24 18:46:35 -04:00
|
|
|
# Get latest (minor or major) version. This may not be the same document (or same major version) as parent.
|
|
|
|
latestSelect = 'version index slug'
|
2015-12-16 20:09:22 -05:00
|
|
|
major = req.body.version?.major
|
|
|
|
original = parent.get('original')
|
|
|
|
if _.isNumber(major)
|
|
|
|
q1 = Model.findOne({original: original, 'version.isLatestMinor': true, 'version.major': major})
|
|
|
|
else
|
|
|
|
q1 = Model.findOne({original: original, 'version.isLatestMajor': true})
|
2016-08-24 18:46:35 -04:00
|
|
|
q1.select latestSelect
|
2015-12-16 20:09:22 -05:00
|
|
|
latest = yield q1.exec()
|
|
|
|
|
2016-08-24 18:46:35 -04:00
|
|
|
# Handle the case where no version is marked as latest, since making new
|
|
|
|
# versions is not atomic
|
2015-12-16 20:09:22 -05:00
|
|
|
if not latest
|
|
|
|
if _.isNumber(major)
|
|
|
|
q2 = Model.findOne({original: original, 'version.major': major})
|
|
|
|
q2.sort({'version.minor': -1})
|
|
|
|
else
|
|
|
|
q2 = Model.findOne()
|
|
|
|
q2.sort({'version.major': -1, 'version.minor': -1})
|
2016-08-24 18:46:35 -04:00
|
|
|
q2.select(latestSelect)
|
2015-12-16 20:09:22 -05:00
|
|
|
latest = yield q2.exec()
|
|
|
|
if not latest
|
|
|
|
throw new errors.NotFound('Previous version not found.')
|
|
|
|
|
2016-08-24 18:46:35 -04:00
|
|
|
# Update the latest version, making it no longer the latest. This includes
|
2015-12-16 20:09:22 -05:00
|
|
|
major = req.body.version?.major
|
|
|
|
version = _.clone(latest.get('version'))
|
|
|
|
wasLatestMajor = version.isLatestMajor
|
|
|
|
version.isLatestMajor = false
|
|
|
|
if _.isNumber(major)
|
|
|
|
version.isLatestMinor = false
|
2016-08-24 18:46:35 -04:00
|
|
|
raw = yield latest.update({$set: {version: version}, $unset: {index: 1, slug: 1}})
|
2015-12-16 20:09:22 -05:00
|
|
|
if not raw.nModified
|
|
|
|
console.error('Conditions', conditions)
|
|
|
|
console.error('Doc', doc)
|
|
|
|
console.error('Raw response', raw)
|
|
|
|
throw new errors.InternalServerError('Latest version could not be modified.')
|
|
|
|
|
|
|
|
# update the new doc with version, index information
|
|
|
|
# Relying heavily on Mongoose schema default behavior here. TODO: Make explicit?
|
|
|
|
if _.isNumber(major)
|
|
|
|
doc.set({
|
|
|
|
'version.major': latest.version.major
|
|
|
|
'version.minor': latest.version.minor + 1
|
|
|
|
'version.isLatestMajor': wasLatestMajor
|
|
|
|
})
|
|
|
|
if wasLatestMajor
|
|
|
|
doc.set('index', true)
|
|
|
|
else
|
|
|
|
doc.set({index: undefined, slug: undefined})
|
|
|
|
else
|
|
|
|
doc.set('version.major', latest.version.major + 1)
|
|
|
|
doc.set('index', true)
|
|
|
|
|
|
|
|
doc.set('parent', latest._id)
|
|
|
|
|
2016-08-24 18:46:35 -04:00
|
|
|
try
|
|
|
|
doc = yield doc.save()
|
|
|
|
catch e
|
|
|
|
# Revert changes to latest doc made earlier, should set everything back to normal
|
|
|
|
yield latest.update({$set: _.pick(latest.toObject(), 'version', 'index', 'slug')})
|
|
|
|
throw e
|
2015-12-16 20:09:22 -05:00
|
|
|
|
|
|
|
editPath = req.headers['x-current-path']
|
|
|
|
docLink = "http://codecombat.com#{editPath}"
|
|
|
|
|
2016-03-18 20:05:21 -04:00
|
|
|
# Post a message on Slack
|
2016-03-22 12:51:18 -04:00
|
|
|
message = "#{req.user.get('name')} saved a change to #{doc.get('name')}: #{doc.get('commitMessage') or '(no commit message)'} #{docLink}"
|
2016-06-27 12:49:07 -04:00
|
|
|
slack.sendSlackMessage message, ['artisans']
|
2015-12-16 20:09:22 -05:00
|
|
|
|
|
|
|
# Send emails to watchers
|
|
|
|
watchers = doc.get('watchers') or []
|
|
|
|
# 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(req.user.get('_id')) and not (w + '' in ['512ef4805a67a8c507000001', '5162fab9c92b4c751e000274', '51538fdb812dd9af02000001']))
|
|
|
|
if watchers.length
|
|
|
|
User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) ->
|
|
|
|
for watcher in watchers
|
2016-07-13 19:50:03 -04:00
|
|
|
continue if not watcher.get('email')
|
2015-12-16 20:09:22 -05:00
|
|
|
context =
|
2016-06-06 19:53:05 -04:00
|
|
|
email_id: sendwithus.templates.change_made_notify_watcher
|
2015-12-16 20:09:22 -05:00
|
|
|
recipient:
|
|
|
|
address: watcher.get('email')
|
|
|
|
name: watcher.get('name')
|
|
|
|
email_data:
|
|
|
|
doc_name: doc.get('name') or '???'
|
|
|
|
submitter_name: req.user.get('name') or '???'
|
|
|
|
doc_link: if editPath then docLink else null
|
|
|
|
commit_message: doc.get('commitMessage')
|
|
|
|
sendwithus.api.send context, _.noop
|
|
|
|
|
|
|
|
res.status(201).send(doc.toObject())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getLatestVersion: (Model, options={}) -> wrap (req, res) ->
|
|
|
|
# can get latest overall version, latest of a major version, or a specific version
|
|
|
|
original = req.params.handle
|
|
|
|
version = req.params.version
|
2016-02-22 18:08:58 -05:00
|
|
|
if not database.isID(original)
|
2016-06-06 19:53:05 -04:00
|
|
|
throw new errors.UnprocessableEntity('Invalid MongoDB id: '+original)
|
2015-12-16 20:09:22 -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)
|
|
|
|
dbq = Model.findOne(query)
|
|
|
|
|
|
|
|
dbq.sort({ 'version.major': -1, 'version.minor': -1 })
|
|
|
|
|
|
|
|
# Make sure that permissions and version are fetched, but not sent back if they didn't ask for them.
|
2016-02-22 18:08:58 -05:00
|
|
|
projection = parse.getProjectFromReq(req)
|
2015-12-16 20:09:22 -05:00
|
|
|
if projection
|
|
|
|
extraProjectionProps = []
|
|
|
|
extraProjectionProps.push 'permissions' unless projection.permissions
|
|
|
|
extraProjectionProps.push 'version' unless projection.version
|
|
|
|
projection.permissions = 1
|
|
|
|
projection.version = 1
|
|
|
|
dbq.select(projection)
|
|
|
|
|
|
|
|
doc = yield dbq.exec()
|
|
|
|
throw new errors.NotFound() if not doc
|
2016-02-22 18:08:58 -05:00
|
|
|
throw new errors.Forbidden() unless database.hasAccessToDocument(req, doc)
|
2015-12-16 20:09:22 -05:00
|
|
|
doc = _.omit doc, extraProjectionProps if extraProjectionProps?
|
|
|
|
|
|
|
|
res.status(200).send(doc.toObject())
|
|
|
|
|
|
|
|
|
|
|
|
versions: (Model, options={}) -> wrap (req, res) ->
|
|
|
|
original = req.params.handle
|
|
|
|
dbq = Model.find({'original': mongoose.Types.ObjectId(original)})
|
|
|
|
dbq.sort({'created': -1})
|
2016-02-22 18:08:58 -05:00
|
|
|
dbq.limit(parse.getLimitFromReq(req))
|
|
|
|
dbq.skip(parse.getSkipFromReq(req))
|
|
|
|
dbq.select(parse.getProjectFromReq(req) or 'slug name version commitMessage created creator permissions')
|
2015-12-16 20:09:22 -05:00
|
|
|
|
|
|
|
results = yield dbq.exec()
|
|
|
|
res.status(200).send(results)
|