mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 00:31:21 -05:00
ae82875c57
When a new version is created, the latest version is updated, then the new one is made. If making a new one fails (most commonly due to a name conflict), the latest version is left in a broken state. Set up the new middleware to revert changes to latest version in this case, and update the level handler to use the middleware. Also added warning logs if models do not have editableProperties or postEditableProperties set.
179 lines
7.3 KiB
CoffeeScript
179 lines
7.3 KiB
CoffeeScript
utils = require '../lib/utils'
|
|
errors = require '../commons/errors'
|
|
User = require '../models/User'
|
|
sendwithus = require '../sendwithus'
|
|
slack = require '../slack'
|
|
_ = require 'lodash'
|
|
wrap = require 'co-express'
|
|
mongoose = require 'mongoose'
|
|
database = require '../commons/database'
|
|
parse = require '../commons/parse'
|
|
|
|
# More info on database versioning: https://github.com/codecombat/codecombat/wiki/Versioning
|
|
|
|
module.exports =
|
|
postNewVersion: (Model, options={}) -> wrap (req, res) ->
|
|
# Find the document which is getting a new version
|
|
parent = yield database.getDocFromHandle(req, Model)
|
|
if not parent
|
|
throw new errors.NotFound('Parent not found.')
|
|
|
|
# Check permissions
|
|
# TODO: Figure out an encapsulated way to do this; it's more permissions than versioning
|
|
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)
|
|
if Model.schema.uses_coco_permissions and not hasPermission
|
|
hasPermission = parent.hasPermissionsForMethod(req.user, req.method)
|
|
if not (hasPermission or database.isJustFillingTranslations(req, parent))
|
|
throw new errors.Forbidden()
|
|
|
|
# Create the new version, a clone of the parent with POST data applied
|
|
doc = database.initDoc(req, Model)
|
|
ATTRIBUTES_NOT_INHERITED = ['_id', 'version', 'created', 'creator']
|
|
doc.set(_.omit(parent.toObject(), ATTRIBUTES_NOT_INHERITED))
|
|
database.assignBody(req, doc, { unsetMissing: true })
|
|
|
|
# Get latest (minor or major) version. This may not be the same document (or same major version) as parent.
|
|
latestSelect = 'version index slug'
|
|
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})
|
|
q1.select latestSelect
|
|
latest = yield q1.exec()
|
|
|
|
# Handle the case where no version is marked as latest, since making new
|
|
# versions is not atomic
|
|
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})
|
|
q2.select(latestSelect)
|
|
latest = yield q2.exec()
|
|
if not latest
|
|
throw new errors.NotFound('Previous version not found.')
|
|
|
|
# Update the latest version, making it no longer the latest. This includes
|
|
major = req.body.version?.major
|
|
version = _.clone(latest.get('version'))
|
|
wasLatestMajor = version.isLatestMajor
|
|
version.isLatestMajor = false
|
|
if _.isNumber(major)
|
|
version.isLatestMinor = false
|
|
raw = yield latest.update({$set: {version: version}, $unset: {index: 1, slug: 1}})
|
|
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)
|
|
|
|
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
|
|
|
|
editPath = req.headers['x-current-path']
|
|
docLink = "http://codecombat.com#{editPath}"
|
|
|
|
# Post a message on Slack
|
|
message = "#{req.user.get('name')} saved a change to #{doc.get('name')}: #{doc.get('commitMessage') or '(no commit message)'} #{docLink}"
|
|
slack.sendSlackMessage message, ['artisans']
|
|
|
|
# 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
|
|
continue if not watcher.get('email')
|
|
context =
|
|
email_id: sendwithus.templates.change_made_notify_watcher
|
|
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
|
|
if not database.isID(original)
|
|
throw new errors.UnprocessableEntity('Invalid MongoDB id: '+original)
|
|
|
|
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.
|
|
projection = parse.getProjectFromReq(req)
|
|
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
|
|
throw new errors.Forbidden() unless database.hasAccessToDocument(req, doc)
|
|
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})
|
|
dbq.limit(parse.getLimitFromReq(req))
|
|
dbq.skip(parse.getSkipFromReq(req))
|
|
dbq.select(parse.getProjectFromReq(req) or 'slug name version commitMessage created creator permissions')
|
|
|
|
results = yield dbq.exec()
|
|
res.status(200).send(results)
|