2014-07-01 10:16:26 +08:00
Level = require ' ./Level '
Session = require ' ./sessions/LevelSession '
2014-03-19 19:42:42 -07:00
User = require ' ../users/User '
2014-07-01 10:16:26 +08:00
SessionHandler = require ' ./sessions/level_session_handler '
Feedback = require ' ./feedbacks/LevelFeedback '
Handler = require ' ../commons/Handler '
mongoose = require ' mongoose '
2014-04-14 14:28:28 -07:00
async = require ' async '
2015-01-14 11:09:01 -08:00
utils = require ' ../lib/utils '
2015-01-18 16:29:44 -08:00
log = require ' winston '
2014-07-01 10:16:26 +08:00
2014-01-03 10:32:13 -08:00
LevelHandler = class LevelHandler extends Handler
modelClass: Level
2014-04-12 10:51:02 -07:00
jsonSchema: require ' ../../app/schemas/models/level '
2014-01-03 10:32:13 -08:00
editableProperties: [
' description '
' documentation '
' background '
' nextLevel '
' scripts '
' thangs '
' systems '
' victory '
' name '
' i18n '
' icon '
2014-02-20 11:42:01 -08:00
' goals '
2014-03-07 13:14:27 -08:00
' type '
' showsGuide '
2014-07-17 09:12:21 -07:00
' banner '
' employerDescription '
2014-09-21 22:10:52 -07:00
' terrain '
2014-10-27 17:11:48 -07:00
' i18nCoverage '
2014-11-12 15:00:24 -08:00
' loadingTip '
2014-12-03 11:46:03 -08:00
' requiresSubscription '
2014-12-22 16:21:57 -05:00
' adventurer '
' practice '
2014-12-28 13:25:20 -08:00
' adminOnly '
2014-12-16 17:46:24 -08:00
' disableSpaces '
' hidesSubmitUntilRun '
' hidesPlayButton '
' hidesRunShortcut '
' hidesHUD '
' hidesSay '
' hidesCodeToolbar '
' hidesRealTimePlayback '
' backspaceThrottle '
' lockDefaultCode '
' moveRightLoopSnippet '
' realTimeSpeedFactor '
' autocompleteFontSizePx '
' requiredCode '
' suspectCode '
' requiredGear '
' restrictedGear '
' allowedHeroes '
2014-12-20 20:01:07 -08:00
' tasks '
2014-12-28 13:25:20 -08:00
' helpVideos '
' campaign '
2015-01-05 10:44:17 -08:00
' replayable '
2015-01-10 09:33:36 -08:00
' buildTime '
2014-01-03 10:32:13 -08:00
]
2014-02-14 14:55:30 -08:00
postEditableProperties: [ ' name ' ]
2014-01-03 10:32:13 -08:00
getByRelationship: (req, res, args...) ->
return @ getSession ( req , res , args [ 0 ] ) if args [ 1 ] is ' session '
2014-02-07 15:51:05 -08:00
return @ getLeaderboard ( req , res , args [ 0 ] ) if args [ 1 ] is ' leaderboard '
2014-03-20 14:40:17 -07:00
return @ getMyLeaderboardRank ( req , res , args [ 0 ] ) if args [ 1 ] is ' leaderboard_rank '
2014-03-02 13:24:41 -08:00
return @ getMySessions ( req , res , args [ 0 ] ) if args [ 1 ] is ' my_sessions '
2014-01-03 10:32:13 -08:00
return @ getFeedback ( req , res , args [ 0 ] ) if args [ 1 ] is ' feedback '
2014-08-30 23:04:45 -07:00
return @ getAllFeedback ( req , res , args [ 0 ] ) if args [ 1 ] is ' all_feedback '
2014-07-01 10:16:26 +08:00
return @ getRandomSessionPair ( req , res , args [ 0 ] ) if args [ 1 ] is ' random_session_pair '
2014-03-23 09:30:01 -07:00
return @ getLeaderboardFacebookFriends ( req , res , args [ 0 ] ) if args [ 1 ] is ' leaderboard_facebook_friends '
return @ getLeaderboardGPlusFriends ( req , res , args [ 0 ] ) if args [ 1 ] is ' leaderboard_gplus_friends '
2014-04-04 13:38:36 -07:00
return @ getHistogramData ( req , res , args [ 0 ] ) if args [ 1 ] is ' histogram_data '
2014-04-10 11:42:22 -07:00
return @ checkExistence ( req , res , args [ 0 ] ) if args [ 1 ] is ' exists '
2014-08-27 22:23:24 -07:00
return @ getPlayCountsBySlugs ( req , res ) if args [ 1 ] is ' play_counts '
2014-12-31 12:25:18 -08:00
return @ getLevelPlaytimesBySlugs ( req , res ) if args [ 1 ] is ' playtime_averages '
2014-04-11 10:33:22 -07:00
super ( arguments . . . )
2014-01-03 10:32:13 -08:00
2014-02-14 14:55:30 -08:00
fetchLevelByIDAndHandleErrors: (id, req, res, callback) ->
2014-01-03 10:32:13 -08:00
@ getDocumentForIdOrSlug id , (err, level) =>
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless level ?
2014-09-19 02:26:18 -07:00
return @ sendForbiddenError ( res ) unless @ hasAccessToDocument ( req , level , ' get ' )
2014-02-14 14:55:30 -08:00
callback err , level
2014-01-03 10:32:13 -08:00
2014-02-14 14:55:30 -08:00
getSession: (req, res, id) ->
2014-02-24 20:27:38 -08:00
return @ sendNotFoundError ( res ) unless req . user
2014-02-14 14:55:30 -08:00
@ fetchLevelByIDAndHandleErrors id , req , res , (err, level) =>
sessionQuery =
level:
original: level . original . toString ( )
majorVersion: level . version . major
2014-01-03 10:32:13 -08:00
creator: req . user . id
2014-02-07 11:50:40 -08:00
2014-02-14 14:55:30 -08:00
if req . query . team ?
sessionQuery.team = req . query . team
2014-03-07 15:15:49 -08:00
2014-01-03 10:32:13 -08:00
Session . findOne ( sessionQuery ) . exec (err, doc) =>
return @ sendDatabaseError ( res , err ) if err
2014-02-14 14:55:30 -08:00
return @ sendSuccess ( res , doc ) if doc ?
2014-12-28 13:25:20 -08:00
return @ sendPaymentRequiredError ( res , err ) if ( not req . user . isPremium ( ) ) and level . get ( ' requiresSubscription ' ) and not level . get ( ' adventurer ' )
2014-02-14 14:55:30 -08:00
@ createAndSaveNewSession sessionQuery , req , res
2014-01-03 10:32:13 -08:00
2014-02-14 14:55:30 -08:00
createAndSaveNewSession: (sessionQuery, req, res) =>
initVals = sessionQuery
initVals.state =
2014-07-01 10:16:26 +08:00
complete: false
2014-02-14 14:55:30 -08:00
scripts:
2014-07-01 10:16:26 +08:00
currentScript: null # will not save empty objects
2014-02-14 14:55:30 -08:00
initVals.permissions = [
{
2014-07-01 10:16:26 +08:00
target: req . user . id
access: ' owner '
2014-02-14 14:55:30 -08:00
}
{
2014-07-01 10:16:26 +08:00
target: ' public '
access: ' write '
2014-02-14 14:55:30 -08:00
}
]
2014-09-25 13:17:41 -07:00
initVals.codeLanguage = req . user . get ( ' aceConfig ' ) ? . language ? ' python '
2014-02-14 14:55:30 -08:00
session = new Session ( initVals )
session . save (err) =>
2014-02-07 11:50:40 -08:00
return @ sendDatabaseError ( res , err ) if err
2014-02-14 14:55:30 -08:00
@ sendSuccess ( res , @ formatEntity ( req , session ) )
# TODO: tying things like @formatEntity and saveChangesToDocument don't make sense
# associated with the handler, because the handler might return a different type
# of model, like in this case. Refactor to move that logic to the model instead.
2014-02-07 11:50:40 -08:00
2014-03-10 10:56:33 -07:00
getMySessions: (req, res, slugOrID) ->
2014-11-22 18:40:28 -08:00
return @ sendForbiddenError ( res ) if not req . user
2014-03-10 10:56:33 -07:00
findParameters = { }
if Handler . isID slugOrID
2014-07-01 10:16:26 +08:00
findParameters [ ' _id ' ] = slugOrID
2014-03-10 10:56:33 -07:00
else
2014-07-01 10:16:26 +08:00
findParameters [ ' slug ' ] = slugOrID
2014-03-10 10:56:33 -07:00
selectString = ' original version.major permissions '
query = Level . findOne ( findParameters )
. select ( selectString )
. lean ( )
2014-05-14 21:54:36 -07:00
2014-03-10 10:56:33 -07:00
query . exec (err, level) =>
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless level ?
2014-02-14 14:55:30 -08:00
sessionQuery =
level:
original: level . original . toString ( )
majorVersion: level . version . major
2014-03-02 13:24:41 -08:00
creator: req . user . _id + ' '
2014-05-14 21:54:36 -07:00
2014-03-10 10:56:33 -07:00
query = Session . find ( sessionQuery ) . select ( ' -screenshot ' )
2014-02-14 14:55:30 -08:00
query . exec (err, results) =>
if err then @ sendDatabaseError ( res , err ) else @ sendSuccess res , results
2014-05-14 21:54:36 -07:00
2014-07-01 10:16:26 +08:00
getHistogramData: (req, res, slug) ->
2014-04-04 13:38:36 -07:00
query = Session . aggregate [
2014-07-01 10:16:26 +08:00
{ $match: { ' levelID ' : slug , ' submitted ' : true , ' team ' : req . query . team } }
2014-04-04 13:38:36 -07:00
{ $project: { totalScore: 1 , _id: 0 } }
]
2014-05-14 21:54:36 -07:00
2014-04-04 13:38:36 -07:00
query . exec (err, data) =>
if err ? then return @ sendDatabaseError res , err
2014-07-01 10:16:26 +08:00
valueArray = _ . pluck data , ' totalScore '
2014-04-04 13:38:36 -07:00
@ sendSuccess res , valueArray
2014-05-14 21:54:36 -07:00
2014-04-10 11:42:22 -07:00
checkExistence: (req, res, slugOrID) ->
findParameters = { }
if Handler . isID slugOrID
2014-07-01 10:16:26 +08:00
findParameters [ ' _id ' ] = slugOrID
2014-04-10 11:42:22 -07:00
else
2014-07-01 10:16:26 +08:00
findParameters [ ' slug ' ] = slugOrID
2014-04-10 11:42:22 -07:00
selectString = ' original version.major permissions '
query = Level . findOne ( findParameters )
. select ( selectString )
. lean ( )
query . exec (err, level) =>
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless level ?
2014-07-01 10:16:26 +08:00
res . send ( { ' exists ' : true } )
2014-04-10 11:42:22 -07:00
res . end ( )
2014-02-13 15:14:08 -08:00
2014-02-14 14:55:30 -08:00
getLeaderboard: (req, res, id) ->
2014-03-20 14:40:17 -07:00
sessionsQueryParameters = @ makeLeaderboardQueryParameters ( req , id )
2014-02-13 15:14:08 -08:00
2014-02-14 14:55:30 -08:00
sortParameters =
2014-07-01 10:16:26 +08:00
' totalScore ' : req . query . order
2014-07-13 20:19:51 -07:00
selectProperties = [ ' totalScore ' , ' creatorName ' , ' creator ' , ' submittedCodeLanguage ' ]
2014-05-14 21:54:36 -07:00
2014-02-14 14:55:30 -08:00
query = Session
. find ( sessionsQueryParameters )
. limit ( req . query . limit )
2014-03-14 11:57:17 -07:00
. sort ( sortParameters )
2014-02-14 14:55:30 -08:00
. select ( selectProperties . join ' ' )
query . exec (err, resultSessions) =>
2014-01-03 10:32:13 -08:00
return @ sendDatabaseError ( res , err ) if err
2014-02-14 14:55:30 -08:00
resultSessions ? = [ ]
@ sendSuccess res , resultSessions
2014-03-19 19:42:42 -07:00
2014-03-20 14:40:17 -07:00
getMyLeaderboardRank: (req, res, id) ->
req.query.order = 1
sessionsQueryParameters = @ makeLeaderboardQueryParameters ( req , id )
Session . count sessionsQueryParameters , (err, count) =>
return @ sendDatabaseError ( res , err ) if err
res . send JSON . stringify ( count + 1 )
makeLeaderboardQueryParameters: (req, id) ->
@ validateLeaderboardRequestParameters req
[ original , version ] = id . split ' . '
version = parseInt ( version ) ? 0
scoreQuery = { }
2014-07-01 10:16:26 +08:00
scoreQuery [ if req . query . order is 1 then ' $gt ' else ' $lt ' ] = req . query . scoreOffset
2014-03-20 14:40:17 -07:00
query =
level:
original: original
majorVersion: version
team: req . query . team
totalScore: scoreQuery
submitted: true
query
validateLeaderboardRequestParameters: (req) ->
req.query.order = parseInt ( req . query . order ) ? - 1
req.query.scoreOffset = parseFloat ( req . query . scoreOffset ) ? 100000
req . query . team ? = ' humans '
req.query.limit = parseInt ( req . query . limit ) ? 20
2014-03-19 19:42:42 -07:00
2014-03-23 09:30:01 -07:00
getLeaderboardFacebookFriends: (req, res, id) -> @ getLeaderboardFriends ( req , res , id , ' facebookID ' )
getLeaderboardGPlusFriends: (req, res, id) -> @ getLeaderboardFriends ( req , res , id , ' gplusID ' )
getLeaderboardFriends: (req, res, id, serviceProperty) ->
2014-03-19 19:42:42 -07:00
friendIDs = req . body . friendIDs or [ ]
return res . send ( [ ] ) unless friendIDs . length
2014-03-23 09:30:01 -07:00
q = { }
2014-07-01 10:16:26 +08:00
q [ serviceProperty ] = { $in: friendIDs }
2014-03-23 09:30:01 -07:00
query = User . find ( q ) . select ( " #{ serviceProperty } name " ) . lean ( )
2014-03-19 19:42:42 -07:00
query . exec (err, userResults) ->
return res . send ( [ ] ) unless userResults . length
[ id , version ] = id . split ( ' . ' )
userIDs = ( r . _id + ' ' for r in userResults )
2014-07-01 10:16:26 +08:00
q = { ' level.original ' : id , ' level.majorVersion ' : parseInt ( version ) , creator: { $in: userIDs } , totalScore: { $exists: true } }
2014-03-19 19:42:42 -07:00
query = Session . find ( q )
2014-03-23 09:30:01 -07:00
. select ( ' creator creatorName totalScore team ' )
. lean ( )
2014-03-19 19:42:42 -07:00
query . exec (err, sessionResults) ->
return res . send ( [ ] ) unless sessionResults . length
userMap = { }
2014-03-23 09:30:01 -07:00
userMap [ u . _id ] = u [ serviceProperty ] for u in userResults
session [ serviceProperty ] = userMap [ session . creator ] for session in sessionResults
2014-03-19 19:42:42 -07:00
res . send ( sessionResults )
2014-04-14 14:28:28 -07:00
2014-03-14 12:54:52 -07:00
getRandomSessionPair: (req, res, slugOrID) ->
2014-03-14 11:30:04 -07:00
findParameters = { }
2014-03-14 12:54:52 -07:00
if Handler . isID slugOrID
2014-07-01 10:16:26 +08:00
findParameters [ ' _id ' ] = slugOrID
2014-03-14 12:54:52 -07:00
else
2014-07-01 10:16:26 +08:00
findParameters [ ' slug ' ] = slugOrID
2014-03-14 12:54:52 -07:00
selectString = ' original version '
query = Level . findOne ( findParameters )
. select ( selectString )
. lean ( )
2014-03-14 11:30:04 -07:00
2014-03-14 12:54:52 -07:00
query . exec (err, level) =>
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless level ?
2014-04-14 14:28:28 -07:00
2014-03-14 12:54:52 -07:00
sessionsQueryParameters =
level:
original: level . original . toString ( )
majorVersion: level . version . major
2014-07-01 10:16:26 +08:00
submitted: true
2014-04-14 14:28:28 -07:00
2014-07-01 10:16:26 +08:00
query = Session . find ( sessionsQueryParameters ) . distinct ( ' team ' )
2014-04-14 14:28:28 -07:00
query . exec (err, teams) =>
return @ sendDatabaseError res , err if err ? or not teams
findTop20Players = (sessionQueryParams, team, cb) ->
2014-07-01 10:16:26 +08:00
sessionQueryParams [ ' team ' ] = team
2014-04-14 14:28:28 -07:00
Session . aggregate [
{ $match: sessionQueryParams }
2014-07-01 10:16:26 +08:00
{ $project: { ' totalScore ' : 1 } }
{ $sort: { ' totalScore ' : - 1 } }
2014-04-14 14:28:28 -07:00
{ $limit: 20 }
] , cb
async . map teams , findTop20Players . bind ( @ , sessionsQueryParameters ) , (err, map) =>
if err ? then return @ sendDatabaseError ( res , err )
sessions = [ ]
for mapItem in map
sessions . push _ . sample ( mapItem )
2014-07-01 10:16:26 +08:00
if map . length != 2 then return @ sendDatabaseError res , ' There aren \' t sessions of 2 teams, so cannot choose random opponents! '
2014-04-14 14:28:28 -07:00
@ sendSuccess res , sessions
2014-08-30 23:04:45 -07:00
getFeedback: (req, res, levelID) ->
2014-02-24 20:27:38 -08:00
return @ sendNotFoundError ( res ) unless req . user
2014-08-30 23:04:45 -07:00
@ doGetFeedback req , res , levelID , false
getAllFeedback: (req, res, levelID) ->
2014-11-22 18:40:28 -08:00
return @ sendNotFoundError ( res ) unless req . user
2014-08-30 23:04:45 -07:00
@ doGetFeedback req , res , levelID , true
doGetFeedback: (req, res, levelID, multiple) ->
@ fetchLevelByIDAndHandleErrors levelID , req , res , (err, level) =>
2014-02-14 14:55:30 -08:00
feedbackQuery =
2014-01-03 10:32:13 -08:00
' level.original ' : level . original . toString ( )
' level.majorVersion ' : level . version . major
2014-08-30 23:04:45 -07:00
feedbackQuery.creator = mongoose . Types . ObjectId ( req . user . id . toString ( ) ) unless multiple
fn = if multiple then ' find ' else ' findOne '
Feedback [ fn ] ( feedbackQuery ) . exec (err, result) =>
2014-01-03 10:32:13 -08:00
return @ sendDatabaseError ( res , err ) if err
2014-08-30 23:04:45 -07:00
return @ sendNotFoundError ( res ) unless result ?
@ sendSuccess ( res , result )
2014-01-03 10:32:13 -08:00
2014-08-27 22:23:24 -07:00
getPlayCountsBySlugs: (req, res) ->
# This is hella slow (4s on my box), so relying on some dumb caching for it.
# If we can't make this faster with indexing or something, we might want to maintain the counts another way.
levelIDs = req . query . ids or req . body . ids
2015-01-08 17:17:34 -08:00
return @ sendSuccess res , [ ] unless levelIDs ?
2014-08-27 22:23:24 -07:00
@ playCountCache ? = { }
@ playCountCachedSince ? = new Date ( )
if ( new Date ( ) ) - @ playCountCachedSince > 86400 * 1000 # Dumb cache expiration
@playCountCache = { }
2015-01-01 12:01:43 -08:00
@playCountCachedSince = new Date ( )
2014-08-27 22:23:24 -07:00
cacheKey = levelIDs . join ' , '
if playCounts = @ playCountCache [ cacheKey ]
return @ sendSuccess res , playCounts
query = Session . aggregate [
2014-08-29 23:09:38 -07:00
{ $match: { levelID: { $in: levelIDs } } }
{ $group: { _id: " $levelID " , playtime: { $sum: " $playtime " } , sessions: { $sum: 1 } } }
2014-08-27 22:23:24 -07:00
{ $sort: { sessions: - 1 } }
]
query . exec (err, data) =>
if err ? then return @ sendDatabaseError res , err
@ playCountCache [ cacheKey ] = data
@ sendSuccess res , data
2014-09-19 03:52:34 -07:00
hasAccessToDocument: (req, document, method=null) ->
2014-11-25 16:20:41 -08:00
method ? = req . method
2014-09-19 08:17:30 -07:00
return true if method is null or method is ' get '
2014-09-19 03:52:34 -07:00
super ( req , document , method )
2014-08-27 22:23:24 -07:00
2014-12-31 12:25:18 -08:00
getLevelPlaytimesBySlugs: (req, res) ->
# Returns an array of per-day level average playtimes
# Parameters:
# slugs - array of level slugs
# startDay - Inclusive, optional, e.g. '2014-12-14'
# endDay - Exclusive, optional, e.g. '2014-12-16'
2015-01-01 12:01:43 -08:00
# TODO: An uncached call takes about 5s for dungeons-of-kithgard locally
2014-12-31 12:25:18 -08:00
# TODO: This is very similar to getLevelCompletionsBySlugs(), time to generalize analytics APIs?
2015-01-26 14:58:35 -08:00
# TODO: exclude admin data
2014-12-31 12:25:18 -08:00
levelSlugs = req . query . slugs or req . body . slugs
startDay = req . query . startDay or req . body . startDay
endDay = req . query . endDay or req . body . endDay
return @ sendSuccess res , [ ] unless levelSlugs ?
2015-01-18 16:29:44 -08:00
# log.warn "playtime_averages levelSlugs='#{levelSlugs}' startDay=#{startDay} endDay=#{endDay}"
2014-12-31 12:25:18 -08:00
# Cache results for 1 day
@ levelPlaytimesCache ? = { }
@ levelPlaytimesCachedSince ? = new Date ( )
if ( new Date ( ) ) - @ levelPlaytimesCachedSince > 86400 * 1000 # Dumb cache expiration
@levelPlaytimesCache = { }
2015-01-01 12:01:43 -08:00
@levelPlaytimesCachedSince = new Date ( )
2014-12-31 12:25:18 -08:00
cacheKey = levelSlugs . join ( ' , ' )
cacheKey += ' s ' + startDay if startDay ?
cacheKey += ' e ' + endDay if endDay ?
return @ sendSuccess res , levelPlaytimes if levelPlaytimes = @ levelPlaytimesCache [ cacheKey ]
# Build query
2014-12-31 13:19:46 -08:00
match = { $match: { $and: [ { " state.complete " : true } , { " playtime " : { $gt: 0 } } , { levelID: { $in: levelSlugs } } ] } }
2015-01-14 11:09:01 -08:00
match [ " $match " ] [ " $and " ] . push _id: { $gte: utils . objectIdFromTimestamp ( startDay + " T00:00:00.000Z " ) } if startDay ?
match [ " $match " ] [ " $and " ] . push _id: { $lt: utils . objectIdFromTimestamp ( endDay + " T00:00:00.000Z " ) } if endDay ?
project = { " $project " : { " _id " : 0 , " levelID " : 1 , " playtime " : 1 , " created " : { " $concat " : [ { " $substr " : [ " $created " , 0 , 10 ] } ] } } }
2014-12-31 12:25:18 -08:00
group = { " $group " : { " _id " : { " created " : " $created " , " level " : " $levelID " } , " average " : { " $avg " : " $playtime " } } }
query = Session . aggregate match , project , group
query . exec (err, data) =>
if err ? then return @ sendDatabaseError res , err
# Build list of level average playtimes
playtimes = [ ]
for item in data
2015-01-05 10:44:17 -08:00
playtimes . push
2014-12-31 13:19:46 -08:00
level: item . _id . level
created: item . _id . created
2014-12-31 12:25:18 -08:00
average: item . average
@ levelPlaytimesCache [ cacheKey ] = playtimes
@ sendSuccess res , playtimes
2014-01-03 10:32:13 -08:00
module.exports = new LevelHandler ( )