All edits are now tracked
intermediate intermediate
This commit is contained in:
parent
b951205681
commit
fce9f0031b
13 changed files with 146 additions and 51 deletions
app
server
articles
levels
patches
plugins
users
test/server
|
@ -1,4 +1,4 @@
|
||||||
CocoClass = require 'lib/CocoClass'
|
CocoClass = require './CocoClass'
|
||||||
|
|
||||||
namesCache = {}
|
namesCache = {}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
SystemNameLoader = require 'lib/SystemNameLoader'
|
SystemNameLoader = require './SystemNameLoader'
|
||||||
###
|
###
|
||||||
Good-to-knows:
|
Good-to-knows:
|
||||||
dataPath: an array of keys that walks you up a JSON object that's being patched
|
dataPath: an array of keys that walks you up a JSON object that's being patched
|
||||||
|
@ -10,7 +10,6 @@ SystemNameLoader = require 'lib/SystemNameLoader'
|
||||||
module.exports.expandDelta = (delta, left, schema) ->
|
module.exports.expandDelta = (delta, left, schema) ->
|
||||||
flattenedDeltas = flattenDelta(delta)
|
flattenedDeltas = flattenDelta(delta)
|
||||||
(expandFlattenedDelta(fd, left, schema) for fd in flattenedDeltas)
|
(expandFlattenedDelta(fd, left, schema) for fd in flattenedDeltas)
|
||||||
|
|
||||||
|
|
||||||
flattenDelta = (delta, dataPath=null, deltaPath=null) ->
|
flattenDelta = (delta, dataPath=null, deltaPath=null) ->
|
||||||
# takes a single jsondiffpatch delta and returns an array of objects with
|
# takes a single jsondiffpatch delta and returns an array of objects with
|
||||||
|
@ -27,9 +26,7 @@ flattenDelta = (delta, dataPath=null, deltaPath=null) ->
|
||||||
results = results.concat flattenDelta(
|
results = results.concat flattenDelta(
|
||||||
childDelta, dataPath.concat([dataIndex]), deltaPath.concat([deltaIndex]))
|
childDelta, dataPath.concat([dataIndex]), deltaPath.concat([deltaIndex]))
|
||||||
results
|
results
|
||||||
|
pandFlattenedDelta = (delta, left, schema) ->
|
||||||
|
|
||||||
expandFlattenedDelta = (delta, left, schema) ->
|
|
||||||
# takes a single flattened delta and converts into an object that can be
|
# takes a single flattened delta and converts into an object that can be
|
||||||
# easily formatted into something human readable.
|
# easily formatted into something human readable.
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ PatchSchema = c.object({title:'Patch', required:['target', 'delta', 'commitMessa
|
||||||
major: { type: 'number', minimum: 0 }
|
major: { type: 'number', minimum: 0 }
|
||||||
minor: { type: 'number', minimum: 0 }
|
minor: { type: 'number', minimum: 0 }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
_wasPending: type: 'boolean'
|
||||||
})
|
})
|
||||||
|
|
||||||
c.extendBasicProperties(PatchSchema, 'patch')
|
c.extendBasicProperties(PatchSchema, 'patch')
|
||||||
|
|
|
@ -160,7 +160,13 @@ UserSchema = c.object {},
|
||||||
data: c.object {description: "Cached LinkedIn data slurped from profile.", additionalProperties: true}
|
data: c.object {description: "Cached LinkedIn data slurped from profile.", additionalProperties: true}
|
||||||
points: {type:'number'}
|
points: {type:'number'}
|
||||||
activity: {type: 'object', description: 'Summary statistics about user activity', additionalProperties: c.activity}
|
activity: {type: 'object', description: 'Summary statistics about user activity', additionalProperties: c.activity}
|
||||||
|
stats: c.object {additionalProperties: true}, # TODO set to false after dev
|
||||||
|
gamesCompleted: type: 'integer'
|
||||||
|
articleEdits: type: 'integer'
|
||||||
|
levelEdits: type: 'integer'
|
||||||
|
levelSystemEdits: type: 'integer'
|
||||||
|
levelComponentEdits: type: 'integer'
|
||||||
|
thangTypeEdits: type: 'integer'
|
||||||
|
|
||||||
|
|
||||||
c.extendBasicProperties UserSchema, 'user'
|
c.extendBasicProperties UserSchema, 'user'
|
||||||
|
|
|
@ -8,16 +8,4 @@ ArticleSchema.plugin(plugins.VersionedPlugin)
|
||||||
ArticleSchema.plugin(plugins.SearchablePlugin, {searchable: ['body', 'name']})
|
ArticleSchema.plugin(plugins.SearchablePlugin, {searchable: ['body', 'name']})
|
||||||
ArticleSchema.plugin(plugins.PatchablePlugin)
|
ArticleSchema.plugin(plugins.PatchablePlugin)
|
||||||
|
|
||||||
# Assumes every article save is a new version
|
|
||||||
ArticleSchema.pre 'save', (next) ->
|
|
||||||
return next() unless @get('creator')
|
|
||||||
User = require '../users/User' # Avoid mutual inclusion cycles
|
|
||||||
|
|
||||||
userID = @get('creator').toHexString()
|
|
||||||
User.update {_id: userID}, {$inc: 'stats.articleEdits': 1}, {}, (err, docs) ->
|
|
||||||
log.error err if err?
|
|
||||||
|
|
||||||
next()
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = mongoose.model('article', ArticleSchema)
|
module.exports = mongoose.model('article', ArticleSchema)
|
||||||
|
|
|
@ -21,16 +21,5 @@ LevelSchema.pre 'init', (next) ->
|
||||||
LevelSchema.post 'init', (doc) ->
|
LevelSchema.post 'init', (doc) ->
|
||||||
if _.isString(doc.get('nextLevel'))
|
if _.isString(doc.get('nextLevel'))
|
||||||
doc.set('nextLevel', undefined)
|
doc.set('nextLevel', undefined)
|
||||||
|
|
||||||
# Assumes every level save is a new level
|
|
||||||
LevelSchema.pre 'save', (next) ->
|
|
||||||
return next() unless @get('creator')
|
|
||||||
User = require '../users/User' # Avoid mutual inclusion cycles
|
|
||||||
|
|
||||||
userID = @get('creator').toHexString()
|
|
||||||
User.update {_id: userID}, {$inc: 'stats.levelEdits': 1}, {}, (err, docs) ->
|
|
||||||
log.error err if err?
|
|
||||||
|
|
||||||
next()
|
|
||||||
|
|
||||||
module.exports = Level = mongoose.model('level', LevelSchema)
|
module.exports = Level = mongoose.model('level', LevelSchema)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
mongoose = require('mongoose')
|
mongoose = require('mongoose')
|
||||||
|
deltas = require '../../app/lib/deltas'
|
||||||
{handlers} = require '../commons/mapping'
|
{handlers} = require '../commons/mapping'
|
||||||
|
|
||||||
PatchSchema = new mongoose.Schema({}, {strict: false})
|
PatchSchema = new mongoose.Schema({status: String}, {strict: false})
|
||||||
|
|
||||||
PatchSchema.pre 'save', (next) ->
|
PatchSchema.pre 'save', (next) ->
|
||||||
return next() unless @isNew # patch can't be altered after creation, so only need to check data once
|
return next() unless @isNew # patch can't be altered after creation, so only need to check data once
|
||||||
|
@ -45,4 +46,31 @@ PatchSchema.pre 'save', (next) ->
|
||||||
@targetLoaded = document
|
@targetLoaded = document
|
||||||
document.save (err) -> next(err)
|
document.save (err) -> next(err)
|
||||||
|
|
||||||
|
PatchSchema.methods.isTranslationPatch = ->
|
||||||
|
console.log @get 'delta'
|
||||||
|
expanded = deltas.expandDelta @get('delta')
|
||||||
|
console.log 'expanded'
|
||||||
|
_.some expanded, (delta) -> 'i18n' in expanded.dataPath
|
||||||
|
|
||||||
|
PatchSchema.methods.isMiscPatch = ->
|
||||||
|
expanded = deltas.expandDelta @get 'delta'
|
||||||
|
_.some expanded, (delta) -> 'i18n' not in expanded.dataPath
|
||||||
|
|
||||||
|
# Keep track of when a patch is pending. Accepted patches can be rejected still.
|
||||||
|
PatchSchema.path('status').set (newVal) ->
|
||||||
|
@set '_wasPending', @status is 'pending' and newVal isnt 'pending'
|
||||||
|
newVal
|
||||||
|
|
||||||
|
PatchSchema.pre 'save', (next) ->
|
||||||
|
User = require '../users/User'
|
||||||
|
userID = @get('creator').toHexString()
|
||||||
|
|
||||||
|
if @get('status') is 'accepted'
|
||||||
|
User.incrementStat userID, 'stats.patchesContributed' # accepted patches
|
||||||
|
else if @get('status') is 'pending'
|
||||||
|
User.incrementStat userID, 'stats.patchesSubmitted' # submitted patches
|
||||||
|
|
||||||
|
next()
|
||||||
|
|
||||||
|
|
||||||
module.exports = mongoose.model('patch', PatchSchema)
|
module.exports = mongoose.model('patch', PatchSchema)
|
||||||
|
|
|
@ -48,11 +48,18 @@ PatchHandler = class PatchHandler extends Handler
|
||||||
|
|
||||||
if newStatus is 'withdrawn'
|
if newStatus is 'withdrawn'
|
||||||
return @sendUnauthorizedError(res) unless req.user.get('_id').equals patch.get('creator')
|
return @sendUnauthorizedError(res) unless req.user.get('_id').equals patch.get('creator')
|
||||||
|
|
||||||
|
# newly accepted
|
||||||
|
if newStatus is 'accepted' and patch.get '_wasPending'
|
||||||
|
accepter = req.user.get 'id'
|
||||||
|
User.incrementStat accepter, 'stats.'
|
||||||
|
|
||||||
# these require callbacks
|
# these require callbacks
|
||||||
patch.update {$set:{status:newStatus}}, {}, ->
|
patch.set 'status', newStatus
|
||||||
target.update {$pull:{patches:patch.get('_id')}}, {}, ->
|
patch.save (err) =>
|
||||||
@sendSuccess(res, null)
|
log.error err if err?
|
||||||
|
target.update {$pull:{patches:patch.get('_id')}}, {}, ->
|
||||||
|
@sendSuccess(res, null)
|
||||||
|
|
||||||
onPostSuccess: (req, doc) ->
|
onPostSuccess: (req, doc) ->
|
||||||
log.error "Error sending patch created: could not find the loaded target on the patch object." unless doc.targetLoaded
|
log.error "Error sending patch created: could not find the loaded target on the patch object." unless doc.targetLoaded
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mongoose = require('mongoose')
|
mongoose = require('mongoose')
|
||||||
textSearch = require('mongoose-text-search')
|
textSearch = require('mongoose-text-search')
|
||||||
|
log = require 'winston'
|
||||||
|
|
||||||
module.exports.MigrationPlugin = (schema, migrations) ->
|
module.exports.MigrationPlugin = (schema, migrations) ->
|
||||||
# Property name migrations made EZ
|
# Property name migrations made EZ
|
||||||
|
@ -256,13 +257,14 @@ module.exports.VersionedPlugin = (schema) ->
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Assume ever save is a new version, hence an edit
|
||||||
schema.pre 'save', (next) ->
|
schema.pre 'save', (next) ->
|
||||||
return next() unless @get('creator')
|
|
||||||
User = require '../users/User' # Avoid mutual inclusion cycles
|
User = require '../users/User' # Avoid mutual inclusion cycles
|
||||||
|
userID = @get('creator')?.toHexString()
|
||||||
|
return next() unless userID?
|
||||||
|
|
||||||
userID = @get('creator').toHexString()
|
statName = User.statsMapping.edits[@constructor.modelName]
|
||||||
User.update {_id: userID}, {$inc: 'stats.levelEdits': 1}, {}, (err, docs) ->
|
User.incrementStat userID, statName
|
||||||
log.error err if err?
|
|
||||||
|
|
||||||
next()
|
next()
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,19 @@ UserSchema.statics.updateMailChimp = (doc, callback) ->
|
||||||
|
|
||||||
mc?.lists.subscribe params, onSuccess, onFailure
|
mc?.lists.subscribe params, onSuccess, onFailure
|
||||||
|
|
||||||
|
UserSchema.statics.statsMapping =
|
||||||
|
edits:
|
||||||
|
article: 'stats.articleEdits'
|
||||||
|
level: 'stats.levelEdits'
|
||||||
|
'level.component': 'stats.levelComponentEdits'
|
||||||
|
'level.system': 'stats.levelSystemEdits'
|
||||||
|
'thang.type': 'stats.thangTypeEdits'
|
||||||
|
|
||||||
|
UserSchema.statics.incrementStat = (id, statName, done, inc=1) ->
|
||||||
|
update = $inc: {}
|
||||||
|
update.$inc[statName] = inc
|
||||||
|
@update {_id:id}, update, {}, (err) ->
|
||||||
|
done err if done?
|
||||||
|
|
||||||
UserSchema.pre('save', (next) ->
|
UserSchema.pre('save', (next) ->
|
||||||
@set('emailLower', @get('email')?.toLowerCase())
|
@set('emailLower', @get('email')?.toLowerCase())
|
||||||
|
|
|
@ -375,7 +375,9 @@ UserHandler = class UserHandler extends Handler
|
||||||
return @sendNotFoundError res unless remark?
|
return @sendNotFoundError res unless remark?
|
||||||
@sendSuccess res, remark
|
@sendSuccess res, remark
|
||||||
|
|
||||||
countEdits = (model, statKey, done) ->
|
countEdits = (model, done) ->
|
||||||
|
statKey = User.statsMapping.edits[model.modelName]
|
||||||
|
return done(new Error 'Could not resolve statKey for model') unless statKey?
|
||||||
User.find {}, (err, users) ->
|
User.find {}, (err, users) ->
|
||||||
async.eachSeries users, ((user, doneWithUser) ->
|
async.eachSeries users, ((user, doneWithUser) ->
|
||||||
userID = user.get('_id').toHexString()
|
userID = user.get('_id').toHexString()
|
||||||
|
@ -409,23 +411,23 @@ UserHandler = class UserHandler extends Handler
|
||||||
|
|
||||||
articleEdits: (done) ->
|
articleEdits: (done) ->
|
||||||
Article = require '../articles/Article'
|
Article = require '../articles/Article'
|
||||||
countEdits Article, 'stats.articleEdits', done
|
countEdits Article, done
|
||||||
|
|
||||||
levelEdits: (done) ->
|
levelEdits: (done) ->
|
||||||
Level = require '../levels/Level'
|
Level = require '../levels/Level'
|
||||||
countEdits Level, 'stats.levelEdits', done
|
countEdits Level, done
|
||||||
|
|
||||||
levelComponentEdits: (done) ->
|
levelComponentEdits: (done) ->
|
||||||
LevelComponent = require '../levels/components/LevelComponent'
|
LevelComponent = require '../levels/components/LevelComponent'
|
||||||
countEdits LevelComponent, 'stats.levelComponentEdits', done
|
countEdits LevelComponent, done
|
||||||
|
|
||||||
levelSystemEdits: (done) ->
|
levelSystemEdits: (done) ->
|
||||||
LevelSystem = require '../levels/systems/LevelSystem'
|
LevelSystem = require '../levels/systems/LevelSystem'
|
||||||
countEdits LevelSystem, 'stats.levelSystemEdits', done
|
countEdits LevelSystem, done
|
||||||
|
|
||||||
thangTypeEdits: (done) ->
|
thangTypeEdits: (done) ->
|
||||||
ThangType = require '../levels/thangs/ThangType'
|
ThangType = require '../levels/thangs/ThangType'
|
||||||
countEdits ThangType, 'stats.thangTypeEdits', done
|
countEdits ThangType, done
|
||||||
|
|
||||||
|
|
||||||
recalculate: (req, res, statName) ->
|
recalculate: (req, res, statName) ->
|
||||||
|
|
|
@ -272,6 +272,10 @@ describe 'GET /db/user', ->
|
||||||
describe 'Statistics', ->
|
describe 'Statistics', ->
|
||||||
LevelSession = require '../../../server/levels/sessions/LevelSession'
|
LevelSession = require '../../../server/levels/sessions/LevelSession'
|
||||||
Article = require '../../../server/articles/Article'
|
Article = require '../../../server/articles/Article'
|
||||||
|
Level = require '../../../server/levels/Level'
|
||||||
|
LevelSystem = require '../../../server/levels/systems/LevelSystem'
|
||||||
|
LevelComponent = require '../../../server/levels/components/LevelComponent'
|
||||||
|
ThangType = require '../../../server/levels/thangs/ThangType'
|
||||||
User = require '../../../server/users/User'
|
User = require '../../../server/users/User'
|
||||||
UserHandler = require '../../../server/users/user_handler'
|
UserHandler = require '../../../server/users/user_handler'
|
||||||
|
|
||||||
|
@ -315,7 +319,7 @@ describe 'Statistics', ->
|
||||||
url = getURL('/db/article')
|
url = getURL('/db/article')
|
||||||
|
|
||||||
loginAdmin (carl) ->
|
loginAdmin (carl) ->
|
||||||
expect(carl.get 'stats.articleEdits').toBeUndefined()
|
expect(carl.get User.statsMapping.edits.article).toBeUndefined()
|
||||||
article.creator = carl.get 'id'
|
article.creator = carl.get 'id'
|
||||||
|
|
||||||
# Create major version 1.0
|
# Create major version 1.0
|
||||||
|
@ -326,7 +330,7 @@ describe 'Statistics', ->
|
||||||
|
|
||||||
User.findById carl.get('id'), (err, guy) ->
|
User.findById carl.get('id'), (err, guy) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(guy.get 'stats.articleEdits').toBe 1
|
expect(guy.get User.statsMapping.edits.article).toBe 1
|
||||||
|
|
||||||
# Create minor version 1.1
|
# Create minor version 1.1
|
||||||
request.post {uri:url, json: article}, (err, res, body) ->
|
request.post {uri:url, json: article}, (err, res, body) ->
|
||||||
|
@ -334,7 +338,7 @@ describe 'Statistics', ->
|
||||||
|
|
||||||
User.findById carl.get('id'), (err, guy) ->
|
User.findById carl.get('id'), (err, guy) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(guy.get 'stats.articleEdits').toBe 2
|
expect(guy.get User.statsMapping.edits.article).toBe 2
|
||||||
|
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
@ -342,16 +346,52 @@ describe 'Statistics', ->
|
||||||
loginAdmin (carl) ->
|
loginAdmin (carl) ->
|
||||||
User.findByIdAndUpdate carl.get('id'), {$unset:'stats.articleEdits': ''}, (err, guy) ->
|
User.findByIdAndUpdate carl.get('id'), {$unset:'stats.articleEdits': ''}, (err, guy) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(guy.get 'stats.articleEdits').toBeUndefined()
|
expect(guy.get User.statsMapping.edits.article).toBeUndefined()
|
||||||
|
|
||||||
UserHandler.statHandlers.articleEdits ->
|
UserHandler.statHandlers.articleEdits ->
|
||||||
User.findById carl.get('id'), (err, guy) ->
|
User.findById carl.get('id'), (err, guy) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(guy.get 'stats.articleEdits').toBe 2
|
expect(guy.get User.statsMapping.edits.article).toBe 2
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'keeps track of level edits', (done) ->
|
||||||
|
level = new Level
|
||||||
|
name: "King's Peak 3"
|
||||||
|
description: 'Climb a mountain.'
|
||||||
|
permissions: simplePermissions
|
||||||
|
scripts: []
|
||||||
|
thangs: []
|
||||||
|
|
||||||
|
loginAdmin (carl) ->
|
||||||
|
expect(carl.get User.statsMapping.edits.level).toBeUndefined()
|
||||||
|
level.creator = carl.get 'id'
|
||||||
|
level.save (err) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
|
||||||
|
User.findById carl.get('id'), (err, guy) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(guy.get 'id').toBe carl.get 'id'
|
||||||
|
expect(guy.get User.statsMapping.edits.level).toBe 1
|
||||||
|
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'recalculates level edits', (done) ->
|
||||||
|
unittest.getAdmin (jose) ->
|
||||||
|
User.findByIdAndUpdate jose.get('id'), {$unset:'stats.levelEdits':''}, (err, guy) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(guy.get User.statsMapping.edits.level).toBeUndefined()
|
||||||
|
|
||||||
|
UserHandler.statHandlers.levelEdits ->
|
||||||
|
User.findById jose.get('id'), (err, guy) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(guy.get User.statsMapping.edits.level).toBe 1
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'cleans up', (done) ->
|
it 'cleans up', (done) ->
|
||||||
clearModels [LevelSession, Article], (err) ->
|
clearModels [LevelSession, Article, Level, LevelSystem, LevelComponent, ThangType], (err) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
|
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
21
test/server/unit/patch.spec.coffee
Normal file
21
test/server/unit/patch.spec.coffee
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
require '../common'
|
||||||
|
|
||||||
|
describe 'schema methods', ->
|
||||||
|
patch = new Patch
|
||||||
|
delta:
|
||||||
|
scripts: 0: i18n: 'aaahw yeahh'
|
||||||
|
_t: 'a'
|
||||||
|
|
||||||
|
it 'is translation patch', ->
|
||||||
|
expect(patch.isTranslationPatch()).toBeTruthy()
|
||||||
|
patch.set 'delta.i18n', undefined
|
||||||
|
expect(patch.isTranslationPatch()).toBeFalsy()
|
||||||
|
|
||||||
|
it 'is miscellaneous patch', ->
|
||||||
|
expect(patch.isMiscPatch()).toBeTruthy()
|
||||||
|
patch.set 'delta.thangs', undefined
|
||||||
|
expect(patch.isMiscPatch()).toBeFalsy()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Reference in a new issue