2016-04-06 13:56:06 -04:00
Level = require ' ./../models/Level '
Session = require ' ./../models/LevelSession '
User = require ' ../models/User '
2016-04-07 12:40:53 -04:00
SessionHandler = require ' ./level_session_handler '
2016-04-06 13:56:06 -04:00
Feedback = require ' ./../models/LevelFeedback '
2014-06-30 22:16:26 -04:00
Handler = require ' ../commons/Handler '
mongoose = require ' mongoose '
2014-04-14 17:28:28 -04:00
async = require ' async '
2015-01-14 14:09:01 -05:00
utils = require ' ../lib/utils '
2015-01-18 19:29:44 -05:00
log = require ' winston '
2016-04-06 13:56:06 -04:00
Campaign = require ' ../models/Campaign '
2016-03-30 16:57:19 -04:00
Course = require ' ../models/Course '
2016-04-06 13:56:06 -04:00
CourseInstance = require ' ../models/CourseInstance '
Classroom = require ' ../models/Classroom '
2014-06-30 22:16:26 -04:00
2014-01-03 13:32:13 -05:00
LevelHandler = class LevelHandler extends Handler
modelClass: Level
2014-04-12 13:51:02 -04:00
jsonSchema: require ' ../../app/schemas/models/level '
2014-01-03 13:32:13 -05:00
editableProperties: [
' description '
' documentation '
' background '
' nextLevel '
' scripts '
' thangs '
' systems '
' victory '
' name '
' i18n '
' icon '
2014-02-20 14:42:01 -05:00
' goals '
2014-03-07 16:14:27 -05:00
' type '
' showsGuide '
2014-07-17 12:12:21 -04:00
' banner '
' employerDescription '
2014-09-22 01:10:52 -04:00
' terrain '
2014-10-27 20:11:48 -04:00
' i18nCoverage '
2014-11-12 18:00:24 -05:00
' loadingTip '
2014-12-03 14:46:03 -05:00
' requiresSubscription '
2014-12-22 16:21:57 -05:00
' adventurer '
' practice '
2014-12-28 16:25:20 -05:00
' adminOnly '
2014-12-16 20:46:24 -05:00
' disableSpaces '
' hidesSubmitUntilRun '
' hidesPlayButton '
' hidesRunShortcut '
' hidesHUD '
' hidesSay '
' hidesCodeToolbar '
' hidesRealTimePlayback '
' backspaceThrottle '
' lockDefaultCode '
' moveRightLoopSnippet '
' realTimeSpeedFactor '
' autocompleteFontSizePx '
' requiredCode '
' suspectCode '
' requiredGear '
' restrictedGear '
' allowedHeroes '
2014-12-20 23:01:07 -05:00
' tasks '
2014-12-28 16:25:20 -05:00
' helpVideos '
' campaign '
2015-11-24 11:56:28 -05:00
' campaignIndex '
2015-01-05 13:44:17 -05:00
' replayable '
2015-01-10 12:33:36 -05:00
' buildTime '
2015-01-31 00:36:36 -05:00
' scoreTypes '
2015-06-30 14:27:58 -04:00
' concepts '
2016-02-10 17:16:54 -05:00
' picoCTFProblem '
2014-01-03 13:32:13 -05:00
]
2014-02-14 17:55:30 -05:00
postEditableProperties: [ ' name ' ]
2014-01-03 13:32:13 -05:00
getByRelationship: (req, res, args...) ->
return @ getSession ( req , res , args [ 0 ] ) if args [ 1 ] is ' session '
2014-02-07 18:51:05 -05:00
return @ getLeaderboard ( req , res , args [ 0 ] ) if args [ 1 ] is ' leaderboard '
2014-03-20 17:40:17 -04:00
return @ getMyLeaderboardRank ( req , res , args [ 0 ] ) if args [ 1 ] is ' leaderboard_rank '
2014-03-02 16:24:41 -05:00
return @ getMySessions ( req , res , args [ 0 ] ) if args [ 1 ] is ' my_sessions '
2014-01-03 13:32:13 -05:00
return @ getFeedback ( req , res , args [ 0 ] ) if args [ 1 ] is ' feedback '
2014-08-31 02:04:45 -04:00
return @ getAllFeedback ( req , res , args [ 0 ] ) if args [ 1 ] is ' all_feedback '
2014-06-30 22:16:26 -04:00
return @ getRandomSessionPair ( req , res , args [ 0 ] ) if args [ 1 ] is ' random_session_pair '
2014-03-23 12:30:01 -04: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 16:38:36 -04:00
return @ getHistogramData ( req , res , args [ 0 ] ) if args [ 1 ] is ' histogram_data '
2014-04-10 14:42:22 -04:00
return @ checkExistence ( req , res , args [ 0 ] ) if args [ 1 ] is ' exists '
2014-08-28 01:23:24 -04:00
return @ getPlayCountsBySlugs ( req , res ) if args [ 1 ] is ' play_counts '
2014-12-31 15:25:18 -05:00
return @ getLevelPlaytimesBySlugs ( req , res ) if args [ 1 ] is ' playtime_averages '
2015-01-31 00:36:36 -05:00
return @ getTopScores ( req , res , args [ 0 ] , args [ 2 ] , args [ 3 ] ) if args [ 1 ] is ' top_scores '
2014-04-11 13:33:22 -04:00
super ( arguments . . . )
2014-01-03 13:32:13 -05:00
2014-02-14 17:55:30 -05:00
fetchLevelByIDAndHandleErrors: (id, req, res, callback) ->
2015-03-23 18:26:44 -04:00
# TODO: this could probably be faster with projections, right?
2014-01-03 13:32:13 -05:00
@ getDocumentForIdOrSlug id , (err, level) =>
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless level ?
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless @ hasAccessToDocument ( req , level , ' get ' )
2014-02-14 17:55:30 -05:00
callback err , level
2014-01-03 13:32:13 -05:00
2014-02-14 17:55:30 -05:00
getSession: (req, res, id) ->
2014-02-24 23:27:38 -05:00
return @ sendNotFoundError ( res ) unless req . user
2014-02-14 17:55:30 -05:00
@ fetchLevelByIDAndHandleErrors id , req , res , (err, level) =>
sessionQuery =
level:
original: level . original . toString ( )
majorVersion: level . version . major
2014-01-03 13:32:13 -05:00
creator: req . user . id
2014-02-07 14:50:40 -05:00
2014-02-14 17:55:30 -05:00
if req . query . team ?
sessionQuery.team = req . query . team
2014-03-07 18:15:49 -05:00
2014-01-03 13:32:13 -05:00
Session . findOne ( sessionQuery ) . exec (err, doc) =>
return @ sendDatabaseError ( res , err ) if err
2014-02-14 17:55:30 -05:00
return @ sendSuccess ( res , doc ) if doc ?
2016-02-09 13:13:11 -05:00
if level . get ( ' type ' ) in [ ' course ' , ' course-ladder ' ] or req . query . course ?
2015-10-06 14:08:03 -04:00
return @ makeOrRejectCourseLevelSession ( req , res , level , sessionQuery )
2015-10-09 11:05:34 -04:00
requiresSubscription = level . get ( ' requiresSubscription ' ) or ( req . user . isOnPremiumServer ( ) and level . get ( ' campaign ' ) and not ( level . slug in [ ' dungeons-of-kithgard ' , ' gems-in-the-deep ' , ' shadow-guard ' , ' forgetful-gemsmith ' , ' signs-and-portents ' , ' true-names ' ] ) )
2015-03-23 18:26:44 -04:00
canPlayAnyway = req . user . isPremium ( ) or level . get ' adventurer '
return @ sendPaymentRequiredError ( res , err ) if requiresSubscription and not canPlayAnyway
2014-02-14 17:55:30 -05:00
@ createAndSaveNewSession sessionQuery , req , res
2014-01-03 13:32:13 -05:00
2015-10-06 14:08:03 -04:00
makeOrRejectCourseLevelSession: (req, res, level, sessionQuery) ->
CourseInstance . find { members: req . user . get ( ' _id ' ) } , (err, courseInstances) =>
courseIDs = ( ci . get ( ' courseID ' ) for ci in courseInstances )
Course . find { _id: { $in: courseIDs } } , (err, courses) =>
campaignIDs = ( c . get ( ' campaignID ' ) for c in courses )
Campaign . find { _id: { $in: campaignIDs } } , (err, campaigns) =>
levelOriginals = ( _ . keys ( c . get ( ' levels ' ) ) for c in campaigns )
levelOriginals = _ . flatten ( levelOriginals )
2015-10-30 18:56:43 -04:00
originalString = level . get ( ' original ' ) . toString ( )
if originalString in levelOriginals
campaignStrings = ( campaign . id . toString ( ) for campaign in campaigns when campaign . get ( ' levels ' ) [ originalString ] )
courses = _ . filter ( courses , (course) -> course . get ( ' campaignID ' ) . toString ( ) in campaignStrings )
courseStrings = ( course . id . toString ( ) for course in courses )
courseInstances = _ . filter ( courseInstances , (courseInstance) -> courseInstance . get ( ' courseID ' ) . toString ( ) in courseStrings )
2015-11-17 19:09:29 -05:00
classroomIDs = ( courseInstance . get ( ' classroomID ' ) for courseInstance in courseInstances )
classroomIDs = _ . filter _ . uniq classroomIDs , false , (objectID='') -> objectID . toString ( )
if classroomIDs . length
Classroom . find ( { _id: { $in: classroomIDs } } ) . exec (err, classrooms) =>
aceConfigs = ( c . get ( ' aceConfig ' ) for c in classrooms )
aceConfig = _ . filter ( aceConfigs ) [ 0 ] or { }
req.codeLanguage = aceConfig . language
@ createAndSaveNewSession ( sessionQuery , req , res )
else
@ createAndSaveNewSession ( sessionQuery , req , res )
2015-10-06 14:08:03 -04:00
else
return @ sendPaymentRequiredError ( res , ' You must be in a course which includes this level to play it ' )
2014-02-14 17:55:30 -05:00
createAndSaveNewSession: (sessionQuery, req, res) =>
initVals = sessionQuery
initVals.state =
2014-06-30 22:16:26 -04:00
complete: false
2014-02-14 17:55:30 -05:00
scripts:
2014-06-30 22:16:26 -04:00
currentScript: null # will not save empty objects
2014-02-14 17:55:30 -05:00
initVals.permissions = [
{
2014-06-30 22:16:26 -04:00
target: req . user . id
access: ' owner '
2014-02-14 17:55:30 -05:00
}
{
2014-06-30 22:16:26 -04:00
target: ' public '
access: ' write '
2014-02-14 17:55:30 -05:00
}
]
2015-10-30 18:56:43 -04:00
initVals.codeLanguage = req . codeLanguage ? req . user . get ( ' aceConfig ' ) ? . language ? ' python '
2014-02-14 17:55:30 -05:00
session = new Session ( initVals )
session . save (err) =>
2014-02-07 14:50:40 -05:00
return @ sendDatabaseError ( res , err ) if err
2014-02-14 17:55:30 -05: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 14:50:40 -05:00
2014-03-10 13:56:33 -04:00
getMySessions: (req, res, slugOrID) ->
2014-11-22 21:40:28 -05:00
return @ sendForbiddenError ( res ) if not req . user
2014-03-10 13:56:33 -04:00
findParameters = { }
if Handler . isID slugOrID
2014-06-30 22:16:26 -04:00
findParameters [ ' _id ' ] = slugOrID
2014-03-10 13:56:33 -04:00
else
2014-06-30 22:16:26 -04:00
findParameters [ ' slug ' ] = slugOrID
2014-03-10 13:56:33 -04:00
selectString = ' original version.major permissions '
query = Level . findOne ( findParameters )
. select ( selectString )
. lean ( )
2014-05-15 00:54:36 -04:00
2014-03-10 13:56:33 -04:00
query . exec (err, level) =>
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless level ?
2014-02-14 17:55:30 -05:00
sessionQuery =
level:
original: level . original . toString ( )
majorVersion: level . version . major
2014-03-02 16:24:41 -05:00
creator: req . user . _id + ' '
2014-05-15 00:54:36 -04:00
2015-04-08 15:00:12 -04:00
query = Session . find ( sessionQuery ) . select ( ' -screenshot -transpiledCode ' )
2015-08-15 09:45:38 -04:00
# TODO: take out "code" as well, since that can get huge containing the transpiled code for the lat hero, and find another way of having the LadderSubmissionViews in the MyMatchesTab determine ranking readiness
2014-02-14 17:55:30 -05:00
query . exec (err, results) =>
if err then @ sendDatabaseError ( res , err ) else @ sendSuccess res , results
2014-05-15 00:54:36 -04:00
2015-10-30 12:25:58 -04:00
getHistogramData: (req, res, id) ->
match = @ makeLeaderboardQueryParameters req , id
delete match . totalScore
2015-08-21 06:28:25 -04:00
project = totalScore: 1 , _id: 0
2015-10-30 12:25:58 -04:00
league = req . query [ ' leagues.leagueID ' ]
2015-08-21 06:28:25 -04:00
project [ ' leagues.leagueID ' ] = project [ ' leagues.stats.totalScore ' ] = 1 if league
2015-02-26 21:32:59 -05:00
aggregate = Session . aggregate [
2015-08-15 09:45:38 -04:00
{ $match: match }
2015-08-21 06:28:25 -04:00
{ $project: project }
2014-04-04 16:38:36 -04:00
]
2015-12-05 11:18:36 -05:00
aggregate . cache ( 10 * 60 * 1000 ) unless league
2014-05-15 00:54:36 -04:00
2015-02-26 21:32:59 -05:00
aggregate . exec (err, data) =>
2014-04-04 16:38:36 -04:00
if err ? then return @ sendDatabaseError res , err
2015-08-21 06:28:25 -04:00
if league
valueArray = _ . pluck data , (session) -> _ . find ( session . leagues , leagueID: league ) ? . stats ? . totalScore or 10
else
valueArray = _ . pluck data , ' totalScore '
2014-04-04 16:38:36 -04:00
@ sendSuccess res , valueArray
2014-05-15 00:54:36 -04:00
2014-04-10 14:42:22 -04:00
checkExistence: (req, res, slugOrID) ->
findParameters = { }
if Handler . isID slugOrID
2014-06-30 22:16:26 -04:00
findParameters [ ' _id ' ] = slugOrID
2014-04-10 14:42:22 -04:00
else
2014-06-30 22:16:26 -04:00
findParameters [ ' slug ' ] = slugOrID
2014-04-10 14:42:22 -04: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-06-30 22:16:26 -04:00
res . send ( { ' exists ' : true } )
2014-04-10 14:42:22 -04:00
res . end ( )
2014-02-13 18:14:08 -05:00
2014-02-14 17:55:30 -05:00
getLeaderboard: (req, res, id) ->
2014-03-20 17:40:17 -04:00
sessionsQueryParameters = @ makeLeaderboardQueryParameters ( req , id )
2015-11-24 16:18:38 -05:00
sortParameters = totalScore: req . query . order
selectProperties = [ ' totalScore ' , ' creatorName ' , ' creator ' , ' submittedCodeLanguage ' , ' heroConfig ' , ' leagues.leagueID ' , ' leagues.stats.totalScore ' , ' submitDate ' , ' team ' ]
2014-02-14 17:55:30 -05:00
query = Session
. find ( sessionsQueryParameters )
. limit ( req . query . limit )
2014-03-14 14:57:17 -04:00
. sort ( sortParameters )
2014-02-14 17:55:30 -05:00
. select ( selectProperties . join ' ' )
2015-12-05 11:18:36 -05:00
query . cache ( 5 * 60 * 1000 ) if sessionsQueryParameters . totalScore . $lt is 1000000
2014-02-14 17:55:30 -05:00
query . exec (err, resultSessions) =>
2014-01-03 13:32:13 -05:00
return @ sendDatabaseError ( res , err ) if err
2014-02-14 17:55:30 -05:00
resultSessions ? = [ ]
2015-11-24 16:18:38 -05:00
leaderboardOptions = find: sessionsQueryParameters , limit: req . query . limit , sort: sortParameters , select: selectProperties
@ interleaveAILeaderboardSessions leaderboardOptions , resultSessions , (err, resultSessions) =>
return @ sendDatabaseError ( res , err ) if err
if league = req . query [ ' leagues.leagueID ' ]
resultSessions = _ . sortBy resultSessions , (session) -> _ . find ( session . get ( ' leagues ' ) , leagueID: league ) ? . stats . totalScore ? session . get ( ' totalScore ' ) / 2
resultSessions . reverse ( ) if sortParameters . totalScore is - 1
@ sendSuccess res , resultSessions
2014-03-19 22:42:42 -04:00
2014-03-20 17:40:17 -04: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-06-30 22:16:26 -04:00
scoreQuery [ if req . query . order is 1 then ' $gt ' else ' $lt ' ] = req . query . scoreOffset
2014-03-20 17:40:17 -04:00
query =
level:
original: original
majorVersion: version
team: req . query . team
totalScore: scoreQuery
submitted: true
2015-08-15 09:45:38 -04:00
query [ ' leagues.leagueID ' ] = league if league = req . query [ ' leagues.leagueID ' ]
2014-03-20 17:40:17 -04:00
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 22:42:42 -04:00
2015-11-24 16:18:38 -05:00
ladderBenchmarkAIs: [
' 564ba6cea33967be1312ae59 '
' 564ba830a33967be1312ae61 '
' 564ba91aa33967be1312ae65 '
' 564ba95ca33967be1312ae69 '
' 564ba9b7a33967be1312ae6d '
]
interleaveAILeaderboardSessions: (leaderboardOptions, sessions, cb) ->
return cb null , sessions unless leaderboardOptions . find [ ' leagues.leagueID ' ]
return cb null , sessions if leaderboardOptions . limit < 10 # Don't put them in when we're fetching sessions around another session
# Get our list of benchmark AI sessions
benchmarkSessions = Session
. find ( level: leaderboardOptions . find . level , creator: { $in: @ ladderBenchmarkAIs } )
. sort ( leaderboardOptions . sort )
. select ( leaderboardOptions . select . join ' ' )
2015-12-05 11:18:36 -05:00
. cache ( 30 * 60 * 1000 )
2015-11-24 16:18:38 -05:00
. exec (err, aiSessions) ->
return cb err if err
matchingAISessions = _ . filter aiSessions , (aiSession) ->
return false unless aiSession . get ( ' team ' ) is leaderboardOptions . find . team
return false if $gt = leaderboardOptions . find . totalScore . $gt and aiSession . get ( ' totalScore ' ) <= $gt
return false if $lt = leaderboardOptions . find . totalScore . $lt and aiSession . get ( ' totalScore ' ) >= $lt
true
# TODO: these aren't real league scores for AIs, but rather the general leaderboard scores, which will make most AI scores artificially high. So we divide by 2 for AI scores not part of the league. Pretty weak, I know. Eventually we'd want them to actually play league matches as if they were in all leagues, but without having infinite space requirements or something? Or change the UI to take them out of the main league table and into their separate area.
sessions = _ . sortBy sessions . concat ( matchingAISessions ) , (session) -> _ . find ( session . get ( ' leagues ' ) , leagueID: leaderboardOptions . find [ ' leagues.leagueID ' ] ) ? . stats . totalScore ? session . get ( ' totalScore ' ) / 2
sessions . reverse ( ) if leaderboardOptions . sort . totalScore is - 1
sessions = sessions . slice 0 , leaderboardOptions . limit
return cb null , sessions
2014-03-23 12:30:01 -04: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 22:42:42 -04:00
friendIDs = req . body . friendIDs or [ ]
return res . send ( [ ] ) unless friendIDs . length
2014-03-23 12:30:01 -04:00
q = { }
2014-06-30 22:16:26 -04:00
q [ serviceProperty ] = { $in: friendIDs }
2014-03-23 12:30:01 -04:00
query = User . find ( q ) . select ( " #{ serviceProperty } name " ) . lean ( )
2014-03-19 22:42:42 -04:00
query . exec (err, userResults) ->
return res . send ( [ ] ) unless userResults . length
[ id , version ] = id . split ( ' . ' )
userIDs = ( r . _id + ' ' for r in userResults )
2014-06-30 22:16:26 -04:00
q = { ' level.original ' : id , ' level.majorVersion ' : parseInt ( version ) , creator: { $in: userIDs } , totalScore: { $exists: true } }
2014-03-19 22:42:42 -04:00
query = Session . find ( q )
2014-03-23 12:30:01 -04:00
. select ( ' creator creatorName totalScore team ' )
. lean ( )
2014-03-19 22:42:42 -04:00
query . exec (err, sessionResults) ->
return res . send ( [ ] ) unless sessionResults . length
userMap = { }
2014-03-23 12:30:01 -04:00
userMap [ u . _id ] = u [ serviceProperty ] for u in userResults
session [ serviceProperty ] = userMap [ session . creator ] for session in sessionResults
2014-03-19 22:42:42 -04:00
res . send ( sessionResults )
2014-04-14 17:28:28 -04:00
2014-03-14 15:54:52 -04:00
getRandomSessionPair: (req, res, slugOrID) ->
2014-03-14 14:30:04 -04:00
findParameters = { }
2014-03-14 15:54:52 -04:00
if Handler . isID slugOrID
2014-06-30 22:16:26 -04:00
findParameters [ ' _id ' ] = slugOrID
2014-03-14 15:54:52 -04:00
else
2014-06-30 22:16:26 -04:00
findParameters [ ' slug ' ] = slugOrID
2014-03-14 15:54:52 -04:00
selectString = ' original version '
query = Level . findOne ( findParameters )
2015-12-05 11:18:36 -05:00
. select ( selectString )
. lean ( )
. cache ( 60 * 60 * 1000 )
2014-03-14 14:30:04 -04:00
2014-03-14 15:54:52 -04:00
query . exec (err, level) =>
return @ sendDatabaseError ( res , err ) if err
return @ sendNotFoundError ( res ) unless level ?
2014-04-14 17:28:28 -04:00
2014-03-14 15:54:52 -04:00
sessionsQueryParameters =
level:
original: level . original . toString ( )
majorVersion: level . version . major
2014-06-30 22:16:26 -04:00
submitted: true
2014-04-14 17:28:28 -04:00
2015-12-05 11:18:36 -05:00
teams = [ ' humans ' , ' ogres ' ]
findTop20Players = (sessionQueryParams, team, cb) ->
sessionQueryParams [ ' team ' ] = team
aggregate = Session . aggregate [
{ $match: sessionQueryParams }
{ $sort: { ' totalScore ' : - 1 } }
{ $limit: 20 }
{ $project: { ' totalScore ' : 1 } }
]
aggregate . cache ( 3 * 60 * 1000 )
aggregate . exec 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 )
if map . length != 2 then return @ sendDatabaseError res , ' There aren \' t sessions of 2 teams, so cannot choose random opponents! '
@ sendSuccess res , sessions
2014-04-14 17:28:28 -04:00
2014-08-31 02:04:45 -04:00
getFeedback: (req, res, levelID) ->
2014-02-24 23:27:38 -05:00
return @ sendNotFoundError ( res ) unless req . user
2014-08-31 02:04:45 -04:00
@ doGetFeedback req , res , levelID , false
getAllFeedback: (req, res, levelID) ->
2014-11-22 21:40:28 -05:00
return @ sendNotFoundError ( res ) unless req . user
2014-08-31 02:04:45 -04:00
@ doGetFeedback req , res , levelID , true
doGetFeedback: (req, res, levelID, multiple) ->
@ fetchLevelByIDAndHandleErrors levelID , req , res , (err, level) =>
2014-02-14 17:55:30 -05:00
feedbackQuery =
2014-01-03 13:32:13 -05:00
' level.original ' : level . original . toString ( )
' level.majorVersion ' : level . version . major
2014-08-31 02:04:45 -04: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 13:32:13 -05:00
return @ sendDatabaseError ( res , err ) if err
2014-08-31 02:04:45 -04:00
return @ sendNotFoundError ( res ) unless result ?
@ sendSuccess ( res , result )
2014-01-03 13:32:13 -05:00
2014-08-28 01:23:24 -04: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 20:17:34 -05:00
return @ sendSuccess res , [ ] unless levelIDs ?
2014-08-28 01:23:24 -04:00
@ playCountCache ? = { }
@ playCountCachedSince ? = new Date ( )
if ( new Date ( ) ) - @ playCountCachedSince > 86400 * 1000 # Dumb cache expiration
@playCountCache = { }
2015-01-01 15:01:43 -05:00
@playCountCachedSince = new Date ( )
2014-08-28 01:23:24 -04:00
cacheKey = levelIDs . join ' , '
if playCounts = @ playCountCache [ cacheKey ]
return @ sendSuccess res , playCounts
query = Session . aggregate [
2014-08-30 02:09:38 -04:00
{ $match: { levelID: { $in: levelIDs } } }
{ $group: { _id: " $levelID " , playtime: { $sum: " $playtime " } , sessions: { $sum: 1 } } }
2014-08-28 01:23:24 -04:00
{ $sort: { sessions: - 1 } }
]
query . exec (err, data) =>
if err ? then return @ sendDatabaseError res , err
@ playCountCache [ cacheKey ] = data
@ sendSuccess res , data
2014-09-19 06:52:34 -04:00
hasAccessToDocument: (req, document, method=null) ->
2015-02-26 18:21:08 -05:00
return true if req . user ? . isArtisan ( )
2014-11-25 19:20:41 -05:00
method ? = req . method
2014-09-19 11:17:30 -04:00
return true if method is null or method is ' get '
2014-09-19 06:52:34 -04:00
super ( req , document , method )
2014-08-28 01:23:24 -04:00
2014-12-31 15:25:18 -05: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 15:01:43 -05:00
# TODO: An uncached call takes about 5s for dungeons-of-kithgard locally
2014-12-31 15:25:18 -05:00
# TODO: This is very similar to getLevelCompletionsBySlugs(), time to generalize analytics APIs?
2015-01-26 17:58:35 -05:00
# TODO: exclude admin data
2014-12-31 15:25:18 -05: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 19:29:44 -05:00
# log.warn "playtime_averages levelSlugs='#{levelSlugs}' startDay=#{startDay} endDay=#{endDay}"
2014-12-31 15:25:18 -05:00
# Cache results for 1 day
@ levelPlaytimesCache ? = { }
@ levelPlaytimesCachedSince ? = new Date ( )
if ( new Date ( ) ) - @ levelPlaytimesCachedSince > 86400 * 1000 # Dumb cache expiration
@levelPlaytimesCache = { }
2015-01-01 15:01:43 -05:00
@levelPlaytimesCachedSince = new Date ( )
2014-12-31 15:25:18 -05: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 16:19:46 -05:00
match = { $match: { $and: [ { " state.complete " : true } , { " playtime " : { $gt: 0 } } , { levelID: { $in: levelSlugs } } ] } }
2015-01-14 14:09:01 -05: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 15:25:18 -05: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 13:44:17 -05:00
playtimes . push
2014-12-31 16:19:46 -05:00
level: item . _id . level
created: item . _id . created
2014-12-31 15:25:18 -05:00
average: item . average
@ levelPlaytimesCache [ cacheKey ] = playtimes
@ sendSuccess res , playtimes
2015-01-31 00:36:36 -05:00
getTopScores: (req, res, levelOriginal, scoreType, timespan) ->
query =
' level.original ' : levelOriginal
' state.topScores.type ' : scoreType
now = new Date ( )
if timespan is ' day '
since = new Date now - 1 * 86400 * 1000
else if timespan is ' week '
since = new Date now - 7 * 86400 * 1000
if since
query [ ' state.topScores.date ' ] = $gt: since . toISOString ( )
sort =
' state.topScores.score ' : - 1
select = [ ' state.topScores ' , ' creatorName ' , ' creator ' , ' codeLanguage ' , ' heroConfig ' ]
query = Session
. find ( query )
. limit ( 20 )
. sort ( sort )
. select ( select . join ' ' )
query . exec (err, resultSessions) =>
return @ sendDatabaseError ( res , err ) if err
resultSessions ? = [ ]
@ sendSuccess res , resultSessions
2014-01-03 13:32:13 -05:00
module.exports = new LevelHandler ( )