mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-04 12:51:12 -05:00
300c81e72b
* Restrict patch handling properly * Fix #3860, CS 2 description * i18nCoverage is updated when new translations are auto-accepted * Course patches are listed on PendingPatchesView properly * 'Artisan' permission allows editing course translations
147 lines
5.5 KiB
CoffeeScript
147 lines
5.5 KiB
CoffeeScript
errors = require '../commons/errors'
|
|
log = require 'winston'
|
|
wrap = require 'co-express'
|
|
database = require '../commons/database'
|
|
mongoose = require 'mongoose'
|
|
Campaign = require '../models/Campaign'
|
|
CourseInstance = require '../models/CourseInstance'
|
|
Classroom = require '../models/Classroom'
|
|
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 =
|
|
|
|
fetchLevelSolutions: wrap (req, res) ->
|
|
unless req.user?.isTeacher() or req.user?.isAdmin()
|
|
log.debug "courses.fetchLevelSolutions: level solutions only for teachers, (#{req.user?.id})"
|
|
throw new errors.Forbidden()
|
|
|
|
course = yield database.getDocFromHandle(req, Course)
|
|
throw new errors.NotFound('Course not found.') unless course
|
|
|
|
campaign = yield Campaign.findById course.get('campaignID')
|
|
throw new errors.NotFound('Campaign not found.') unless campaign
|
|
|
|
# TODO: why does campaign.get('levels') return opposite order from direct db query?
|
|
sortedLevelIDs = _.keys campaign.get('levels')
|
|
sortedLevelIDs.reverse()
|
|
|
|
levelOriginals = (mongoose.Types.ObjectId(levelID) for levelID in sortedLevelIDs)
|
|
query = { original: { $in: levelOriginals }, slug: { $exists: true }}
|
|
select = {documentation: 1, intro: 1, name: 1, original: 1, slug: 1, thangs: 1, i18n: 1}
|
|
levels = yield Level.find(query).select(select).lean()
|
|
levels.sort((a, b) -> sortedLevelIDs.indexOf(a.original + '') - sortedLevelIDs.indexOf(b.original + ''))
|
|
res.status(200).send(levels)
|
|
|
|
fetchNextLevel: wrap (req, res) ->
|
|
levelOriginal = req.params.levelOriginal
|
|
if not database.isID(levelOriginal)
|
|
throw new errors.UnprocessableEntity('Invalid level original ObjectId')
|
|
|
|
course = yield database.getDocFromHandle(req, Course)
|
|
if not course
|
|
throw new errors.NotFound('Course not found.')
|
|
|
|
campaign = yield Campaign.findById course.get('campaignID')
|
|
if not campaign
|
|
throw new errors.NotFound('Campaign not found.')
|
|
|
|
levels = _.values(campaign.get('levels'))
|
|
levels = _.sortBy(levels, 'campaignIndex')
|
|
|
|
nextLevelOriginal = null
|
|
foundLevelOriginal = false
|
|
for level, index in levels
|
|
if level.original.toString() is levelOriginal
|
|
foundLevelOriginal = true
|
|
nextLevelOriginal = levels[index+1]?.original
|
|
break
|
|
|
|
if not foundLevelOriginal
|
|
throw new errors.NotFound('Level original ObjectId not found in that course')
|
|
|
|
if not nextLevelOriginal
|
|
return res.status(200).send({})
|
|
|
|
dbq = Level.findOne({original: mongoose.Types.ObjectId(nextLevelOriginal)})
|
|
|
|
dbq.sort({ 'version.major': -1, 'version.minor': -1 })
|
|
dbq.select(parse.getProjectFromReq(req))
|
|
level = yield dbq
|
|
level = level.toObject({req: req})
|
|
res.status(200).send(level)
|
|
|
|
get: (Model, options={}) -> wrap (req, res) ->
|
|
query = {}
|
|
if req.query.releasePhase
|
|
query.releasePhase = req.query.releasePhase
|
|
dbq = Model.find(query)
|
|
dbq.select(parse.getProjectFromReq(req))
|
|
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']
|