Merge branch 'master' into production

This commit is contained in:
phoenixeliot 2016-07-14 11:28:02 -07:00
commit 5a98926d52
27 changed files with 118 additions and 267 deletions

View file

@ -56,6 +56,7 @@ so we can accept your pull requests. It is easy.
![Josh Callebaut](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Josh%20Callebaut/josh_callebaut_100.png "Josh Callebaut")
![Michael Schmatz](http://codecombat.com/images/pages/about/michael_small.png "Michael Schmatz")
![Josh Lee](http://codecombat.com/images/pages/about/josh_small.png "Josh Lee")
![Dan TDM](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Dan_TDM/dan_tdm_100.png "Dan TDM")
![Alex Cotsarelis](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Cotsarelis/alex_100.png "Alex Cotsarelis")
![Alex Crooks](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Crooks/alex_100.png "Alex Crooks")
![Alexandru Caciulescu](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alexandru%20Caciulescu/alexandru_100.png "Alexandru Caciulescu")

View file

@ -5,12 +5,6 @@ module.exports = class LevelSessionCollection extends CocoCollection
url: '/db/level.session'
model: LevelSession
fetchMineForCourseInstance: (courseInstanceID, options) ->
options = _.extend({
url: "/db/course_instance/#{courseInstanceID}/my-course-level-sessions"
}, options)
@fetch(options)
fetchForCourseInstance: (courseInstanceID, options) ->
options = _.extend({
url: "/db/course_instance/#{courseInstanceID}/my-course-level-sessions"

View file

@ -478,7 +478,7 @@ module.exports = class LevelLoader extends CocoClass
@world.difficulty = @session?.get('state')?.difficulty ? 0
if @observing
@world.difficulty = Math.max 0, @world.difficulty - 1 # Show the difficulty they won, not the next one.
serializedLevel = @level.serialize(@supermodel, @session, @opponentSession)
serializedLevel = @level.serialize {@supermodel, @session, @opponentSession, @headless, @sessionless}
@world.loadFromLevel serializedLevel, false
console.log 'World has been initialized from level loader.' if LOG

View file

@ -226,7 +226,7 @@ module.exports = class Simulator extends CocoClass
@levelLoader = null
setupGod: ->
@god.setLevel @level.serialize(@supermodel, @session, @otherSession)
@god.setLevel @level.serialize {@supermodel, @session, @otherSession, headless: true, sessionless: false}
@god.setLevelSessionIDs (session.sessionID for session in @task.getSessions())
@god.setWorldClassMap @world.classMap
@god.setGoalManager new GoalManager @world, @level.get('goals'), null, {headless: true}

View file

@ -12,7 +12,8 @@ module.exports = class Level extends CocoModel
urlRoot: '/db/level'
editableByArtisans: true
serialize: (supermodel, session, otherSession, cached=false) ->
serialize: (options) ->
{supermodel, session, otherSession, @headless, @sessionless, cached=false} = options
o = @denormalize supermodel, session, otherSession # hot spot to optimize
# Figure out Components
@ -146,7 +147,7 @@ module.exports = class Level extends CocoModel
levelThang.components.push placeholderComponent
# Load the user's chosen hero AFTER getting stats from default char
if /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['course']
if /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['course'] and not @headless and not @sessionless
heroThangType = me.get('heroConfig')?.thangType or ThangType.heroes.captain
levelThang.thangType = heroThangType if heroThangType

View file

@ -54,7 +54,7 @@ _.extend LevelSessionSchema.properties,
changed: c.date
title: 'Changed'
readOnly: true
dateFirstCompleted: {} # c.stringDate
# title: 'Completed'
# readOnly: true
@ -62,9 +62,6 @@ _.extend LevelSessionSchema.properties,
team: c.shortString()
level: LevelSessionLevelSchema
screenshot:
type: 'string'
heroConfig: c.HeroConfigSchema
state: c.object {},
@ -208,14 +205,6 @@ _.extend LevelSessionSchema.properties,
submittedCodeLanguage:
type: 'string'
transpiledCode:
type: 'object'
additionalProperties:
type: 'object'
additionalProperties:
type: 'string'
format: 'code'
isRanking:
type: 'boolean'
description: 'Whether this session is still in the first ranking chain after being submitted.'

View file

@ -1,14 +0,0 @@
#admin-level-sessions-view
.session_tile
display: inline-block
position: relative
margin: 8px
.session_info
position: absolute
top: 0
left: 0
right: 0
text-align: center
background: rgba(0, 0, 0, 0.5)
color: white

View file

@ -30,8 +30,6 @@ block content
h4 Entities
ul
li
a(href="/admin/level-sessions") Active Instances
li
a(href="/admin/trial-requests") Trial Requests
li

View file

@ -1,17 +0,0 @@
extends /templates/base
block content
h1 Latest Games
each session in view.sessions.models
- var url = '/play/level/'+session.get('levelID')+'?session='+session.id
.session_tile
a(href=url)
if session.get('screenshot')
img(src=session.get('screenshot'))
else
img(src="/images/generic-icon.png")
.session_info
.level_name= session.get('levelName')
.creator_name= session.get('creatorName')

View file

@ -132,8 +132,9 @@ block outer_content
div.tab-pane#editor-level-tasks-tab-view
div.tab-pane#editor-level-patches
.patches-view
div.tab-pane#editor-level-patches.nano
.nano-content
.patches-view
div.tab-pane#related-achievements-view

View file

@ -83,21 +83,21 @@ module.exports = class AnalyticsView extends RootView
campaignDauTotal += count
else if event.indexOf('DAU classroom') >= 0
classroomDauTotal += count
eventMap[event] = true;
eventMap[event] = true
entry.events['DAU campaign total'] = campaignDauTotal
eventMap['DAU campaign total'] = true;
eventMap['DAU campaign total'] = true
campaignDauTotals.unshift(campaignDauTotal)
campaignDauTotals.pop() while campaignDauTotals.length > 30
if campaignDauTotals.length is 30
entry.events['DAU campaign 30-day average'] = Math.round(_.reduce(campaignDauTotals, (a, b) -> a + b) / 30)
eventMap['DAU campaign 30-day average'] = true;
eventMap['DAU campaign 30-day average'] = true
entry.events['DAU classroom total'] = classroomDauTotal
eventMap['DAU classroom total'] = true;
eventMap['DAU classroom total'] = true
classroomDauTotals.unshift(classroomDauTotal)
classroomDauTotals.pop() while classroomDauTotals.length > 30
if classroomDauTotals.length is 30
entry.events['DAU classroom 30-day average'] = Math.round(_.reduce(classroomDauTotals, (a, b) -> a + b) / 30)
eventMap['DAU classroom 30-day average'] = true;
eventMap['DAU classroom 30-day average'] = true
@activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
@activeUserEventNames = Object.keys(eventMap)

View file

@ -1,19 +0,0 @@
RootView = require 'views/core/RootView'
template = require 'templates/admin/level_sessions'
LevelSession = require 'models/LevelSession'
CocoCollection = require 'collections/CocoCollection'
class LevelSessionCollection extends CocoCollection
url: '/db/level.session/x/active?project=screenshot,levelName,creatorName'
model: LevelSession
module.exports = class LevelSessionsView extends RootView
id: 'admin-level-sessions-view'
template: template
constructor: (options) ->
super options
@getLevelSessions()
getLevelSessions: ->
@sessions = @supermodel.loadCollection(new LevelSessionCollection(), 'sessions', {cache: false}).model

View file

@ -23,6 +23,7 @@ module.exports = class MainAdminView extends RootView
'submit #user-search-form': 'onSubmitUserSearchForm'
'click #stop-spying-btn': 'onClickStopSpyingButton'
'click #increment-button': 'incrementUserAttribute'
'click .user-spy-button': 'onClickUserSpyButton'
'click #user-search-result': 'onClickUserSearchResult'
'click #create-free-sub-btn': 'onClickFreeSubLink'
'click #terminal-create': 'onClickTerminalSubLink'
@ -59,6 +60,18 @@ module.exports = class MainAdminView extends RootView
errors.showNotyNetworkError(arguments...)
})
onClickUserSpyButton: (e) ->
e.stopPropagation()
userID = $(e.target).closest('tr').data('user-id')
button = $(e.currentTarget)
forms.disableSubmit(button)
me.spy(userID, {
success: -> window.location.reload()
error: ->
forms.enableSubmit(button)
errors.showNotyNetworkError(arguments...)
})
onSubmitUserSearchForm: (e) ->
e.preventDefault()
searchValue = @$el.find('#user-search').val()
@ -76,7 +89,7 @@ module.exports = class MainAdminView extends RootView
forms.enableSubmit(@$('#user-search-button'))
result = ''
if users.length
result = ("<tr data-user-id='#{user._id}'><td><code>#{user._id}</code></td><td>#{_.escape(user.name or 'Anonymous')}</td><td>#{_.escape(user.email)}</td></tr>" for user in users)
result = ("<tr data-user-id='#{user._id}'><td><code>#{user._id}</code></td><td>#{_.escape(user.name or 'Anonymous')}</td><td>#{_.escape(user.email)}</td><td><button class='user-spy-button'>Spy</button></td></tr>" for user in users)
result = "<table class=\"table\">#{result.join('\n')}</table>"
@$el.find('#user-search-result').html(result)

View file

@ -595,7 +595,7 @@ module.exports = class ThangsTabView extends CocoView
@level.set 'thangs', thangs
return if @editThangView
return if skipSerialization
serializedLevel = @level.serialize @supermodel, null, null, true
serializedLevel = @level.serialize {@supermodel, session: null, otherSession: null, headless: false, sessionless: true, cached: true}
try
@world.loadFromLevel serializedLevel, false
catch error

View file

@ -25,9 +25,9 @@ module.exports = class VerifierTest extends CocoClass
@loadStartTime = new Date()
@god = new God maxAngels: 1, headless: true
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, headless: true, fakeSessionConfig: {codeLanguage: @language, callback: @configureSession}
@listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded
@listenToOnce @levelLoader, 'world-necessities-loaded', -> _.defer @onWorldNecessitiesLoaded
onWorldNecessitiesLoaded: ->
onWorldNecessitiesLoaded: =>
# Called when we have enough to build the world, but not everything is loaded
@grabLevelLoaderData()
@ -62,7 +62,7 @@ module.exports = class VerifierTest extends CocoClass
@solution = @levelLoader.session.solution
setupGod: ->
@god.setLevel @level.serialize @supermodel, @session
@god.setLevel @level.serialize {@supermodel, @session, otherSession: null, headless: true, sessionless: false}
@god.setLevelSessionIDs [@session.id]
@god.setWorldClassMap @world.classMap
@god.lastFlagHistory = @session.get('state').flagHistory
@ -134,8 +134,10 @@ module.exports = class VerifierTest extends CocoClass
setTimeout @cleanup, 100
cleanup: =>
if @levelLoader
@stopListening @levelLoader
@levelLoader.destroy()
if @god
@stopListening @god
@god.destroy()
@world = null

View file

@ -42,6 +42,7 @@ module.exports = class LadderView extends RootView
initialize: (options, @levelID, @leagueType, @leagueID) ->
@level = @supermodel.loadModel(new Level(_id: @levelID)).model
@level.once 'sync', =>
return if @destroyed
@levelDescription = marked(@level.get('description')) if @level.get('description')
@teams = teamDataFromLevel @level
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(@levelID), 'your_sessions', {cache: false}).model

View file

@ -69,7 +69,7 @@ module.exports = class SpectateLevelView extends RootView
@load()
setLevel: (@level, @supermodel) ->
serializedLevel = @level.serialize @supermodel, @session, @otherSession
serializedLevel = @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false}
@god?.setLevel serializedLevel
if @world
@world.loadFromLevel serializedLevel, false
@ -106,7 +106,7 @@ module.exports = class SpectateLevelView extends RootView
#at this point, all requisite data is loaded, and sessions are not denormalized
team = @world.teamForPlayer(0)
@loadOpponentTeam(team)
@god.setLevel @level.serialize @supermodel, @session, @otherSession
@god.setLevel @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false}
@god.setLevelSessionIDs if @otherSession then [@session.id, @otherSession.id] else [@session.id]
@god.setWorldClassMap @world.classMap
@setTeam team

View file

@ -69,7 +69,6 @@ module.exports = class PlayLevelView extends RootView
'god:infinite-loop': 'onInfiniteLoop'
'level:reload-from-data': 'onLevelReloadFromData'
'level:reload-thang-type': 'onLevelReloadThangType'
'level:session-will-save': 'onSessionWillSave'
'level:started': 'onLevelStarted'
'level:loading-view-unveiling': 'onLoadingViewUnveiling'
'level:loading-view-unveiled': 'onLoadingViewUnveiled'
@ -112,7 +111,6 @@ module.exports = class PlayLevelView extends RootView
@gameUIState = new GameUIState()
$(window).on 'resize', @onWindowResize
@saveScreenshot = _.throttle @saveScreenshot, 30000
application.tracker?.enableInspectletJS(@levelID)
@ -130,7 +128,7 @@ module.exports = class PlayLevelView extends RootView
@supermodel.collections = givenSupermodel.collections
@supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups
serializedLevel = @level.serialize @supermodel, @session, @otherSession
serializedLevel = @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false}
@god?.setLevel serializedLevel
if @world
@world.loadFromLevel serializedLevel, false
@ -246,7 +244,7 @@ module.exports = class PlayLevelView extends RootView
@session.set 'multiplayer', false
setupGod: ->
@god.setLevel @level.serialize @supermodel, @session, @otherSession
@god.setLevel @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false}
@god.setLevelSessionIDs if @otherSession then [@session.id, @otherSession.id] else [@session.id]
@god.setWorldClassMap @world.classMap
@ -594,15 +592,6 @@ module.exports = class PlayLevelView extends RootView
@bus.removeFirebaseData =>
@bus.disconnect()
onSessionWillSave: (e) ->
# Something interesting has happened, so (at a lower frequency), we'll save a screenshot.
#@saveScreenshot e.session
# Throttled
saveScreenshot: (session) =>
return unless screenshot = @surface?.screenshot()
session.save {screenshot: screenshot}, {patch: true, type: 'PUT'}
onContactClicked: (e) ->
Backbone.Mediator.publish 'level:contact-button-pressed', {}
@openModalView contactModal = new ContactModal levelID: @level.get('slug') or @level.id, courseID: @courseID, courseInstanceID: @courseInstanceID
@ -954,20 +943,6 @@ module.exports = class PlayLevelView extends RootView
console.error 'Failed to read sessionState in onRealTimeMultiplayerCast'
console.info 'Submitting my code'
# Transpiling code copied from scripts/transpile.coffee
# TODO: Should this live somewhere else?
transpiledCode = {}
for thang, spells of @session.get('code')
transpiledCode[thang] = {}
for spellID, spell of spells
spellName = thang + '/' + spellID
continue if @session.get('teamSpells') and not (spellName in @session.get('teamSpells')[@session.get('team')])
# console.log "PlayLevelView Transpiling spell #{spellName}"
aetherOptions = createAetherOptions functionName: spellID, codeLanguage: @session.get('submittedCodeLanguage'), includeFlow: true
aether = new Aether aetherOptions
transpiledCode[thang][spellID] = aether.transpile spell
# console.log "PlayLevelView transpiled code", transpiledCode
@session.set 'transpiledCode', transpiledCode
permissions = @session.get 'permissions' ? []
unless _.find(permissions, (p) -> p.target is 'public' and p.access is 'read')
permissions.push target:'public', access:'read'

View file

@ -1,15 +1,9 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/play/level/modal/course-victory-modal'
Achievements = require 'collections/Achievements'
Level = require 'models/Level'
Course = require 'models/Course'
ThangType = require 'models/ThangType'
ThangTypes = require 'collections/ThangTypes'
LevelSessions = require 'collections/LevelSessions'
EarnedAchievement = require 'models/EarnedAchievement'
LocalMongo = require 'lib/LocalMongo'
ProgressView = require './ProgressView'
NewItemView = require './NewItemView'
Classroom = require 'models/Classroom'
utils = require 'core/utils'
@ -18,7 +12,6 @@ module.exports = class CourseVictoryModal extends ModalView
template: template
closesOnClickOutside: false
initialize: (options) ->
@courseID = options.courseID
@courseInstanceID = options.courseInstanceID
@ -26,20 +19,10 @@ module.exports = class CourseVictoryModal extends ModalView
@session = options.session
@level = options.level
@newItems = new ThangTypes()
@newHeroes = new ThangTypes()
if @courseInstanceID
@classroom = new Classroom()
@supermodel.trackRequest(@classroom.fetchForCourseInstance(@courseInstanceID))
@achievements = options.achievements
if not @achievements
@achievements = new Achievements()
@achievements.fetchRelatedToLevel(@session.get('level').original)
@achievements = @supermodel.loadCollection(@achievements, 'achievements').model
@listenToOnce @achievements, 'sync', @onAchievementsLoaded
else
@onAchievementsLoaded()
@playSound 'victory'
@nextLevel = new Level()
@ -68,69 +51,12 @@ module.exports = class CourseVictoryModal extends ModalView
return
super(arguments...)
onAchievementsLoaded: ->
@achievements.models = _.filter @achievements.models, (m) -> not m.get('query')?.ladderAchievementDifficulty # Don't show higher AI difficulty achievements
itemOriginals = []
heroOriginals = []
achievementIDs = []
for achievement in @achievements.models
rewards = achievement.get('rewards') or {}
heroOriginals.push rewards.heroes or []
itemOriginals.push rewards.items or []
achievement.completed = LocalMongo.matchesQuery(@session.attributes, achievement.get('query'))
achievementIDs.push(achievement.id) if achievement.completed
itemOriginals = _.uniq _.flatten itemOriginals
heroOriginals = _.uniq _.flatten heroOriginals
#project = ['original', 'rasterIcon', 'name', 'soundTriggers', 'i18n'] # This is what we need, but the PlayHeroesModal needs more, and so we load more to fill up the supermodel.
project = ['original', 'rasterIcon', 'name', 'slug', 'soundTriggers', 'featureImages', 'gems', 'heroClass', 'description', 'components', 'extendedName', 'unlockLevelName', 'i18n']
for [newThangTypeCollection, originals] in [[@newItems, itemOriginals], [@newHeroes, heroOriginals]]
for original in originals
thang= new ThangType()
thang.url = "/db/thang.type/#{original}/version"
thang.project = project
@supermodel.loadModel(thang)
newThangTypeCollection.add(thang)
@newEarnedAchievements = []
for achievement in @achievements.models
continue unless achievement.completed
ea = new EarnedAchievement({
collection: achievement.get('collection')
triggeredBy: @session.id
achievement: achievement.id
})
if me.isSessionless()
@newEarnedAchievements.push ea
else
ea.save()
# Can't just add models to supermodel because each ea has the same url
ea.sr = @supermodel.addSomethingResource(ea.cid)
@newEarnedAchievements.push ea
@listenToOnce ea, 'sync', (model) ->
model.sr.markLoaded()
if _.all((ea.id for ea in @newEarnedAchievements))
unless me.loading
@supermodel.loadModel(me, {cache: false})
@newEarnedAchievementsResource.markLoaded()
unless me.isSessionless()
# have to use a something resource because addModelResource doesn't handle models being upserted/fetched via POST like we're doing here
@newEarnedAchievementsResource = @supermodel.addSomethingResource('earned achievements') if @newEarnedAchievements.length
onLoaded: ->
super()
@views = []
# TODO: Add main victory view
# TODO: Add level up view
# TODO: Add new hero view?
for newItem in @newItems.models
@views.push(new NewItemView({item: newItem}))
@levelSessions?.remove(@session)
@levelSessions?.add(@session)
progressView = new ProgressView({
level: @level
nextLevel: @nextLevel

View file

@ -1,7 +1,7 @@
// Follow up on Close.io leads
'use strict';
if (process.argv.length !== 7) {
if (process.argv.length !== 8) {
log("Usage: node <script> <Close.io general API key> <Close.io mail API key1> <Close.io mail API key2> <Close.io mail API key3> <mongo connection Url>");
process.exit();
}
@ -20,8 +20,8 @@ const demoRequestEmailTemplatesAuto2 = ['tmpl_HJ5zebh1SqC1QydDto05VPUMu4F7i5M35L
const scriptStartTime = new Date();
const closeIoApiKey = process.argv[2];
const closeIoMailApiKeys = [process.argv[3], process.argv[4], process.argv[5]]; // Automatic mails sent as API owners
const mongoConnUrl = process.argv[6];
const closeIoMailApiKeys = [process.argv[3], process.argv[4], process.argv[5], process.argv[6]]; // Automatic mails sent as API owners
const mongoConnUrl = process.argv[7];
const MongoClient = require('mongodb').MongoClient;
const async = require('async');
const request = require('request');

View file

@ -1,8 +1,8 @@
// Upsert new lead data into Close.io
'use strict';
if (process.argv.length !== 9) {
log("Usage: node <script> <Close.io general API key> <Close.io mail API key1> <Close.io mail API key2> <Close.io mail API key3> <Close.io EU mail API key> <Intercom 'App ID:API key'> <mongo connection Url>");
if (process.argv.length !== 10) {
log("Usage: node <script> <Close.io general API key> <Close.io mail API key1> <Close.io mail API key2> <Close.io mail API key3> <Close.io mail API key4> <Close.io EU mail API key> <Intercom 'App ID:API key'> <mongo connection Url>");
process.exit();
}
@ -64,18 +64,22 @@ const closeIoMailApiKeys = [
},
{
apiKey: process.argv[4],
weight: .25
weight: .20
},
{
apiKey: process.argv[5],
weight: .05
},
{
apiKey: process.argv[6],
weight: .05
},
];
const closeIoEuMailApiKey = process.argv[6];
const intercomAppIdApiKey = process.argv[7];
const closeIoEuMailApiKey = process.argv[7];
const intercomAppIdApiKey = process.argv[8];
const intercomAppId = intercomAppIdApiKey.split(':')[0];
const intercomApiKey = intercomAppIdApiKey.split(':')[1];
const mongoConnUrl = process.argv[8];
const mongoConnUrl = process.argv[9];
const MongoClient = require('mongodb').MongoClient;
const async = require('async');
const countryData = require('country-data');

View file

@ -142,20 +142,20 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless courseInstance
Course.findById courseInstance.get('courseID'), (err, course) =>
Classroom.findById courseInstance.get('classroomID'), (err, classroom) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless course
Campaign.findById course.get('campaignID'), (err, campaign) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless campaign
levelIDs = (levelID for levelID, level of campaign.get('levels') when not _.contains(level.type, 'ladder'))
query = {$and: [{creator: req.user.id}, {'level.original': {$in: levelIDs}}]}
cursor = LevelSession.find(query)
cursor = cursor.select(req.query.project) if req.query.project
cursor.exec (err, documents) =>
return @sendDatabaseError(res, err) if err?
cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
@sendSuccess(res, cleandocs)
return @sendNotFoundError(res) unless classroom
levelIDs = []
for course in classroom.get('courses') when course._id.equals(courseInstance.get('courseID'))
for level in course.levels when not _.contains(level.type, 'ladder')
levelIDs.push(level.original + "")
query = {$and: [{creator: req.user.id}, {'level.original': {$in: levelIDs}}]}
cursor = LevelSession.find(query)
cursor = cursor.select(req.query.project) if req.query.project
cursor.exec (err, documents) =>
return @sendDatabaseError(res, err) if err?
cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
@sendSuccess(res, cleandocs)
getMembersAPI: (req, res, courseInstanceID) ->
return @sendUnauthorizedError(res) if not req.user?

View file

@ -195,7 +195,7 @@ LevelHandler = class LevelHandler extends Handler
majorVersion: level.version.major
creator: req.user._id+''
query = Session.find(sessionQuery).select('-screenshot -transpiledCode')
query = Session.find(sessionQuery)
# 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
query.exec (err, results) =>
if err then @sendDatabaseError(res, err) else @sendSuccess res, results

View file

@ -31,6 +31,6 @@ AnalyticsLogEventSchema.statics.logEvent = (user, event, properties={}) ->
unless config.proxy
analyticsMongoose = mongoose.createConnection()
analyticsMongoose.open "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}", (error) ->
log.warn "Couldnt connect to analytics", error
log.error "Couldnt connect to analytics", error if error
module.exports = AnalyticsLogEvent = analyticsMongoose.model('analytics.log.event', AnalyticsLogEventSchema, config.mongo.analytics_collection)

View file

@ -84,9 +84,9 @@ LevelSessionSchema.pre 'save', (next) ->
LevelSessionSchema.statics.privateProperties = ['code', 'submittedCode', 'unsubscribed']
LevelSessionSchema.statics.editableProperties = ['multiplayer', 'players', 'code', 'codeLanguage', 'completed', 'state',
'levelName', 'creatorName', 'levelID', 'screenshot',
'levelName', 'creatorName', 'levelID',
'chat', 'teamSpells', 'submitted', 'submittedCodeLanguage',
'unsubscribed', 'playtime', 'heroConfig', 'team', 'transpiledCode',
'unsubscribed', 'playtime', 'heroConfig', 'team',
'browser']
LevelSessionSchema.statics.jsonSchema = jsonschema
@ -94,7 +94,7 @@ LevelSessionSchema.set('toObject', {
transform: (doc, ret, options) ->
req = options.req
return ret unless req # TODO: Make deleting properties the default, but the consequences are far reaching
submittedCode = doc.get('submittedCode')
unless req.user?.isAdmin() or req.user?.id is doc.get('creator') or ('employer' in (req.user?.get('permissions') ? [])) or not doc.get('submittedCode') # TODO: only allow leaderboard access to non-top-5 solutions
ret = _.omit ret, LevelSession.privateProperties
@ -106,4 +106,32 @@ LevelSessionSchema.set('toObject', {
return ret
})
module.exports = LevelSession = mongoose.model('level.session', LevelSessionSchema, 'level.sessions')
if config.mongo.level_session_replica_string?
levelSessionMongo = mongoose.createConnection()
levelSessionMongo.open config.mongo.level_session_replica_string, (error) ->
if error
log.error "Couldnt connect to session mongo!", error
else
log.info "Connected to seperate level session server with string", config.mongo.level_session_replica_string
else
levelSessionMongo = mongoose
LevelSession = levelSessionMongo.model('level.session', LevelSessionSchema, 'level.sessions')
if config.mongo.level_session_aux_replica_string?
auxLevelSessionMongo = mongoose.createConnection()
auxLevelSessionMongo.open config.mongo.level_session_aux_replica_string, (error) ->
if error
log.error "Couldnt connect to AUX session mongo!", error
else
log.info "Connected to seperate level AUX session server with string", config.mongo.level_session_aux_replica_string
auxLevelSession = auxLevelSessionMongo.model('level.session', LevelSessionSchema, 'level.sessions')
LevelSessionSchema.post 'save', (d) ->
return unless d instanceof LevelSession
o = d.toObject {transform: ((x, r) -> r), virtuals: false}
auxLevelSession.collection.save o, {w:1}, (err, v) ->
log.error err.stack if err
module.exports = LevelSession

View file

@ -26,6 +26,11 @@ config.mongo =
mongoose_tokyo_replica_string: process.env.COCO_MONGO_MONGOOSE_TOKYO_REPLICA_STRING or ''
mongoose_saoPaulo_replica_string : process.env.COCO_MONGO_MONGOOSE_SAOPAULO_REPLICA_STRING or ''
if process.env.COCO_MONGO_LS_REPLICA_STRING?
config.mongo.level_session_replica_string = process.env.COCO_MONGO_LS_REPLICA_STRING
if process.env.COCO_MONGO_LS_AUX_REPLICA_STRING?
config.mongo.level_session_aux_replica_string = process.env.COCO_MONGO_LS_AUX_REPLICA_STRING
if config.tokyo or config.saoPaulo

View file

@ -1,9 +1,7 @@
Course = require 'models/Course'
Level = require 'models/Level'
LevelSession = require 'models/LevelSession'
Achievements = require 'collections/Achievements'
CourseVictoryModal = require 'views/play/level/modal/CourseVictoryModal'
NewItemView = require 'views/play/level/modal/NewItemView'
ProgressView = require 'views/play/level/modal/ProgressView'
factories = require 'test/app/factories'
@ -12,7 +10,7 @@ describe 'CourseVictoryModal', ->
me.clear()
it 'will eventually be the only victory modal'
makeViewOptions = ->
level = factories.makeLevel()
course = factories.makeCourse()
@ -21,24 +19,18 @@ describe 'CourseVictoryModal', ->
course: factories.makeCourse()
level: level
session: factories.makeLevelSession({ state: { complete: true } }, { level })
achievements: new Achievements([factories.makeLevelCompleteAchievement({}, {level: level})])
nextLevel: factories.makeLevel()
courseInstanceID: courseInstance.id
courseID: course.id
}
nextLevelRequest = null
handleRequests = (modal) ->
requests = jasmine.Ajax.requests.all()
thangRequest = _.find(requests, (r) -> _.string.startsWith(r.url, '/db/thang.type'))
thangRequest?.respondWith({status: 200, responseText: factories.makeThangType().stringify()})
modal.newEarnedAchievements[0].fakeRequests[0].respondWith({
status: 200, responseText: factories.makeEarnedAchievement().stringify()
})
modal.levelSessions.fakeRequests[0].respondWith({ status: 200, responseText: '[]' })
modal.classroom.fakeRequests[0].respondWith({
status: 200, responseText: factories.makeClassroom().stringify()
status: 200, responseText: factories.makeClassroom().stringify()
})
if me.fakeRequests
lastRequest = _.last(me.fakeRequests)
@ -47,7 +39,7 @@ describe 'CourseVictoryModal', ->
status: 200, responseText: factories.makeUser().stringify()
})
nextLevelRequest = modal.nextLevel.fakeRequests[0]
describe 'given a course level with a next level and no item or hero rewards', ->
modal = null
@ -63,7 +55,7 @@ describe 'CourseVictoryModal', ->
expect(modal.views[0] instanceof ProgressView).toBe(true)
it '(demo)', -> jasmine.demoModal(modal)
describe 'its ProgressView', ->
it 'has a next level button which navigates to the next level on click', ->
spyOn(application.router, 'navigate')
@ -71,12 +63,12 @@ describe 'CourseVictoryModal', ->
expect(button.length).toBe(1)
button.click()
expect(application.router.navigate).toHaveBeenCalled()
it 'has two columns', ->
expect(modal.$('.row:first .col-sm-12').length).toBe(0)
expect(modal.$('.row:first .col-sm-5').length).toBe(1)
expect(modal.$('.row:first .col-sm-7').length).toBe(1)
describe 'given a course level without a next level', ->
modal = null
@ -91,13 +83,13 @@ describe 'CourseVictoryModal', ->
handleRequests(modal)
nextLevelRequest.respondWith({status: 404, responseText: '{}'})
_.defer done
describe 'its ProgressView', ->
it 'has a single large column, since there is no next level to display', ->
expect(modal.$('.row:first .col-sm-12').length).toBe(1)
expect(modal.$('.row:first .col-sm-5').length).toBe(0)
expect(modal.$('.row:first .col-sm-7').length).toBe(0)
it 'has a done button which navigates to the CourseDetailsView for the given course instance', ->
spyOn(application.router, 'navigate')
button = modal.$el.find('#done-btn')
@ -106,32 +98,3 @@ describe 'CourseVictoryModal', ->
expect(application.router.navigate).toHaveBeenCalled()
it '(demo)', -> jasmine.demoModal(modal)
describe 'given a course level with a new item', ->
modal = null
beforeEach (done) ->
options = makeViewOptions()
# insert new item into achievement properties
achievement = options.achievements.first()
rewards = _.cloneDeep(achievement.get('rewards'))
rewards.items = ["53e4108204c00d4607a89f78"]
achievement.set('rewards', rewards)
modal = new CourseVictoryModal(options)
handleRequests(modal)
nextLevelRequest.respondWith({status: 200, responseText: factories.makeLevel().stringify()})
_.defer done
it 'includes a NewItemView when the level rewards a new item', ->
expect(_.size(modal.views)).toBe(2)
expect(modal.views[0] instanceof NewItemView).toBe(true)
it 'continues to the ProgressView when you click the continue button', ->
expect(modal.currentView instanceof NewItemView).toBe(true)
modal.$el.find('#continue-btn').click()
expect(modal.currentView instanceof ProgressView).toBe(true)
it '(demo)', -> jasmine.demoModal(modal)