mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 01:25:42 -05:00
Generalize new I18N view system
Previously, when diplomats submit translations, the system would try to figure out whether it should be a 'patch' or a 'change', and then would either create a patch for an admin or artisan to review and accept or reject, or would apply the changes immediately and they would be live. This was done as a compromise between getting translations live quickly, but also preventing already-translated text from getting overwritten without oversight. But having the client handle this added logical complexity. So this makes all diplomats submit patches, no matter what. The server is then in charge of deciding if it should auto-accept the patch or not. Either way, a patch is created. There was also much refactoring. This commit includes: * Update jsondiffpatch so changes within array items are handled correctly * Refactor posting patches to use the new auto-accepting logic, and out of Patch model * Refactor POST /db/patch/:handle/status so that it doesn't rely on handlers * Refactor patch stat handling to ensure auto-accepted patches are counted * Refactor User.incrementStat to use mongodb update commands, to avoid race conditions * Refactor Patch tests
This commit is contained in:
parent
e70556e900
commit
7bab895dee
29 changed files with 860 additions and 636 deletions
|
@ -89,8 +89,8 @@ expandFlattenedDelta = (delta, left, schema) ->
|
|||
delta
|
||||
|
||||
module.exports.makeJSONDiffer = ->
|
||||
hasher = (obj) -> if obj? then obj.name or obj.id or obj._id or JSON.stringify(_.keys(obj)) else 'null'
|
||||
jsondiffpatch.create({objectHash: hasher})
|
||||
objectHash = (obj) -> if obj? then (obj.name or obj.id or obj._id or JSON.stringify(_.keys(obj))) else 'null'
|
||||
jsondiffpatch.create({objectHash})
|
||||
|
||||
module.exports.getConflicts = (headDeltas, pendingDeltas) ->
|
||||
# headDeltas and pendingDeltas should be lists of deltas returned by expandDelta
|
||||
|
|
|
@ -5,8 +5,10 @@ module.exports = class PatchModel extends CocoModel
|
|||
@schema: require 'schemas/models/patch'
|
||||
urlRoot: '/db/patch'
|
||||
|
||||
setStatus: (status) ->
|
||||
PatchModel.setStatus @id, status
|
||||
setStatus: (status, options={}) ->
|
||||
options.url = "/db/patch/#{@id}/status"
|
||||
options.type = 'PUT'
|
||||
@save({status}, options)
|
||||
|
||||
@setStatus: (id, status) ->
|
||||
$.ajax("/db/patch/#{id}/status", {type: 'PUT', data: {status: status}})
|
||||
|
|
|
@ -29,16 +29,6 @@ block header
|
|||
span.glyphicon-chevron-down.glyphicon
|
||||
|
||||
ul.dropdown-menu
|
||||
li.dropdown-header(data-i18n="common.actions") Actions
|
||||
|
||||
li(class=anonymous ? "disabled": "")
|
||||
a(data-toggle="coco-modal", data-target="modal/RevertModal", data-i18n="editor.revert")#revert-button Revert
|
||||
|
||||
li.divider
|
||||
li.dropdown-header(data-i18n="common.info") Info
|
||||
li#history-button
|
||||
a(href='#', data-i18n="general.version_history") Version History
|
||||
li.divider
|
||||
li.dropdown-header(data-i18n="common.help") Help
|
||||
li
|
||||
a(href='https://github.com/codecombat/codecombat/wiki', data-i18n="editor.wiki", target="_blank") Wiki
|
||||
|
|
|
@ -11,21 +11,6 @@ module.exports = class I18NEditCourseView extends I18NEditModelView
|
|||
id: "i18n-edit-course-view"
|
||||
modelClass: Course
|
||||
|
||||
events:
|
||||
'click .open-patch-link': 'onClickOpenPatchLink'
|
||||
|
||||
constructor: ->
|
||||
super(arguments...)
|
||||
@model.saveBackups = false
|
||||
@madeChanges = false
|
||||
@patches = new Patches()
|
||||
@patches.comparator = '_id'
|
||||
@supermodel.trackRequest(@patches.fetchMineFor(@model))
|
||||
|
||||
onLoaded: ->
|
||||
super(arguments...)
|
||||
@originalModel = @model.clone()
|
||||
|
||||
buildTranslationList: ->
|
||||
lang = @selectedLanguage
|
||||
|
||||
|
@ -36,56 +21,3 @@ module.exports = class I18NEditCourseView extends I18NEditModelView
|
|||
if description = @model.get('description')
|
||||
@wrapRow 'Course description', ['description'], description, i18n[lang]?.description, []
|
||||
|
||||
onTranslationChanged: ->
|
||||
super(arguments...)
|
||||
@madeChanges = true
|
||||
|
||||
onClickOpenPatchLink: (e) ->
|
||||
patchID = $(e.currentTarget).data('patch-id')
|
||||
patch = @patches.get(patchID)
|
||||
modal = new PatchModal(patch, @model)
|
||||
@openModalView(modal)
|
||||
|
||||
onLeaveMessage: ->
|
||||
if @madeChanges
|
||||
return 'You have unsaved changes!'
|
||||
|
||||
onLanguageSelectChanged: ->
|
||||
if @madeChanges
|
||||
return unless confirm('You have unsaved changes!')
|
||||
super(arguments...)
|
||||
@madeChanges = false
|
||||
@model.set(@originalModel.clone().attributes)
|
||||
|
||||
onSubmitPatch: (e) ->
|
||||
|
||||
delta = @model.getDelta()
|
||||
flattened = deltasLib.flattenDelta(delta)
|
||||
|
||||
patch = new Patch({
|
||||
delta
|
||||
target: {
|
||||
'collection': _.string.underscored @model.constructor.className
|
||||
'id': @model.id
|
||||
}
|
||||
commitMessage: "Diplomat submission for lang #{@selectedLanguage}: #{flattened.length} change(s)."
|
||||
})
|
||||
errors = patch.validate()
|
||||
button = $(e.target)
|
||||
button.attr('disabled', 'disabled')
|
||||
return button.text('Failed to Submit Changes') if errors
|
||||
res = patch.save(null, {
|
||||
url: "/db/course/#{@model.id}/patch"
|
||||
})
|
||||
return button.text('Failed to Submit Changes') unless res
|
||||
button.text('Submitting...')
|
||||
Promise.resolve(res)
|
||||
.then =>
|
||||
@savedBefore = true
|
||||
@madeChanges = false
|
||||
@patches.add(patch)
|
||||
@renderSelectors('#patches-col')
|
||||
button.text('Submit Changes')
|
||||
.catch =>
|
||||
button.text('Error Submitting Changes')
|
||||
|
||||
|
|
|
@ -1,27 +1,42 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
locale = require 'locale/locale'
|
||||
Patch = require 'models/Patch'
|
||||
Patches = require 'collections/Patches'
|
||||
PatchModal = require 'views/editor/PatchModal'
|
||||
template = require 'templates/i18n/i18n-edit-model-view'
|
||||
deltasLib = require 'core/deltas'
|
||||
|
||||
# in the template, but need to require to load them
|
||||
require 'views/modal/RevertModal'
|
||||
###
|
||||
This view is the superclass for all views which Diplomats use to submit translations
|
||||
for database documents. They all work mostly the same, except they each set their
|
||||
`@modelClass` which is a patchable Backbone model class, and they use `@wrapRow()`
|
||||
to dynamically specify which properties are being translated.
|
||||
###
|
||||
|
||||
UNSAVED_CHANGES_MESSAGE = 'You have unsaved changes! Really discard them?'
|
||||
|
||||
module.exports = class I18NEditModelView extends RootView
|
||||
className: 'editor i18n-edit-model-view'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'change .translation-input': 'onInputChanged'
|
||||
'input .translation-input': 'onInputChanged'
|
||||
'change #language-select': 'onLanguageSelectChanged'
|
||||
'click #patch-submit': 'onSubmitPatch'
|
||||
'click .open-patch-link': 'onClickOpenPatchLink'
|
||||
|
||||
constructor: (options, @modelHandle) ->
|
||||
super(options)
|
||||
|
||||
@model = new @modelClass(_id: @modelHandle)
|
||||
@model = @supermodel.loadModel(@model).model
|
||||
@model.saveBackups = true
|
||||
@supermodel.trackRequest(@model.fetch())
|
||||
@patches = new Patches()
|
||||
@listenTo @patches, 'change', -> @renderSelectors('#patches-col')
|
||||
@patches.comparator = '_id'
|
||||
@supermodel.trackRequest(@patches.fetchMineFor(@model))
|
||||
|
||||
@selectedLanguage = me.get('preferredLanguage', true)
|
||||
@madeChanges = false
|
||||
|
||||
showLoading: ($el) ->
|
||||
$el ?= @$el.find('.outer-content')
|
||||
|
@ -29,7 +44,7 @@ module.exports = class I18NEditModelView extends RootView
|
|||
|
||||
onLoaded: ->
|
||||
super()
|
||||
@model.markToRevert() unless @model.hasLocalChanges()
|
||||
@originalModel = @model.clone()
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
|
@ -47,11 +62,11 @@ module.exports = class I18NEditModelView extends RootView
|
|||
afterRender: ->
|
||||
super()
|
||||
|
||||
@hush = true
|
||||
@ignoreLanguageSelectChanges = true
|
||||
$select = @$el.find('#language-select').empty()
|
||||
@addLanguagesToSelect($select, @selectedLanguage)
|
||||
@$el.find('option[value="en-US"]').remove()
|
||||
@hush = false
|
||||
@ignoreLanguageSelectChanges = false
|
||||
editors = []
|
||||
|
||||
@$el.find('tr[data-format="markdown"]').each((index, el) =>
|
||||
|
@ -103,7 +118,6 @@ module.exports = class I18NEditModelView extends RootView
|
|||
@onTranslationChanged(rowInfo, value)
|
||||
|
||||
onTranslationChanged: (rowInfo, value) ->
|
||||
|
||||
#- Navigate down to where the translation will live
|
||||
base = @model.attributes
|
||||
|
||||
|
@ -121,61 +135,59 @@ module.exports = class I18NEditModelView extends RootView
|
|||
base = base[seg]
|
||||
|
||||
#- Set the data in a non-kosher way
|
||||
|
||||
base[rowInfo.key[rowInfo.key.length-1]] = value
|
||||
@model.saveBackup()
|
||||
|
||||
#- Enable patch submit button
|
||||
|
||||
@$el.find('#patch-submit').attr('disabled', null)
|
||||
@madeChanges = true
|
||||
|
||||
onLanguageSelectChanged: (e) ->
|
||||
return if @hush
|
||||
return if @ignoreLanguageSelectChanges
|
||||
if @madeChanges
|
||||
return unless confirm(UNSAVED_CHANGES_MESSAGE)
|
||||
@selectedLanguage = $(e.target).val()
|
||||
if @selectedLanguage
|
||||
me.set('preferredLanguage', @selectedLanguage)
|
||||
me.patch()
|
||||
@madeChanges = false
|
||||
@model.set(@originalModel.clone().attributes)
|
||||
@render()
|
||||
|
||||
onClickOpenPatchLink: (e) ->
|
||||
patchID = $(e.currentTarget).data('patch-id')
|
||||
patch = @patches.get(patchID)
|
||||
modal = new PatchModal(patch, @model)
|
||||
@openModalView(modal)
|
||||
|
||||
onLeaveMessage: ->
|
||||
if @madeChanges
|
||||
return UNSAVED_CHANGES_MESSAGE
|
||||
|
||||
onSubmitPatch: (e) ->
|
||||
|
||||
delta = @model.getDelta()
|
||||
delta = @originalModel.getDeltaWith(@model)
|
||||
flattened = deltasLib.flattenDelta(delta)
|
||||
save = _.all(flattened, (delta) ->
|
||||
return _.isArray(delta.o) and delta.o.length is 1 and 'i18n' in delta.dataPath
|
||||
)
|
||||
|
||||
commitMessage = "Diplomat submission for lang #{@selectedLanguage}: #{flattened.length} change(s)."
|
||||
save = false if @savedBefore
|
||||
|
||||
if save
|
||||
modelToSave = @model.cloneNewMinorVersion()
|
||||
modelToSave.updateI18NCoverage() if modelToSave.get('i18nCoverage')
|
||||
if @modelClass.schema.properties.commitMessage
|
||||
modelToSave.set 'commitMessage', commitMessage
|
||||
|
||||
else
|
||||
modelToSave = new Patch()
|
||||
modelToSave.set 'delta', @model.getDelta()
|
||||
modelToSave.set 'target', {
|
||||
'collection': _.string.underscored @model.constructor.className
|
||||
'id': @model.id
|
||||
}
|
||||
modelToSave.set 'commitMessage', commitMessage
|
||||
|
||||
errors = modelToSave.validate()
|
||||
collection = _.string.underscored @model.constructor.className
|
||||
patch = new Patch({
|
||||
delta
|
||||
target: { collection, 'id': @model.id }
|
||||
commitMessage: "Diplomat submission for lang #{@selectedLanguage}: #{flattened.length} change(s)."
|
||||
})
|
||||
errors = patch.validate()
|
||||
button = $(e.target)
|
||||
button.attr('disabled', 'disabled')
|
||||
return button.text('No changes submitted, did not save patch.') unless delta
|
||||
return button.text('Failed to Submit Changes') if errors
|
||||
type = 'PUT'
|
||||
if @modelClass.schema.properties.version or (not save)
|
||||
# Override PUT so we can trigger postNewVersion logic
|
||||
# or you're POSTing a Patch
|
||||
type = 'POST'
|
||||
res = modelToSave.save(null, {type: type})
|
||||
res = patch.save(null, { url: _.result(@model, 'url') + '/patch' })
|
||||
return button.text('Failed to Submit Changes') unless res
|
||||
button.text('Submitting...')
|
||||
res.error => button.text('Error Submitting Changes')
|
||||
res.success =>
|
||||
@savedBefore = true
|
||||
Promise.resolve(res)
|
||||
.then =>
|
||||
@madeChanges = false
|
||||
@patches.add(patch)
|
||||
@renderSelectors('#patches-col')
|
||||
button.text('Submit Changes')
|
||||
.catch =>
|
||||
button.text('Error Submitting Changes')
|
||||
@$el.find('#patch-submit').attr('disabled', null)
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"aether": "~0.5.21",
|
||||
"underscore.string": "~2.3.3",
|
||||
"d3": "~3.4.4",
|
||||
"jsondiffpatch": "0.1.8",
|
||||
"jsondiffpatch": "^0.2.3",
|
||||
"nanoscroller": "~0.8.0",
|
||||
"treema": "https://github.com/codecombat/treema.git#master",
|
||||
"bootstrap": "~3.2.0",
|
||||
|
@ -72,9 +72,9 @@
|
|||
},
|
||||
"jsondiffpatch": {
|
||||
"main": [
|
||||
"build/bundle-full.js",
|
||||
"build/formatters.js",
|
||||
"src/formatters/html.css"
|
||||
"public/build/jsondiffpatch-full.js",
|
||||
"public/build/jsondiffpatch-formatters.js",
|
||||
"public/formatters-styles/html.css"
|
||||
]
|
||||
},
|
||||
"treema": {
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
"geoip-lite": "^1.1.6",
|
||||
"graceful-fs": "~2.0.1",
|
||||
"gridfs-stream": "~1.1.1",
|
||||
"jsondiffpatch": "0.1.17",
|
||||
"jsondiffpatch": "^0.2.3",
|
||||
"lodash": "~2.4.1",
|
||||
"lz-string": "^1.3.3",
|
||||
"mailchimp-api": "2.0.x",
|
||||
|
|
36
server/commons/notify.coffee
Normal file
36
server/commons/notify.coffee
Normal file
|
@ -0,0 +1,36 @@
|
|||
slack = require '../slack'
|
||||
sendwithus = require '../sendwithus'
|
||||
User = require '../models/User'
|
||||
|
||||
# TODO: Refactor notification (slack, watcher emails) logic here
|
||||
|
||||
module.exports =
|
||||
notifyChangesMadeToDoc: (req, doc) ->
|
||||
# TODO: Stop using headers to pass edit paths. Perhaps should be a method property for Mongoose models
|
||||
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._id) and not (w.toHexString() 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
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
mongoose = require 'mongoose'
|
||||
Handler = require '../commons//Handler'
|
||||
Course = require '../models/Course'
|
||||
|
||||
# TODO: Refactor PatchHandler.setStatus into its own route.
|
||||
# This handler has been resurrected solely so that course patches can be accepted.
|
||||
|
||||
CourseHandler = class CourseHandler extends Handler
|
||||
modelClass: Course
|
||||
jsonSchema: require '../../app/schemas/models/course.schema'
|
||||
allowedMethods: []
|
||||
|
||||
hasAccess: (req) ->
|
||||
req.method in @allowedMethods or req.user?.isAdmin()
|
||||
|
||||
hasAccessToDocument: (req, document, method=null) ->
|
||||
method = (method or req.method).toLowerCase()
|
||||
return true if method is 'get'
|
||||
return true if req.user?.isAdmin() or req.user?.isArtisan()
|
||||
return
|
||||
|
||||
module.exports = new CourseHandler()
|
|
@ -21,10 +21,6 @@ PatchHandler = class PatchHandler extends Handler
|
|||
patch.set 'status', 'pending'
|
||||
patch
|
||||
|
||||
getByRelationship: (req, res, args...) ->
|
||||
return @setStatus(req, res, args[0]) if req.route.method is 'put' and args[1] is 'status'
|
||||
super(arguments...)
|
||||
|
||||
get: (req, res) ->
|
||||
if req.query.view in ['pending']
|
||||
query = status: 'pending'
|
||||
|
@ -36,54 +32,6 @@ PatchHandler = class PatchHandler extends Handler
|
|||
else
|
||||
super(arguments...)
|
||||
|
||||
setStatus: (req, res, id) ->
|
||||
newStatus = req.body.status
|
||||
unless newStatus in ['rejected', 'accepted', 'withdrawn']
|
||||
return @sendBadInputError(res, 'Status must be "rejected", "accepted", or "withdrawn"')
|
||||
|
||||
@getDocumentForIdOrSlug id, (err, patch) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res) unless patch?
|
||||
targetInfo = patch.get('target')
|
||||
targetHandler = require('../' + handlers[targetInfo.collection])
|
||||
targetModel = targetHandler.modelClass
|
||||
|
||||
query = { $or: [{'original': targetInfo.original}, {'_id': mongoose.Types.ObjectId(targetInfo.original)}] }
|
||||
sort = { 'version.major': -1, 'version.minor': -1 }
|
||||
targetModel.findOne(query).sort(sort).exec (err, target) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res) unless target?
|
||||
return @sendForbiddenError(res) unless targetHandler.hasAccessToDocument(req, target, 'get')
|
||||
|
||||
if newStatus in ['rejected', 'accepted']
|
||||
return @sendForbiddenError(res) unless targetHandler.hasAccessToDocument(req, target, 'put')
|
||||
|
||||
if newStatus is 'withdrawn'
|
||||
return @sendForbiddenError(res) unless req.user.get('_id').equals patch.get('creator')
|
||||
|
||||
patch.set 'status', newStatus
|
||||
|
||||
# Only increment statistics upon very first accept
|
||||
if patch.isNewlyAccepted()
|
||||
patch.set 'acceptor', req.user.get('id')
|
||||
acceptor = req.user.get 'id'
|
||||
submitter = patch.get 'creator'
|
||||
User.incrementStat acceptor, 'stats.patchesAccepted'
|
||||
# TODO maybe merge these increments together
|
||||
if patch.isTranslationPatch()
|
||||
User.incrementStat submitter, 'stats.totalTranslationPatches'
|
||||
User.incrementStat submitter, User.statsMapping.translations[targetModel.modelName]
|
||||
if patch.isMiscPatch()
|
||||
User.incrementStat submitter, 'stats.totalMiscPatches'
|
||||
User.incrementStat submitter, User.statsMapping.misc[targetModel.modelName]
|
||||
|
||||
|
||||
# these require callbacks
|
||||
patch.save (err) =>
|
||||
log.error err if err?
|
||||
target.update {$pull:{patches:patch.get('_id')}}, {}, ->
|
||||
@sendSuccess(res, null)
|
||||
|
||||
onPostSuccess: (req, doc) ->
|
||||
log.error 'Error sending patch created: could not find the loaded target on the patch object.' unless doc.targetLoaded
|
||||
return unless doc.targetLoaded
|
||||
|
|
|
@ -10,11 +10,6 @@ Course = require '../models/Course'
|
|||
User = require '../models/User'
|
||||
Level = require '../models/Level'
|
||||
parse = require '../commons/parse'
|
||||
Patch = require '../models/Patch'
|
||||
tv4 = require('tv4').tv4
|
||||
slack = require '../slack'
|
||||
{ isJustFillingTranslations } = require '../commons/deltas'
|
||||
{ updateI18NCoverage } = require '../commons/i18n'
|
||||
|
||||
module.exports =
|
||||
|
||||
|
@ -87,61 +82,3 @@ module.exports =
|
|||
results = yield database.viewSearch(dbq, req)
|
||||
results = Course.sortCourses results
|
||||
res.send(results)
|
||||
|
||||
postPatch: wrap (req, res) ->
|
||||
# TODO: Generalize this and use for other models, once this has been put through its paces
|
||||
course = yield database.getDocFromHandle(req, Course)
|
||||
if not course
|
||||
throw new errors.NotFound('Course not found.')
|
||||
|
||||
originalDelta = req.body.delta
|
||||
originalCourse = course.toObject()
|
||||
changedCourse = _.cloneDeep(course.toObject(), (value) ->
|
||||
return value if value instanceof mongoose.Types.ObjectId
|
||||
return value if value instanceof Date
|
||||
return undefined
|
||||
)
|
||||
jsondiffpatch.patch(changedCourse, originalDelta)
|
||||
|
||||
# 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.
|
||||
normalizedDelta = jsondiffpatch.diff(originalCourse, changedCourse)
|
||||
normalizedDelta = _.pick(normalizedDelta, _.keys(originalDelta))
|
||||
reasonNotAutoAccepted = undefined
|
||||
|
||||
validation = tv4.validateMultiple(changedCourse, Course.jsonSchema)
|
||||
if not validation.valid
|
||||
reasonNotAutoAccepted = 'Did not pass json schema.'
|
||||
else if not isJustFillingTranslations(normalizedDelta)
|
||||
reasonNotAutoAccepted = 'Adding to existing translations.'
|
||||
else
|
||||
course.set(changedCourse)
|
||||
updateI18NCoverage(course)
|
||||
yield course.save()
|
||||
|
||||
patch = new Patch(req.body)
|
||||
patch.set({
|
||||
target: {
|
||||
collection: 'course'
|
||||
id: course._id
|
||||
original: course._id
|
||||
}
|
||||
creator: req.user._id
|
||||
status: if reasonNotAutoAccepted then 'pending' else 'accepted'
|
||||
created: new Date().toISOString()
|
||||
reasonNotAutoAccepted: reasonNotAutoAccepted
|
||||
})
|
||||
database.validateDoc(patch)
|
||||
|
||||
if reasonNotAutoAccepted
|
||||
yield course.update({ $addToSet: { patches: patch._id }})
|
||||
patches = course.get('patches') or []
|
||||
patches.push patch._id
|
||||
course.set({patches})
|
||||
yield patch.save()
|
||||
|
||||
res.status(201).send(patch.toObject({req: req}))
|
||||
|
||||
docLink = "https://codecombat.com/editor/course/#{course.id}"
|
||||
message = "#{req.user.get('name')} submitted a patch to #{course.get('name')}: #{patch.get('commitMessage')} #{docLink}"
|
||||
slack.sendSlackMessage message, ['artisans']
|
||||
|
|
|
@ -13,6 +13,7 @@ module.exports =
|
|||
levels: require './levels'
|
||||
named: require './named'
|
||||
patchable: require './patchable'
|
||||
patches: require './patches'
|
||||
prepaids: require './prepaids'
|
||||
rest: require './rest'
|
||||
trialRequests: require './trial-requests'
|
||||
|
|
|
@ -3,11 +3,18 @@ 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))
|
||||
|
@ -31,6 +38,7 @@ module.exports =
|
|||
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
|
||||
|
@ -44,6 +52,7 @@ module.exports =
|
|||
doc.set('watchers', watchers)
|
||||
res.status(200).send(doc)
|
||||
|
||||
|
||||
leaveWatchers: (Model, options={}) -> wrap (req, res) ->
|
||||
doc = yield database.getDocFromHandle(req, Model)
|
||||
if not doc
|
||||
|
@ -54,3 +63,96 @@ module.exports =
|
|||
watchers = _.filter watchers, (id) -> not id.equals(req.user._id)
|
||||
doc.set('watchers', watchers)
|
||||
res.status(200).send(doc)
|
||||
|
||||
|
||||
postPatch: _.curry wrap (Model, collectionName, req, res) ->
|
||||
# handle either "POST /db/<collection>/:handle/patch" or "POST /db/patch" with target included in body
|
||||
if req.params.handle
|
||||
target = yield database.getDocFromHandle(req, Model)
|
||||
else if req.body.target?.id
|
||||
target = yield Model.findById(req.body.target.id)
|
||||
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
|
||||
)
|
||||
jsondiffpatch.patch(newTargetAttrs, originalDelta)
|
||||
normalizedDelta = jsondiffpatch.diff(originalTarget, newTargetAttrs)
|
||||
normalizedDelta = _.pick(normalizedDelta, _.keys(originalDelta))
|
||||
|
||||
# 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._id
|
||||
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']
|
||||
|
|
80
server/middleware/patches.coffee
Normal file
80
server/middleware/patches.coffee
Normal file
|
@ -0,0 +1,80 @@
|
|||
utils = require '../lib/utils'
|
||||
errors = require '../commons/errors'
|
||||
wrap = require 'co-express'
|
||||
database = require '../commons/database'
|
||||
parse = require '../commons/parse'
|
||||
{ postPatch } = require './patchable'
|
||||
mongoose = require 'mongoose'
|
||||
Patch = require '../models/Patch'
|
||||
User = require '../models/User'
|
||||
|
||||
# TODO: Standardize model names so that this map is unnecessary
|
||||
collectionNameMap = {
|
||||
'achievement': 'Achievement'
|
||||
'level_component': 'level.component'
|
||||
'level_system': 'level.system'
|
||||
'thang_type': 'thang.type'
|
||||
}
|
||||
|
||||
module.exports.post = wrap (req, res) ->
|
||||
# Based on body, figure out what collection and document this patch is for
|
||||
if not req.body.target?.collection
|
||||
throw new errors.UnprocessableEntity('target.collection not provided')
|
||||
collection = req.body.target?.collection
|
||||
modelName = collectionNameMap[collection] or collection
|
||||
try
|
||||
Model = mongoose.model(modelName)
|
||||
catch e
|
||||
if e.name is 'MissingSchemaError'
|
||||
throw new errors.NotFound("#{collection} is not a known model")
|
||||
else
|
||||
throw e
|
||||
if not Model.schema.is_patchable
|
||||
throw new errors.UnprocessableEntity("#{collection} is not patchable")
|
||||
|
||||
# pass to logic shared with "POST /db/:collection/:handle/patch"
|
||||
yield postPatch(Model, collection, req, res)
|
||||
|
||||
|
||||
# Allow patch submitters to withdraw their patches, or admins/artisans to accept/reject others' patches
|
||||
module.exports.setStatus = wrap (req, res) ->
|
||||
|
||||
newStatus = req.body.status or req.body
|
||||
unless newStatus in ['rejected', 'accepted', 'withdrawn']
|
||||
throw new errors.UnprocessableEntity('Status must be "rejected", "accepted", or "withdrawn"')
|
||||
|
||||
patch = yield database.getDocFromHandle(req, Patch)
|
||||
if not patch
|
||||
throw new errors.NotFound('Could not find patch')
|
||||
|
||||
# Find the target of the patch
|
||||
collection = patch.get('target.collection')
|
||||
modelName = collectionNameMap[collection] or collection
|
||||
Model = mongoose.model(modelName)
|
||||
original = patch.get('target.original')
|
||||
query = { $or: [{original}, {'_id': mongoose.Types.ObjectId(original)}] }
|
||||
sort = { 'version.major': -1, 'version.minor': -1 }
|
||||
target = yield Model.findOne(query).sort(sort)
|
||||
if not target
|
||||
throw new errors.NotFound('Could not find patch target')
|
||||
|
||||
# Enforce permissions
|
||||
if newStatus in ['rejected', 'accepted']
|
||||
unless req.user.hasPermission('artisan') or target.hasPermissionsForMethod?(req.user, 'put')
|
||||
throw new errors.Forbidden('You do not have access to or own the target document.')
|
||||
|
||||
if newStatus is 'withdrawn'
|
||||
unless req.user._id.equals patch.get('creator')
|
||||
throw new errors.Forbidden('Only the patch creator can withdraw their patch.')
|
||||
|
||||
patch.set 'status', newStatus
|
||||
|
||||
# Only increment statistics upon very first accept
|
||||
if patch.isNewlyAccepted()
|
||||
acceptor = req.user.id
|
||||
patch.set { acceptor }
|
||||
yield User.incrementStat acceptor, 'stats.patchesAccepted'
|
||||
|
||||
yield patch.save()
|
||||
target.update {$pull: {patches:patch.get('_id')}}, {}, _.noop
|
||||
res.send(patch.toObject({req}))
|
|
@ -1,179 +1,164 @@
|
|||
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'
|
||||
{ notifyChangesMadeToDoc } = require '../commons/notify'
|
||||
|
||||
# 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.')
|
||||
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()
|
||||
# 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 })
|
||||
# Create the new version, a clone of the parent with POST data applied
|
||||
Model = parent.constructor
|
||||
doc = database.initDoc(req, Model)
|
||||
exports.initNewVersion(doc, parent)
|
||||
database.assignBody(req, doc, { unsetMissing: true })
|
||||
major = req.body.version?.major
|
||||
yield exports.saveNewVersion(doc, major)
|
||||
notifyChangesMadeToDoc(req, doc)
|
||||
res.status(201).send(doc.toObject())
|
||||
|
||||
# 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')
|
||||
|
||||
exports.initNewVersion = (doc, parent) ->
|
||||
# makes a (mostly) copy of the parent document
|
||||
ATTRIBUTES_NOT_INHERITED = ['_id', 'version', 'created', 'creator']
|
||||
doc.set(_.omit(parent.toObject(), ATTRIBUTES_NOT_INHERITED))
|
||||
return doc
|
||||
|
||||
|
||||
exports.saveNewVersion = wrap (doc, major=null) ->
|
||||
# Given a document created by initNewVersion and then modified, this sets its versions and updates existing
|
||||
# versions accordingly.
|
||||
|
||||
Model = doc.constructor
|
||||
|
||||
# Get latest (minor or major) version. This may not be the same document (or same major version) as parent.
|
||||
latestSelect = 'version index slug'
|
||||
original = doc.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)
|
||||
q1 = Model.findOne({original: original, 'version.isLatestMinor': true, 'version.major': major})
|
||||
q2 = Model.findOne({original: original, 'version.major': major})
|
||||
q2.sort({'version.minor': -1})
|
||||
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
|
||||
q2 = Model.findOne()
|
||||
q2.sort({'version.major': -1, 'version.minor': -1})
|
||||
q2.select(latestSelect)
|
||||
latest = yield q2.exec()
|
||||
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.')
|
||||
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 latest version, making it no longer the latest.
|
||||
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)
|
||||
# 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)
|
||||
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
|
||||
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())
|
||||
return doc
|
||||
|
||||
|
||||
exports.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)
|
||||
|
||||
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)
|
||||
|
||||
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 })
|
||||
|
||||
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)
|
||||
|
||||
# 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?
|
||||
|
||||
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())
|
||||
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')
|
||||
exports.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)
|
||||
results = yield dbq.exec()
|
||||
res.status(200).send(results)
|
||||
|
|
|
@ -9,6 +9,7 @@ CourseSchema = new mongoose.Schema {}, {strict: false, minimize: false, read:con
|
|||
CourseSchema.plugin plugins.NamedPlugin
|
||||
CourseSchema.plugin plugins.SearchablePlugin, {searchable: ['name', 'description']}
|
||||
CourseSchema.plugin(plugins.TranslationCoveragePlugin)
|
||||
CourseSchema.plugin(plugins.PatchablePlugin)
|
||||
|
||||
CourseSchema.statics.privateProperties = []
|
||||
CourseSchema.statics.editableProperties = [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
mongoose = require 'mongoose'
|
||||
plugins = require '../plugins/plugins'
|
||||
jsonschema = require '../../app/schemas/models/level'
|
||||
jsonSchema = require '../../app/schemas/models/level'
|
||||
config = require '../../server_config'
|
||||
|
||||
LevelSchema = new mongoose.Schema({
|
||||
|
@ -47,6 +47,7 @@ LevelSchema.post 'init', (doc) ->
|
|||
doc.set('nextLevel', undefined)
|
||||
|
||||
LevelSchema.statics.postEditableProperties = ['name']
|
||||
LevelSchema.statics.jsonSchema = jsonSchema
|
||||
|
||||
LevelSchema.statics.editableProperties = [
|
||||
'description'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
mongoose = require 'mongoose'
|
||||
plugins = require '../plugins/plugins'
|
||||
jsonschema = require '../../app/schemas/models/level_component'
|
||||
jsonSchema = require '../../app/schemas/models/level_component'
|
||||
config = require '../../server_config'
|
||||
|
||||
LevelComponentSchema = new mongoose.Schema {
|
||||
|
@ -51,4 +51,6 @@ LevelComponentSchema.pre('save', (next) ->
|
|||
next()
|
||||
)
|
||||
|
||||
LevelComponentSchema.statics.jsonSchema = jsonSchema
|
||||
|
||||
module.exports = LevelComponent = mongoose.model('level.component', LevelComponentSchema)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
mongoose = require 'mongoose'
|
||||
plugins = require '../plugins/plugins'
|
||||
jsonschema = require '../../app/schemas/models/level_system'
|
||||
jsonSchema = require '../../app/schemas/models/level_system'
|
||||
config = require '../../server_config'
|
||||
|
||||
LevelSystemSchema = new mongoose.Schema {
|
||||
|
@ -33,6 +33,8 @@ LevelSystemSchema.index(
|
|||
})
|
||||
LevelSystemSchema.index({slug: 1}, {name: 'slug index', sparse: true, unique: true})
|
||||
|
||||
LevelSystemSchema.statics.jsonSchema = jsonSchema
|
||||
|
||||
LevelSystemSchema.plugin(plugins.NamedPlugin)
|
||||
LevelSystemSchema.plugin(plugins.PermissionsPlugin)
|
||||
LevelSystemSchema.plugin(plugins.VersionedPlugin)
|
||||
|
|
|
@ -7,56 +7,6 @@ config = require '../../server_config'
|
|||
PatchSchema = new mongoose.Schema({status: String}, {strict: false,read:config.mongo.readpref})
|
||||
PatchSchema.index({'target.original': 1, 'status': 1}, {name: 'target_status'})
|
||||
|
||||
PatchSchema.pre 'save', (next) ->
|
||||
return next() unless @isNew # patch can't be altered after creation, so only need to check data once
|
||||
target = @get('target')
|
||||
return next() if target.collection is 'course' # Migrating this logic out of the Patch model, into middleware
|
||||
targetID = target.id
|
||||
Handler = require '../commons/Handler'
|
||||
if not Handler.isID(targetID)
|
||||
err = new Error('Invalid input.')
|
||||
err.response = {message: 'isn\'t a MongoDB id.', property: 'target.id'}
|
||||
err.code = 422
|
||||
return next(err)
|
||||
|
||||
collection = target.collection
|
||||
try
|
||||
handler = require('../' + handlers[collection])
|
||||
catch err
|
||||
console.error 'Couldn\'t find handler for collection:', target.collection, 'from target', target
|
||||
err = new Error('Server error.')
|
||||
err.response = {message: '', property: 'target.id'}
|
||||
err.code = 500
|
||||
return next(err)
|
||||
handler.getDocumentForIdOrSlug targetID, (err, document) =>
|
||||
if err
|
||||
err = new Error('Server error.')
|
||||
err.response = {message: '', property: 'target.id'}
|
||||
err.code = 500
|
||||
return next(err)
|
||||
|
||||
if not document
|
||||
err = new Error('Target of patch not found.')
|
||||
err.response = {message: 'was not found.', property: 'target.id'}
|
||||
err.code = 404
|
||||
return next(err)
|
||||
|
||||
target.id = document.get('_id')
|
||||
if handler.modelClass.schema.uses_coco_versions
|
||||
target.original = document.get('original')
|
||||
version = document.get('version')
|
||||
target.version = _.pick document.get('version'), 'major', 'minor'
|
||||
@set('target', target)
|
||||
else
|
||||
target.original = targetID
|
||||
|
||||
patches = document.get('patches') or []
|
||||
patches = _.clone patches
|
||||
patches.push @_id
|
||||
document.set 'patches', patches, {strict: false}
|
||||
@targetLoaded = document
|
||||
document.save (err) -> next(err)
|
||||
|
||||
PatchSchema.methods.isTranslationPatch = -> # Don't ever fat arrow bind this one
|
||||
expanded = deltas.flattenDelta @get('delta')
|
||||
_.some expanded, (delta) -> 'i18n' in delta.dataPath
|
||||
|
@ -77,11 +27,18 @@ PatchSchema.methods.wasPending = -> @get 'wasPending'
|
|||
PatchSchema.pre 'save', (next) ->
|
||||
User = require './User'
|
||||
userID = @get('creator').toHexString()
|
||||
collection = @get('target.collection')
|
||||
|
||||
# Increment patch submitter stats when the patch is "accepted".
|
||||
# Does not handle if the patch is accepted multiple times, but that's an edge case.
|
||||
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
|
||||
if @isTranslationPatch()
|
||||
User.incrementStat(userID, 'stats.totalTranslationPatches')
|
||||
User.incrementStat(userID, User.statsMapping.translations[collection])
|
||||
if @isMiscPatch()
|
||||
User.incrementStat(userID, 'stats.totalMiscPatches')
|
||||
User.incrementStat(userID, User.statsMapping.misc[collection])
|
||||
|
||||
next()
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
mongoose = require 'mongoose'
|
||||
plugins = require '../plugins/plugins'
|
||||
config = require '../../server_config'
|
||||
jsonSchema = require '../../app/schemas/models/thang_type.coffee'
|
||||
|
||||
ThangTypeSchema = new mongoose.Schema({
|
||||
body: String,
|
||||
|
@ -32,6 +33,8 @@ ThangTypeSchema.index(
|
|||
})
|
||||
ThangTypeSchema.index({slug: 1}, {name: 'slug index', sparse: true, unique: true})
|
||||
|
||||
ThangTypeSchema.statics.jsonSchema = jsonSchema
|
||||
|
||||
ThangTypeSchema.plugin plugins.NamedPlugin
|
||||
ThangTypeSchema.plugin plugins.VersionedPlugin
|
||||
ThangTypeSchema.plugin plugins.SearchablePlugin, {searchable: ['name']}
|
||||
|
|
|
@ -221,9 +221,13 @@ UserSchema.statics.statsMapping =
|
|||
article: 'stats.articleEdits'
|
||||
level: 'stats.levelEdits'
|
||||
'level.component': 'stats.levelComponentEdits'
|
||||
'level_component': 'stats.levelComponentEdits'
|
||||
'level.system': 'stats.levelSystemEdits'
|
||||
'level_system': 'stats.levelSystemEdits'
|
||||
'thang.type': 'stats.thangTypeEdits'
|
||||
'thang_type': 'stats.thangTypeEdits'
|
||||
'Achievement': 'stats.achievementEdits'
|
||||
'achievement': 'stats.achievementEdits'
|
||||
'campaign': 'stats.campaignEdits'
|
||||
'poll': 'stats.pollEdits'
|
||||
'course': 'stats.courseEdits'
|
||||
|
@ -231,9 +235,13 @@ UserSchema.statics.statsMapping =
|
|||
article: 'stats.articleTranslationPatches'
|
||||
level: 'stats.levelTranslationPatches'
|
||||
'level.component': 'stats.levelComponentTranslationPatches'
|
||||
'level_component': 'stats.levelComponentTranslationPatches'
|
||||
'level.system': 'stats.levelSystemTranslationPatches'
|
||||
'level_system': 'stats.levelSystemTranslationPatches'
|
||||
'thang.type': 'stats.thangTypeTranslationPatches'
|
||||
'thang_type': 'stats.thangTypeTranslationPatches'
|
||||
'Achievement': 'stats.achievementTranslationPatches'
|
||||
'achievement': 'stats.achievementTranslationPatches'
|
||||
'campaign': 'stats.campaignTranslationPatches'
|
||||
'poll': 'stats.pollTranslationPatches'
|
||||
'course': 'stats.courseTranslationPatches'
|
||||
|
@ -241,20 +249,34 @@ UserSchema.statics.statsMapping =
|
|||
article: 'stats.articleMiscPatches'
|
||||
level: 'stats.levelMiscPatches'
|
||||
'level.component': 'stats.levelComponentMiscPatches'
|
||||
'level_component': 'stats.levelComponentMiscPatches'
|
||||
'level.system': 'stats.levelSystemMiscPatches'
|
||||
'level_system': 'stats.levelSystemMiscPatches'
|
||||
'thang.type': 'stats.thangTypeMiscPatches'
|
||||
'thang_type': 'stats.thangTypeMiscPatches'
|
||||
'Achievement': 'stats.achievementMiscPatches'
|
||||
'achievement': 'stats.achievementMiscPatches'
|
||||
'campaign': 'stats.campaignMiscPatches'
|
||||
'poll': 'stats.pollMiscPatches'
|
||||
'course': 'stats.courseMiscPatches'
|
||||
|
||||
UserSchema.statics.incrementStat = (id, statName, done, inc=1) ->
|
||||
id = mongoose.Types.ObjectId id if _.isString id
|
||||
@findById id, (err, user) ->
|
||||
log.error err if err?
|
||||
err = new Error "Could't find user with id '#{id}'" unless user or err
|
||||
return done() if err?
|
||||
user.incrementStat statName, done, inc
|
||||
# TODO: Migrate from incrementStat to incrementStatAsync
|
||||
UserSchema.statics.incrementStatAsync = Promise.promisify (id, statName, options={}, done) ->
|
||||
# A shim over @incrementStat, providing a Promise interface
|
||||
if _.isFunction(options)
|
||||
done = options
|
||||
options = {}
|
||||
@incrementStat(id, statName, done, options.inc or 1)
|
||||
|
||||
UserSchema.statics.incrementStat = (id, statName, done=_.noop, inc=1) ->
|
||||
_id = if _.isString(id) then mongoose.Types.ObjectId(id) else id
|
||||
update = {$inc: {}}
|
||||
update.$inc[statName] = inc
|
||||
@update({_id}, update).exec((err, res) ->
|
||||
if not res.nModified
|
||||
log.warn "Did not update user stat '#{statName}' for '#{id}'"
|
||||
done?()
|
||||
)
|
||||
|
||||
UserSchema.methods.incrementStat = (statName, done, inc=1) ->
|
||||
if /^concepts\./.test statName
|
||||
|
|
|
@ -31,6 +31,7 @@ module.exports.setup = (app) ->
|
|||
app.get('/db/achievement/names', mw.named.names(Achievement))
|
||||
app.post('/db/achievement/names', mw.named.names(Achievement))
|
||||
app.get('/db/achievement/:handle/patches', mw.patchable.patches(Achievement))
|
||||
app.post('/db/achievement/:handle/patch', mw.auth.checkLoggedIn(), mw.patchable.postPatch(Achievement, 'achievement'))
|
||||
app.post('/db/achievement/:handle/watchers', mw.patchable.joinWatchers(Achievement))
|
||||
app.delete('/db/achievement/:handle/watchers', mw.patchable.leaveWatchers(Achievement))
|
||||
|
||||
|
@ -47,6 +48,7 @@ module.exports.setup = (app) ->
|
|||
app.get('/db/article/:handle/version/?(:version)?', mw.versions.getLatestVersion(Article))
|
||||
app.get('/db/article/:handle/files', mw.files.files(Article, {module: 'article'}))
|
||||
app.get('/db/article/:handle/patches', mw.patchable.patches(Article))
|
||||
app.post('/db/article/:handle/patch', mw.auth.checkLoggedIn(), mw.patchable.postPatch(Article, 'article'))
|
||||
app.post('/db/article/:handle/watchers', mw.patchable.joinWatchers(Article))
|
||||
app.delete('/db/article/:handle/watchers', mw.patchable.leaveWatchers(Article))
|
||||
|
||||
|
@ -60,6 +62,7 @@ module.exports.setup = (app) ->
|
|||
app.get('/db/campaign/:handle/achievements', mw.campaigns.fetchRelatedAchievements)
|
||||
app.get('/db/campaign/:handle/levels', mw.campaigns.fetchRelatedLevels)
|
||||
app.get('/db/campaign/:handle/patches', mw.patchable.patches(Campaign))
|
||||
app.post('/db/campaign/:handle/patch', mw.auth.checkLoggedIn(), mw.patchable.postPatch(Campaign, 'campaign'))
|
||||
app.get('/db/campaign/-/overworld', mw.campaigns.fetchOverworld)
|
||||
|
||||
app.post('/db/classroom', mw.classrooms.post)
|
||||
|
@ -87,7 +90,7 @@ module.exports.setup = (app) ->
|
|||
app.get('/db/course/:handle', mw.rest.getByHandle(Course))
|
||||
app.get('/db/course/:handle/level-solutions', mw.courses.fetchLevelSolutions)
|
||||
app.get('/db/course/:handle/levels/:levelOriginal/next', mw.courses.fetchNextLevel)
|
||||
app.post('/db/course/:handle/patch', mw.auth.checkLoggedIn(), mw.courses.postPatch)
|
||||
app.post('/db/course/:handle/patch', mw.auth.checkLoggedIn(), mw.patchable.postPatch(Course, 'course'))
|
||||
app.get('/db/course/:handle/patches', mw.patchable.patches(Course))
|
||||
|
||||
app.get('/db/course_instance/-/non-hoc', mw.auth.checkHasPermission(['admin']), mw.courseInstances.fetchNonHoc)
|
||||
|
@ -104,6 +107,16 @@ module.exports.setup = (app) ->
|
|||
app.post('/db/level/names', mw.named.names(Level))
|
||||
app.post('/db/level/:handle', mw.auth.checkLoggedIn(), mw.versions.postNewVersion(Level, { hasPermissionsOrTranslations: 'artisan' })) # TODO: add /new-version to route like Article has
|
||||
app.get('/db/level/:handle/session', mw.auth.checkHasUser(), mw.levels.upsertSession)
|
||||
app.post('/db/level/:handle/patch', mw.auth.checkLoggedIn(), mw.patchable.postPatch(Level, 'level'))
|
||||
app.get('/db/level/:handle/patches', mw.patchable.patches(Level))
|
||||
|
||||
LevelComponent = require '../models/LevelComponent'
|
||||
app.post('/db/level.component/:handle/patch', mw.auth.checkLoggedIn(), mw.patchable.postPatch(LevelComponent, 'level_component'))
|
||||
app.get('/db/level.component/:handle/patches', mw.patchable.patches(LevelComponent))
|
||||
|
||||
LevelSystem = require '../models/LevelSystem'
|
||||
app.post('/db/level.system/:handle/patch', mw.auth.checkLoggedIn(), mw.patchable.postPatch(LevelSystem, 'level_system'))
|
||||
app.get('/db/level.system/:handle/patches', mw.patchable.patches(LevelSystem))
|
||||
|
||||
app.put('/db/user/:handle', mw.users.resetEmailVerifiedFlag)
|
||||
app.delete('/db/user/:handle', mw.users.removeFromClassrooms)
|
||||
|
@ -121,6 +134,13 @@ module.exports.setup = (app) ->
|
|||
app.post('/db/user/:handle/deteacher', mw.auth.checkHasPermission(['admin']), mw.users.deteacher)
|
||||
app.post('/db/user/:handle/check-for-new-achievement', mw.auth.checkLoggedIn(), mw.users.checkForNewAchievement)
|
||||
|
||||
app.post('/db/patch', mw.patches.post)
|
||||
app.put('/db/patch/:handle/status', mw.auth.checkLoggedIn(), mw.patches.setStatus)
|
||||
|
||||
Poll = require '../models/Poll'
|
||||
app.post('/db/poll/:handle/patch', mw.auth.checkLoggedIn(), mw.patchable.postPatch(Poll, 'poll'))
|
||||
app.get('/db/poll/:handle/patches', mw.patchable.patches(Poll))
|
||||
|
||||
app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator)
|
||||
app.get('/db/prepaid/-/active-schools', mw.auth.checkHasPermission(['admin']), mw.prepaids.fetchActiveSchools)
|
||||
app.post('/db/prepaid', mw.auth.checkHasPermission(['admin']), mw.prepaids.post)
|
||||
|
@ -128,6 +148,10 @@ module.exports.setup = (app) ->
|
|||
|
||||
app.get '/db/products', require('./db/product').get
|
||||
|
||||
ThangType = require '../models/ThangType'
|
||||
app.post('/db/thang.type/:handle/patch', mw.auth.checkLoggedIn(), mw.patchable.postPatch(ThangType, 'thang_type'))
|
||||
app.get('/db/thang.type/:handle/patches', mw.patchable.patches(ThangType))
|
||||
|
||||
TrialRequest = require '../models/TrialRequest'
|
||||
app.get('/db/trial.request', mw.trialRequests.fetchByApplicant, mw.auth.checkHasPermission(['admin']), mw.rest.get(TrialRequest))
|
||||
app.post('/db/trial.request', mw.trialRequests.post)
|
||||
|
|
|
@ -600,13 +600,14 @@ describe 'GET /db/article/:handle/patches', ->
|
|||
[res, article] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
|
||||
expect(res.statusCode).toBe(201)
|
||||
[res, patch] = yield request.postAsync { uri: getURL('/db/patch'), json: {
|
||||
delta: []
|
||||
delta: {name:['test']}
|
||||
commitMessage: 'Test commit'
|
||||
target: {
|
||||
collection: 'article'
|
||||
id: article._id
|
||||
}
|
||||
}}
|
||||
expect(res.statusCode).toBe(201)
|
||||
[res, patches] = yield request.getAsync getURL("/db/article/#{article._id}/patches"), { json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(patches.length).toBe(1)
|
||||
|
@ -680,3 +681,48 @@ describe 'DELETE /db/article/:handle/watchers', ->
|
|||
ids = (id.toString() for id in article.get('watchers'))
|
||||
expect(_.contains(ids, user.id)).toBe(false)
|
||||
done()
|
||||
|
||||
|
||||
describe 'POST /db/article/:handle/patch', ->
|
||||
|
||||
it 'creates a new version if the changes are translation only', utils.wrap (done) ->
|
||||
# create article
|
||||
yield utils.clearModels([Article])
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
article = yield utils.makeArticle()
|
||||
|
||||
# submit a translation patch
|
||||
user = yield utils.initUser()
|
||||
yield utils.loginUser(user)
|
||||
|
||||
originalArticle = article.toObject()
|
||||
changedArticle = _.extend({}, originalArticle, {i18n: {de: {name: 'Name in German'}}})
|
||||
json = {
|
||||
delta: jsondiffpatch.diff(originalArticle, changedArticle)
|
||||
commitMessage: 'Server test commit'
|
||||
target: {
|
||||
collection: 'article'
|
||||
id: article.id
|
||||
}
|
||||
}
|
||||
url = utils.getURL("/db/article/#{article.id}/patch")
|
||||
[res, body] = yield request.postAsync({ url, json })
|
||||
expect(res.statusCode).toBe(201)
|
||||
|
||||
[firstArticle, secondArticle] = yield Article.find().sort('_id')
|
||||
|
||||
expectedVersion = { isLatestMinor: false, isLatestMajor: false, minor: 0, major: 0 }
|
||||
expect(_.isEqual(firstArticle.get('version'), expectedVersion)).toBe(true)
|
||||
|
||||
expectedVersion = { isLatestMinor: true, isLatestMajor: true, minor: 1, major: 0 }
|
||||
expect(_.isEqual(secondArticle.get('version'), expectedVersion)).toBe(true)
|
||||
|
||||
expect(firstArticle.get('i18n.de.name')).toBe(undefined)
|
||||
expect(secondArticle.get('i18n.de.name')).toBe('Name in German')
|
||||
expect(firstArticle.get('creator').equals(admin._id)).toBe(true)
|
||||
expect(secondArticle.get('creator').equals(user._id)).toBe(true)
|
||||
expect(firstArticle.get('commitMessage')).toBeUndefined()
|
||||
expect(secondArticle.get('commitMessage')).toBe('Server test commit')
|
||||
|
||||
done()
|
||||
|
|
|
@ -10,6 +10,7 @@ Classroom = require '../../../server/models/Classroom'
|
|||
Campaign = require '../../../server/models/Campaign'
|
||||
Level = require '../../../server/models/Level'
|
||||
Patch = require '../../../server/models/Patch'
|
||||
User = require '../../../server/models/User'
|
||||
|
||||
courseFixture = {
|
||||
name: 'Unnamed course'
|
||||
|
@ -233,8 +234,8 @@ describe 'GET /db/course/:handle/level-solutions', ->
|
|||
describe 'POST /db/course/:handle/patch', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels [User, Course]
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
@admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(@admin)
|
||||
|
||||
@course = yield utils.makeCourse({
|
||||
name: 'Test Course'
|
||||
|
@ -253,7 +254,7 @@ describe 'POST /db/course/:handle/patch', ->
|
|||
}
|
||||
done()
|
||||
|
||||
it 'saves the changes immediately if just adding new translations to existing langauge', utils.wrap (done) ->
|
||||
it 'accepts the patch immediately if just adding new translations to existing language', utils.wrap (done) ->
|
||||
originalCourse = _.cloneDeep(@course.toObject())
|
||||
changedCourse = _.cloneDeep(@course.toObject())
|
||||
changedCourse.i18n.de.description = 'German translation!'
|
||||
|
@ -265,9 +266,18 @@ describe 'POST /db/course/:handle/patch', ->
|
|||
expect(course.get('i18n').de.description).toBe('German translation!')
|
||||
expect(course.get('patches')).toBeUndefined()
|
||||
expect(_.contains(course.get('i18nCoverage'),'de')).toBe(true)
|
||||
yield new Promise((resolve) -> setTimeout(resolve, 10))
|
||||
admin = yield User.findById(@admin.id)
|
||||
expected = {
|
||||
patchesSubmitted: 1,
|
||||
courseTranslationPatches: 1,
|
||||
totalTranslationPatches: 1,
|
||||
patchesContributed: 1
|
||||
}
|
||||
expect(_.isEqual(admin.get('stats'), expected)).toBe(true)
|
||||
done()
|
||||
|
||||
it 'saves the changes immediately if translations are for a new langauge', utils.wrap (done) ->
|
||||
it 'accepts the patch immediately if translations are for a new language', utils.wrap (done) ->
|
||||
originalCourse = _.cloneDeep(@course.toObject())
|
||||
changedCourse = _.cloneDeep(@course.toObject())
|
||||
changedCourse.i18n.fr = { description: 'French translation!' }
|
||||
|
@ -296,6 +306,12 @@ describe 'POST /db/course/:handle/patch', ->
|
|||
expect(patch.get('reasonNotAutoAccepted')).toBe('Adding to existing translations.')
|
||||
[res, body] = yield request.getAsync({ url: utils.getURL("/db/course/#{@course.id}/patches?status=pending"), json: true })
|
||||
expect(res.body[0]._id).toBe(patch.id)
|
||||
yield new Promise((resolve) -> setTimeout(resolve, 10))
|
||||
admin = yield User.findById(@admin.id)
|
||||
expected = {
|
||||
patchesSubmitted: 1
|
||||
}
|
||||
expect(_.isEqual(admin.get('stats'), expected)).toBe(true)
|
||||
done()
|
||||
|
||||
it 'saves a patch if applying the patch would invalidate the course data', utils.wrap (done) ->
|
||||
|
@ -327,6 +343,8 @@ describe 'POST /db/course/:handle/patch', ->
|
|||
expect(course.get('i18n').de.description).toBe('Race condition')
|
||||
expect(course.get('patches').length).toBe(1)
|
||||
patch = yield Patch.findById(course.get('patches')[0])
|
||||
expect(_.isEqual(patch.get('delta'), @json.delta)).toBe(true)
|
||||
# will have been normalized to include that it has been modified from "Race condition"
|
||||
expectedDelta = {"i18n":{"de":{"description":["Race condition","German translation!"]}}}
|
||||
expect(_.isEqual(patch.get('delta'), expectedDelta)).toBe(true)
|
||||
expect(patch.get('reasonNotAutoAccepted')).toBe('Adding to existing translations.')
|
||||
done()
|
||||
|
|
|
@ -4,163 +4,170 @@ Article = require '../../../server/models/Article'
|
|||
Patch = require '../../../server/models/Patch'
|
||||
request = require '../request'
|
||||
utils = require '../utils'
|
||||
co = require 'co'
|
||||
Promise = require 'bluebird'
|
||||
|
||||
describe '/db/patch', ->
|
||||
async = require 'async'
|
||||
UserHandler = require '../../../server/handlers/user_handler'
|
||||
|
||||
it 'clears the db first', (done) ->
|
||||
clearModels [User, Article, Patch], (err) ->
|
||||
throw err if err
|
||||
done()
|
||||
|
||||
article = {name: 'Yo', body: 'yo ma'}
|
||||
articleURL = getURL('/db/article')
|
||||
articles = {}
|
||||
|
||||
patchURL = getURL('/db/patch')
|
||||
patches = {}
|
||||
patch =
|
||||
makeArticle = utils.wrap (done) ->
|
||||
@creator = yield utils.initAdmin()
|
||||
yield utils.loginUser(@creator)
|
||||
@article = yield utils.makeArticle()
|
||||
@json = {
|
||||
commitMessage: 'Accept this patch!'
|
||||
delta: {name: ['test']}
|
||||
editPath: '/who/knows/yes'
|
||||
delta: { name: ['test'] }
|
||||
target:
|
||||
id: null
|
||||
id: @article.id
|
||||
collection: 'article'
|
||||
}
|
||||
@url = utils.getURL('/db/patch')
|
||||
@user = yield utils.initUser()
|
||||
yield utils.loginUser(@user)
|
||||
done()
|
||||
|
||||
it 'creates an Article to patch', (done) ->
|
||||
loginAdmin ->
|
||||
request.post {uri: articleURL, json: article}, (err, res, body) ->
|
||||
articles[0] = body
|
||||
patch.target.id = articles[0]._id
|
||||
done()
|
||||
describe 'POST /db/patch', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([User, Patch, Article])
|
||||
done()
|
||||
|
||||
it 'allows someone to submit a patch to something they don\'t control', (done) ->
|
||||
loginJoe (joe) ->
|
||||
request.post {uri: patchURL, json: patch}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.target.original).toBeDefined()
|
||||
expect(body.target.version.major).toBeDefined()
|
||||
expect(body.target.version.minor).toBeDefined()
|
||||
expect(body.status).toBe('pending')
|
||||
expect(body.created).toBeDefined()
|
||||
expect(body.creator).toBe(joe.id)
|
||||
patches[0] = body
|
||||
done()
|
||||
beforeEach makeArticle
|
||||
|
||||
it 'adds a patch to the target document', (done) ->
|
||||
Article.findOne({}).exec (err, article) ->
|
||||
expect(article.toObject().patches[0]).toBeDefined()
|
||||
done()
|
||||
it 'allows someone to submit a patch to something they don\'t control', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync { @url, @json }
|
||||
expect(res.statusCode).toBe(201)
|
||||
expect(body.target.original).toBe(@article.get('original').toString())
|
||||
expect(body.target.version.major).toBeDefined()
|
||||
expect(body.target.version.minor).toBeDefined()
|
||||
expect(body.status).toBe('pending')
|
||||
expect(body.created).toBeDefined()
|
||||
expect(body.creator).toBe(@user.id)
|
||||
done()
|
||||
|
||||
it 'shows up in patch requests', (done) ->
|
||||
patchesURL = getURL("/db/article/#{articles[0]._id}/patches")
|
||||
request.get {uri: patchesURL}, (err, res, body) ->
|
||||
body = JSON.parse(body)
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(1)
|
||||
done()
|
||||
it 'adds a patch to the target document', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync { @url, @json }
|
||||
article = yield Article.findById(@article.id)
|
||||
expect(article.get('patches').length).toBe(1)
|
||||
done()
|
||||
|
||||
it 'allows you to set yourself as watching', (done) ->
|
||||
watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
|
||||
request.put {uri: watchingURL, json: {on: true}}, (err, res, body) ->
|
||||
expect(body.watchers[1]).toBeDefined()
|
||||
done()
|
||||
it 'shows up in patch requests', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync { @url, @json }
|
||||
patchID = res.body._id
|
||||
url = utils.getURL("/db/article/#{@article.id}/patches")
|
||||
[res, body] = yield request.getAsync { url, json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(1)
|
||||
expect(body[0]._id).toBe(patchID)
|
||||
done()
|
||||
|
||||
it 'added the watcher to the target document', (done) ->
|
||||
Article.findOne({}).exec (err, article) ->
|
||||
expect(article.toObject().watchers[1]).toBeDefined()
|
||||
done()
|
||||
it 'accepts all patchable collections', utils.wrap (done) ->
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
|
||||
it 'does not add duplicate watchers', (done) ->
|
||||
watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
|
||||
request.put {uri: watchingURL, json: {on: true}}, (err, res, body) ->
|
||||
expect(body.watchers.length).toBe(3)
|
||||
done()
|
||||
targets = [
|
||||
{ collection: 'achievement', modelPromise: utils.makeAchievement() }
|
||||
{ collection: 'article', modelPromise: utils.makeArticle() }
|
||||
{ collection: 'campaign', modelPromise: utils.makeCampaign() }
|
||||
{ collection: 'course', modelPromise: utils.makeCourse() }
|
||||
{ collection: 'level', modelPromise: utils.makeLevel() }
|
||||
{ collection: 'level_component', modelPromise: utils.makeLevelComponent() }
|
||||
{ collection: 'level_system', modelPromise: utils.makeLevelSystem() }
|
||||
{ collection: 'poll', modelPromise: utils.makePoll() }
|
||||
{ collection: 'thang_type', modelPromise: utils.makeThangType() }
|
||||
]
|
||||
|
||||
it 'allows removing yourself', (done) ->
|
||||
watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
|
||||
request.put {uri: watchingURL, json: {on: false}}, (err, res, body) ->
|
||||
expect(body.watchers.length).toBe(2)
|
||||
done()
|
||||
# concisely test everything in parallel
|
||||
promises = targets.map((target) => co =>
|
||||
model = yield target.modelPromise
|
||||
json = {
|
||||
commitMessage: 'Accept this patch!'
|
||||
delta: { name: ['test'] }
|
||||
target:
|
||||
id: model.id
|
||||
collection: target.collection
|
||||
}
|
||||
[res, body] = yield request.postAsync { @url, json }
|
||||
expect(res.statusCode).toBe(201)
|
||||
)
|
||||
yield promises
|
||||
count = yield Patch.count()
|
||||
expect(count).toBe(targets.length) # make sure all patches got created
|
||||
done()
|
||||
|
||||
it 'allows the submitter to withdraw the pull request', (done) ->
|
||||
statusURL = getURL("/db/patch/#{patches[0]._id}/status")
|
||||
request.put {uri: statusURL, json: {status: 'withdrawn'}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
Patch.findOne({}).exec (err, article) ->
|
||||
expect(article.get('status')).toBe 'withdrawn'
|
||||
Article.findOne({}).exec (err, article) ->
|
||||
expect(article.toObject().patches.length).toBe(0)
|
||||
done()
|
||||
describe 'PUT /db/:collection/:handle/watch', ->
|
||||
beforeEach makeArticle
|
||||
|
||||
it 'does not allow the submitter to reject or accept the pull request', (done) ->
|
||||
statusURL = getURL("/db/patch/#{patches[0]._id}/status")
|
||||
request.put {uri: statusURL, json: {status: 'rejected'}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
request.put {uri: statusURL, json: {status: 'accepted'}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
Patch.findOne({}).exec (err, article) ->
|
||||
expect(article.get('status')).toBe 'withdrawn'
|
||||
done()
|
||||
it 'adds the user to the list of watchers idempotently when body is {on: true}', utils.wrap (done) ->
|
||||
url = getURL("/db/article/#{@article.id}/watch")
|
||||
[res, body] = yield request.putAsync({url, json: {on: true}})
|
||||
expect(body.watchers[1]).toBeDefined()
|
||||
expect(_.last(body.watchers)).toBe(@user.id)
|
||||
previousWatchers = body.watchers
|
||||
[res, body] = yield request.putAsync({url, json: {on: true}})
|
||||
expect(_.isEqual(previousWatchers, body.watchers)).toBe(true)
|
||||
done()
|
||||
|
||||
it 'allows the recipient to accept or reject the pull request', (done) ->
|
||||
statusURL = getURL("/db/patch/#{patches[0]._id}/status")
|
||||
loginAdmin ->
|
||||
request.put {uri: statusURL, json: {status: 'rejected'}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
Patch.findOne({}).exec (err, article) ->
|
||||
expect(article.get('status')).toBe 'rejected'
|
||||
request.put {uri: statusURL, json: {status: 'accepted'}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
Patch.findOne({}).exec (err, article) ->
|
||||
expect(article.get('status')).toBe 'accepted'
|
||||
expect(article.get('acceptor')).toBeDefined()
|
||||
done()
|
||||
it 'removes user from the list of watchers when body is {on: false}', utils.wrap (done) ->
|
||||
url = getURL("/db/article/#{@article.id}/watch")
|
||||
[res, body] = yield request.putAsync({url, json: {on: true}})
|
||||
expect(_.contains(body.watchers, @user.id)).toBe(true)
|
||||
[res, body] = yield request.putAsync({url, json: {on: false}})
|
||||
expect(_.contains(body.watchers, @user.id)).toBe(false)
|
||||
done()
|
||||
|
||||
it 'keeps track of amount of submitted and accepted patches', (done) ->
|
||||
loginJoe (joe) ->
|
||||
User.findById joe.get('_id'), (err, guy) ->
|
||||
expect(err).toBeNull()
|
||||
expect(guy.get 'stats.patchesSubmitted').toBe 1
|
||||
expect(guy.get 'stats.patchesContributed').toBe 1
|
||||
expect(guy.get 'stats.totalMiscPatches').toBe 1
|
||||
expect(guy.get 'stats.articleMiscPatches').toBe 1
|
||||
expect(guy.get 'stats.totalTranslationPatches').toBeUndefined()
|
||||
done()
|
||||
|
||||
it 'recalculates amount of submitted and accepted patches', (done) ->
|
||||
loginJoe (joe) ->
|
||||
User.findById joe.get('_id'), (err, joe) ->
|
||||
expect(joe.get 'stats.patchesSubmitted').toBe 1
|
||||
joe.update {$unset: stats: ''}, (err) ->
|
||||
UserHandler.modelClass.findById joe.get('_id'), (err, joe) ->
|
||||
expect(err).toBeNull()
|
||||
expect(joe.get 'stats').toBeUndefined()
|
||||
async.parallel [
|
||||
(done) -> UserHandler.recalculateStats 'patchesContributed', done
|
||||
(done) -> UserHandler.recalculateStats 'patchesSubmitted', done
|
||||
(done) -> UserHandler.recalculateStats 'totalMiscPatches', done
|
||||
(done) -> UserHandler.recalculateStats 'totalTranslationPatches', done
|
||||
(done) -> UserHandler.recalculateStats 'articleMiscPatches', done
|
||||
], (err) ->
|
||||
expect(err).toBeNull()
|
||||
UserHandler.modelClass.findById joe.get('_id'), (err, joe) ->
|
||||
expect(joe.get 'stats.patchesSubmitted').toBe 1
|
||||
expect(joe.get 'stats.patchesContributed').toBe 1
|
||||
expect(joe.get 'stats.totalMiscPatches').toBe 1
|
||||
expect(joe.get 'stats.articleMiscPatches').toBe 1
|
||||
expect(joe.get 'stats.totalTranslationPatches').toBeUndefined()
|
||||
done()
|
||||
describe 'PUT /db/patch/:handle/status', ->
|
||||
beforeEach makeArticle
|
||||
|
||||
it 'does not allow the recipient to withdraw the pull request', (done) ->
|
||||
loginAdmin ->
|
||||
statusURL = getURL("/db/patch/#{patches[0]._id}/status")
|
||||
request.put {uri: statusURL, json: {status:'withdrawn'}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
Patch.findOne({}).exec (err, article) ->
|
||||
expect(article.get('status')).toBe 'accepted'
|
||||
done()
|
||||
beforeEach utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync { url: utils.getURL('/db/patch'), @json }
|
||||
@patchID = body._id
|
||||
@url = utils.getURL("/db/patch/#{@patchID}/status")
|
||||
done()
|
||||
|
||||
it 'withdraws the submitter\'s patch', utils.wrap (done) ->
|
||||
[res, body] = yield request.putAsync {@url, json: {status: 'withdrawn'}}
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.status).toBe('withdrawn')
|
||||
article = yield Article.findById(@article.id)
|
||||
expect(article.get('patches').length).toBe(0)
|
||||
done()
|
||||
|
||||
it 'does not allow the submitter to reject or accept the pull request', utils.wrap (done) ->
|
||||
[res, body] = yield request.putAsync {@url, json: {status: 'rejected'}}
|
||||
expect(res.statusCode).toBe(403)
|
||||
[res, body] = yield request.putAsync {@url, json: {status: 'accepted'}}
|
||||
expect(res.statusCode).toBe(403)
|
||||
patch = yield Patch.findById(@patchID)
|
||||
expect(patch.get('status')).toBe('pending')
|
||||
done()
|
||||
|
||||
it 'allows the recipient to accept or reject the pull request', utils.wrap (done) ->
|
||||
yield utils.loginUser(@creator)
|
||||
[res, body] = yield request.putAsync {@url, json: {status: 'rejected'}}
|
||||
expect(res.statusCode).toBe(200)
|
||||
patch = yield Patch.findById(@patchID)
|
||||
expect(patch.get('status')).toBe 'rejected'
|
||||
[res, body] = yield request.putAsync {@url, json: {status: 'accepted'}}
|
||||
expect(body.status).toBe('accepted')
|
||||
expect(body.acceptor).toBe(@creator.id)
|
||||
done()
|
||||
|
||||
it 'keeps track of amount of submitted and accepted patches', utils.wrap (done) ->
|
||||
yield utils.loginUser(@creator)
|
||||
[res, body] = yield request.putAsync {@url, json: {status: 'accepted'}}
|
||||
expect(res.statusCode).toBe(200)
|
||||
yield new Promise((resolve) -> setTimeout(resolve, 100))
|
||||
user = yield User.findById(@user.id)
|
||||
expect(user.get 'stats.patchesSubmitted').toBe 1
|
||||
expect(user.get 'stats.patchesContributed').toBe 1
|
||||
expect(user.get 'stats.totalMiscPatches').toBe 1
|
||||
expect(user.get 'stats.articleMiscPatches').toBe 1
|
||||
expect(user.get 'stats.totalTranslationPatches').toBeUndefined()
|
||||
done()
|
||||
|
||||
it 'does not allow the recipient to withdraw the pull request', utils.wrap (done) ->
|
||||
yield utils.loginUser(@creator)
|
||||
[res, body] = yield request.putAsync {@url, json: {status: 'withdrawn'}}
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'only allows artisans and admins to set patch status for courses', utils.wrap (done) ->
|
||||
submitter = yield utils.initUser()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
GLOBAL._ = require 'lodash'
|
||||
|
||||
User = require '../../../server/models/User'
|
||||
utils = require '../utils'
|
||||
|
||||
describe 'User', ->
|
||||
|
||||
|
@ -65,3 +66,13 @@ describe 'User', ->
|
|||
[timestamp, hash] = code.split(':')
|
||||
expect(new Date(parseInt(timestamp))).toEqual(now)
|
||||
done()
|
||||
|
||||
describe '.incrementStatAsync()', ->
|
||||
it 'records nested stats', utils.wrap (done) ->
|
||||
user = yield utils.initUser()
|
||||
yield User.incrementStatAsync user.id, 'stats.testNumber'
|
||||
yield User.incrementStatAsync user.id, 'stats.concepts.basic', {inc: 10}
|
||||
user = yield User.findById(user.id)
|
||||
expect(user.get('stats.testNumber')).toBe(1)
|
||||
expect(user.get('stats.concepts.basic')).toBe(10)
|
||||
done()
|
||||
|
|
51
spec/server/unit/user_handler.spec.coffee
Normal file
51
spec/server/unit/user_handler.spec.coffee
Normal file
|
@ -0,0 +1,51 @@
|
|||
User = require '../../../server/models/User'
|
||||
request = require '../request'
|
||||
utils = require '../utils'
|
||||
Promise = require 'bluebird'
|
||||
|
||||
UserHandler = require '../../../server/handlers/user_handler'
|
||||
|
||||
describe 'UserHandler', ->
|
||||
describe '.recalculateStats(statName, done)', ->
|
||||
it 'recalculates amount of submitted and accepted patches', utils.wrap (done) ->
|
||||
creator = yield utils.initAdmin()
|
||||
yield utils.loginUser(creator)
|
||||
article = yield utils.makeArticle()
|
||||
|
||||
user = yield utils.initUser()
|
||||
yield utils.loginUser(user)
|
||||
json = {
|
||||
commitMessage: 'Accept this patch!'
|
||||
delta: {name: ['test']}
|
||||
target: {id: article.id, collection: 'article'}
|
||||
}
|
||||
url = utils.getURL('/db/patch')
|
||||
[res, body] = yield request.postAsync { url, json }
|
||||
patchID = body._id
|
||||
expect(res.statusCode).toBe(201)
|
||||
|
||||
yield utils.loginUser(creator)
|
||||
url = utils.getURL("/db/patch/#{patchID}/status")
|
||||
[res, body] = yield request.putAsync {url, json: {status: 'accepted'}}
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
user = yield User.findById(user.id)
|
||||
expect(user.get 'stats.patchesSubmitted').toBe 1
|
||||
statsBefore = user.get('stats')
|
||||
|
||||
yield user.update({$unset: {stats: ''}})
|
||||
user = yield User.findById(user.id)
|
||||
expect(user.get 'stats').toBeUndefined()
|
||||
recalculateStatsAsync = Promise.promisify(UserHandler.recalculateStats)
|
||||
yield [
|
||||
recalculateStatsAsync 'patchesContributed'
|
||||
recalculateStatsAsync 'patchesSubmitted'
|
||||
recalculateStatsAsync 'totalMiscPatches'
|
||||
recalculateStatsAsync 'totalTranslationPatches'
|
||||
recalculateStatsAsync 'articleMiscPatches'
|
||||
]
|
||||
user = yield User.findById(user.id)
|
||||
statsAfter = user.get('stats')
|
||||
|
||||
expect(_.isEqual(statsBefore, statsAfter)).toBe(true)
|
||||
done()
|
|
@ -2,6 +2,11 @@ async = require 'async'
|
|||
utils = require '../../server/lib/utils'
|
||||
co = require 'co'
|
||||
Promise = require 'bluebird'
|
||||
Article = require '../../server/models/Article'
|
||||
LevelComponent = require '../../server/models/LevelComponent'
|
||||
LevelSystem = require '../../server/models/LevelSystem'
|
||||
Poll = require '../../server/models/Poll'
|
||||
ThangType = require '../../server/models/ThangType'
|
||||
User = require '../../server/models/User'
|
||||
Level = require '../../server/models/Level'
|
||||
LevelSession = require '../../server/models/LevelSession'
|
||||
|
@ -132,6 +137,78 @@ module.exports = mw =
|
|||
session = new LevelSession(data)
|
||||
session.save(done)
|
||||
|
||||
makeArticle: Promise.promisify (data, sources, done) ->
|
||||
args = Array.from(arguments)
|
||||
[done, [data, sources]] = [args.pop(), args]
|
||||
|
||||
data = _.extend({}, {
|
||||
name: _.uniqueId('Article ')
|
||||
}, data)
|
||||
|
||||
request.post { uri: getURL('/db/article'), json: data }, (err, res) ->
|
||||
return done(err) if err
|
||||
expect(res.statusCode).toBe(201)
|
||||
Article.findById(res.body._id).exec done
|
||||
|
||||
makeLevelComponent: Promise.promisify (data, sources, done) ->
|
||||
args = Array.from(arguments)
|
||||
[done, [data, sources]] = [args.pop(), args]
|
||||
|
||||
data = _.extend({}, {
|
||||
name: _.uniqueId('LevelComponent')
|
||||
system: 'ai'
|
||||
code: 'let const = var'
|
||||
permissions: [{target: mw.lastLogin.id, access: 'owner'}]
|
||||
}, data)
|
||||
|
||||
request.post { uri: getURL('/db/level.component'), json: data }, (err, res) ->
|
||||
return done(err) if err
|
||||
expect(res.statusCode).toBe(200)
|
||||
LevelComponent.findById(res.body._id).exec done
|
||||
|
||||
makeLevelSystem: Promise.promisify (data, sources, done) ->
|
||||
args = Array.from(arguments)
|
||||
[done, [data, sources]] = [args.pop(), args]
|
||||
|
||||
data = _.extend({}, {
|
||||
name: _.uniqueId('LevelSystem')
|
||||
permissions: [{target: mw.lastLogin.id, access: 'owner'}]
|
||||
code: 'let const = var'
|
||||
}, data)
|
||||
|
||||
request.post { uri: getURL('/db/level.system'), json: data }, (err, res) ->
|
||||
return done(err) if err
|
||||
expect(res.statusCode).toBe(200)
|
||||
LevelSystem.findById(res.body._id).exec done
|
||||
|
||||
makePoll: Promise.promisify (data, sources, done) ->
|
||||
args = Array.from(arguments)
|
||||
[done, [data, sources]] = [args.pop(), args]
|
||||
|
||||
data = _.extend({}, {
|
||||
name: _.uniqueId('Poll ')
|
||||
permissions: [{target: mw.lastLogin.id, access: 'owner'}]
|
||||
}, data)
|
||||
|
||||
request.post { uri: getURL('/db/poll'), json: data }, (err, res) ->
|
||||
return done(err) if err
|
||||
expect(res.statusCode).toBe(200)
|
||||
Poll.findById(res.body._id).exec done
|
||||
|
||||
makeThangType: Promise.promisify (data, sources, done) ->
|
||||
args = Array.from(arguments)
|
||||
[done, [data, sources]] = [args.pop(), args]
|
||||
|
||||
data = _.extend({}, {
|
||||
name: _.uniqueId('Thang Type ')
|
||||
permissions: [{target: mw.lastLogin.id, access: 'owner'}]
|
||||
}, data)
|
||||
|
||||
request.post { uri: getURL('/db/thang.type'), json: data }, (err, res) ->
|
||||
return done(err) if err
|
||||
expect(res.statusCode).toBe(200)
|
||||
ThangType.findById(res.body._id).exec done
|
||||
|
||||
makeAchievement: Promise.promisify (data, sources, done) ->
|
||||
args = Array.from(arguments)
|
||||
[done, [data, sources]] = [args.pop(), args]
|
||||
|
|
Loading…
Reference in a new issue