mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-02 11:58:10 -05:00
Merge branch 'master' into game-dev-levels
This commit is contained in:
commit
61caf3dcd3
43 changed files with 521 additions and 474 deletions
|
@ -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")
|
![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")
|
![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")
|
![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 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")
|
![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")
|
![Alexandru Caciulescu](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alexandru%20Caciulescu/alexandru_100.png "Alexandru Caciulescu")
|
||||||
|
|
|
@ -5,12 +5,6 @@ module.exports = class LevelSessionCollection extends CocoCollection
|
||||||
url: '/db/level.session'
|
url: '/db/level.session'
|
||||||
model: LevelSession
|
model: LevelSession
|
||||||
|
|
||||||
fetchMineForCourseInstance: (courseInstanceID, options) ->
|
|
||||||
options = _.extend({
|
|
||||||
url: "/db/course_instance/#{courseInstanceID}/my-course-level-sessions"
|
|
||||||
}, options)
|
|
||||||
@fetch(options)
|
|
||||||
|
|
||||||
fetchForCourseInstance: (courseInstanceID, options) ->
|
fetchForCourseInstance: (courseInstanceID, options) ->
|
||||||
options = _.extend({
|
options = _.extend({
|
||||||
url: "/db/course_instance/#{courseInstanceID}/my-course-level-sessions"
|
url: "/db/course_instance/#{courseInstanceID}/my-course-level-sessions"
|
||||||
|
|
|
@ -488,7 +488,7 @@ module.exports = class LevelLoader extends CocoClass
|
||||||
@world.difficulty = @session?.get('state')?.difficulty ? 0
|
@world.difficulty = @session?.get('state')?.difficulty ? 0
|
||||||
if @observing
|
if @observing
|
||||||
@world.difficulty = Math.max 0, @world.difficulty - 1 # Show the difficulty they won, not the next one.
|
@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
|
@world.loadFromLevel serializedLevel, false
|
||||||
console.log 'World has been initialized from level loader.' if LOG
|
console.log 'World has been initialized from level loader.' if LOG
|
||||||
|
|
||||||
|
|
|
@ -226,7 +226,7 @@ module.exports = class Simulator extends CocoClass
|
||||||
@levelLoader = null
|
@levelLoader = null
|
||||||
|
|
||||||
setupGod: ->
|
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.setLevelSessionIDs (session.sessionID for session in @task.getSessions())
|
||||||
@god.setWorldClassMap @world.classMap
|
@god.setWorldClassMap @world.classMap
|
||||||
@god.setGoalManager new GoalManager @world, @level.get('goals'), null, {headless: true}
|
@god.setGoalManager new GoalManager @world, @level.get('goals'), null, {headless: true}
|
||||||
|
|
|
@ -12,7 +12,8 @@ module.exports = class Level extends CocoModel
|
||||||
urlRoot: '/db/level'
|
urlRoot: '/db/level'
|
||||||
editableByArtisans: true
|
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
|
o = @denormalize supermodel, session, otherSession # hot spot to optimize
|
||||||
|
|
||||||
# Figure out Components
|
# Figure out Components
|
||||||
|
@ -146,7 +147,7 @@ module.exports = class Level extends CocoModel
|
||||||
levelThang.components.push placeholderComponent
|
levelThang.components.push placeholderComponent
|
||||||
|
|
||||||
# Load the user's chosen hero AFTER getting stats from default char
|
# 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
|
heroThangType = me.get('heroConfig')?.thangType or ThangType.heroes.captain
|
||||||
levelThang.thangType = heroThangType if heroThangType
|
levelThang.thangType = heroThangType if heroThangType
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ _.extend ClassroomSchema.properties,
|
||||||
levels: c.array { title: 'Levels' }, c.object { title: 'Level' }, {
|
levels: c.array { title: 'Levels' }, c.object { title: 'Level' }, {
|
||||||
practice: {type: 'boolean'}
|
practice: {type: 'boolean'}
|
||||||
practiceThresholdMinutes: {type: 'number'}
|
practiceThresholdMinutes: {type: 'number'}
|
||||||
|
shareable: {type: 'boolean'}
|
||||||
type: c.shortString()
|
type: c.shortString()
|
||||||
original: c.objectId()
|
original: c.objectId()
|
||||||
name: {type: 'string'}
|
name: {type: 'string'}
|
||||||
|
|
|
@ -267,7 +267,7 @@ LevelSchema = c.object {
|
||||||
victory: {}
|
victory: {}
|
||||||
type: 'hero'
|
type: 'hero'
|
||||||
goals: [
|
goals: [
|
||||||
{id: 'ogres-die', name: 'Ogres must die.', killThangs: ['ogres'], worldEndsAfter: 3}
|
{id: 'ogres-die', name: 'Defeat the ogres.', killThangs: ['ogres'], worldEndsAfter: 3}
|
||||||
{id: 'humans-survive', name: 'Your hero must survive.', saveThangs: ['Hero Placeholder'], howMany: 1, worldEndsAfter: 3, hiddenGoal: true}
|
{id: 'humans-survive', name: 'Your hero must survive.', saveThangs: ['Hero Placeholder'], howMany: 1, worldEndsAfter: 3, hiddenGoal: true}
|
||||||
]
|
]
|
||||||
concepts: ['basic_syntax']
|
concepts: ['basic_syntax']
|
||||||
|
@ -325,6 +325,7 @@ _.extend LevelSchema.properties,
|
||||||
replayable: {type: 'boolean', title: 'Replayable', description: 'Whether this (hero) level infinitely scales up its difficulty and can be beaten over and over for greater rewards.'}
|
replayable: {type: 'boolean', title: 'Replayable', description: 'Whether this (hero) level infinitely scales up its difficulty and can be beaten over and over for greater rewards.'}
|
||||||
buildTime: {type: 'number', description: 'How long it has taken to build this level.'}
|
buildTime: {type: 'number', description: 'How long it has taken to build this level.'}
|
||||||
practice: { type: 'boolean' }
|
practice: { type: 'boolean' }
|
||||||
|
shareable: { type: 'boolean', title: 'Shareable' }
|
||||||
practiceThresholdMinutes: {type: 'number', description: 'Players with larger playtimes may be directed to a practice level.'}
|
practiceThresholdMinutes: {type: 'number', description: 'Players with larger playtimes may be directed to a practice level.'}
|
||||||
|
|
||||||
# Admin flags
|
# Admin flags
|
||||||
|
|
|
@ -15,6 +15,16 @@
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
text-align: left
|
text-align: left
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
input
|
||||||
|
max-height: 5vh
|
||||||
|
|
||||||
|
.form-container
|
||||||
|
> .form-group, > .row
|
||||||
|
max-height: 84px
|
||||||
|
flex-grow: 1
|
||||||
|
align-self: flex-start
|
||||||
|
|
||||||
.btn-illustrated img
|
.btn-illustrated img
|
||||||
// Undo previous opacity-toggling hover behavior
|
// Undo previous opacity-toggling hover behavior
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
@import "app/styles/style-flat-variables"
|
@import "app/styles/style-flat-variables"
|
||||||
|
|
||||||
#choose-account-type-view
|
#choose-account-type-view
|
||||||
|
.choose-type-title
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
flex-grow: 0.5
|
||||||
|
justify-content: flex-end
|
||||||
|
|
||||||
|
h4
|
||||||
|
padding-bottom: 10px
|
||||||
|
|
||||||
.path-cards
|
.path-cards
|
||||||
margin-top: 15px
|
|
||||||
display: flex
|
display: flex
|
||||||
|
|
||||||
.path-card ~ .path-card
|
.path-card ~ .path-card
|
||||||
|
@ -13,7 +21,8 @@
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
justify-content: space-between
|
justify-content: space-between
|
||||||
width: 235px
|
width: 235px
|
||||||
min-height: 340px
|
height: 340px
|
||||||
|
max-height: 42vh
|
||||||
border-style: solid
|
border-style: solid
|
||||||
border-width: thin
|
border-width: thin
|
||||||
border-radius: 5px
|
border-radius: 5px
|
||||||
|
@ -35,6 +44,7 @@
|
||||||
align-items: center
|
align-items: center
|
||||||
justify-content: center
|
justify-content: center
|
||||||
height: 50px
|
height: 50px
|
||||||
|
max-height: 5vh
|
||||||
color: white
|
color: white
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
text-align: center
|
text-align: center
|
||||||
|
@ -43,7 +53,8 @@
|
||||||
flex-grow: 1
|
flex-grow: 1
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
margin: 50px 20px 0
|
justify-content: center
|
||||||
|
margin: 0 20px
|
||||||
|
|
||||||
ul
|
ul
|
||||||
align-self: center
|
align-self: center
|
||||||
|
@ -55,14 +66,26 @@
|
||||||
left: -5px
|
left: -5px
|
||||||
|
|
||||||
.card-footer
|
.card-footer
|
||||||
margin: 20px
|
margin: 0 20px 20px
|
||||||
|
min-height: 62px
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
justify-content: flex-end
|
||||||
|
|
||||||
.individual-section
|
.individual-section
|
||||||
margin-top: 50px
|
display: flex
|
||||||
|
flex-grow: 1
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
max-width: 425px
|
max-width: 425px
|
||||||
|
|
||||||
.individual-title
|
.individual-title
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
|
|
||||||
|
.individual-description
|
||||||
|
margin: 0
|
||||||
|
flex-grow: 0.2
|
||||||
|
|
||||||
.text-h6
|
.text-h6
|
||||||
color: white
|
color: white
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
height: 850px
|
height: 850px
|
||||||
|
max-height: 90vh
|
||||||
width: 850px
|
width: 850px
|
||||||
text-align: center
|
text-align: center
|
||||||
padding: 0
|
padding: 0
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
align-items: center
|
align-items: center
|
||||||
justify-content: flex-end
|
justify-content: flex-end
|
||||||
height: 100px
|
height: 100px
|
||||||
|
max-height: 10.5vh
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
background-color: $navy
|
background-color: $navy
|
||||||
|
|
|
@ -267,9 +267,9 @@ block content
|
||||||
h1 Active Users
|
h1 Active Users
|
||||||
if view.activeUsers.length > 0
|
if view.activeUsers.length > 0
|
||||||
- var eventNames = [];
|
- var eventNames = [];
|
||||||
each count, event in view.activeUsers[0].events
|
each event in view.activeUserEventNames
|
||||||
if event.indexOf('classroom') >= 0
|
if event.indexOf('classroom') >= 0
|
||||||
- eventNames.push(event)
|
- eventNames.push(event);
|
||||||
- eventNames.sort(function (a, b) {return a.localeCompare(b);});
|
- eventNames.sort(function (a, b) {return a.localeCompare(b);});
|
||||||
table.table.table-striped.table-condensed
|
table.table.table-striped.table-condensed
|
||||||
tr
|
tr
|
||||||
|
@ -322,9 +322,9 @@ block content
|
||||||
h1 Active Users
|
h1 Active Users
|
||||||
if view.activeUsers.length > 0
|
if view.activeUsers.length > 0
|
||||||
- var eventNames = [];
|
- var eventNames = [];
|
||||||
each count, event in view.activeUsers[0].events
|
each event in view.activeUserEventNames
|
||||||
if event.indexOf('campaign') >= 0
|
if event.indexOf('campaign') >= 0
|
||||||
- eventNames.push(event)
|
- eventNames.push(event);
|
||||||
- eventNames.sort(function (a, b) {return a.localeCompare(b);});
|
- eventNames.sort(function (a, b) {return a.localeCompare(b);});
|
||||||
table.table.table-striped.table-condensed
|
table.table.table-striped.table-condensed
|
||||||
tr
|
tr
|
||||||
|
@ -346,27 +346,13 @@ block content
|
||||||
|
|
||||||
h1 Active Users
|
h1 Active Users
|
||||||
if view.activeUsers.length > 0
|
if view.activeUsers.length > 0
|
||||||
- var eventNames = [];
|
|
||||||
each count, event in view.activeUsers[0].events
|
|
||||||
- eventNames.push(event)
|
|
||||||
- eventNames.sort(function (a, b) {
|
|
||||||
- if (a.indexOf('campaign') == b.indexOf('campaign') || a.indexOf('classroom') == b.indexOf('classroom')) {
|
|
||||||
- return a.localeCompare(b);
|
|
||||||
- }
|
|
||||||
- else if (a.indexOf('campaign') > b.indexOf('campaign')) {
|
|
||||||
- return 1;
|
|
||||||
- }
|
|
||||||
- else {
|
|
||||||
- return -1;
|
|
||||||
- }
|
|
||||||
- });
|
|
||||||
table.table.table-striped.table-condensed
|
table.table.table-striped.table-condensed
|
||||||
tr
|
tr
|
||||||
th(style='min-width:85px;') Day
|
th(style='min-width:85px;') Day
|
||||||
each eventName in eventNames
|
each eventName in view.activeUserEventNames
|
||||||
th= eventName
|
th= eventName
|
||||||
each activeUser in view.activeUsers
|
each activeUser in view.activeUsers
|
||||||
tr
|
tr
|
||||||
td= activeUser.day
|
td= activeUser.day
|
||||||
each eventName in eventNames
|
each eventName in view.activeUserEventNames
|
||||||
td= activeUser.events[eventName] || 0
|
td= activeUser.events[eventName] || 0
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.modal-body-content
|
.modal-body-content
|
||||||
h4
|
h4.choose-type-title
|
||||||
span(data-i18n="signup.choose_type")
|
span(data-i18n="signup.choose_type")
|
||||||
.path-cards
|
.path-cards
|
||||||
.path-card.navy
|
.path-card.navy
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
else
|
else
|
||||||
p(data-i18n="signup.confirm_individual_blurb")
|
p(data-i18n="signup.confirm_individual_blurb")
|
||||||
|
|
||||||
.signup-info-box-wrapper.m-y-3
|
.signup-info-box-wrapper
|
||||||
.text-burgandy(data-i18n="signup.write_this_down")
|
.text-burgandy(data-i18n="signup.write_this_down")
|
||||||
.signup-info-box.text-center
|
.signup-info-box.text-center
|
||||||
if me.get('name')
|
if me.get('name')
|
||||||
|
@ -32,4 +32,4 @@
|
||||||
span(data-i18n="general.email")
|
span(data-i18n="general.email")
|
||||||
| : #{me.get('email')}
|
| : #{me.get('email')}
|
||||||
|
|
||||||
button#start-btn.btn.btn-navy.btn-lg.m-y-3(data-i18n="signup.start_playing")
|
button#start-btn.btn.btn-navy.btn-lg.m-t-3(data-i18n="signup.start_playing")
|
||||||
|
|
|
@ -45,7 +45,7 @@ form.modal-body.segment-check
|
||||||
option(value='',data-i18n="calendar.day")
|
option(value='',data-i18n="calendar.day")
|
||||||
for day in _.range(1,32)
|
for day in _.range(1,32)
|
||||||
option(selected=(day == view.signupState.get('birthdayDay'))) #{day}
|
option(selected=(day == view.signupState.get('birthdayDay'))) #{day}
|
||||||
select#birthday-year-input.input-large.form-control(name="birthdayYear", style="width: 90px;")
|
select#birthday-year-input.input-large.form-control(name="birthdayYear", style="width: 90px; float: left")
|
||||||
option(value='',data-i18n="calendar.year")
|
option(value='',data-i18n="calendar.year")
|
||||||
- var thisYear = new Date().getFullYear()
|
- var thisYear = new Date().getFullYear()
|
||||||
for year in _.range(thisYear, thisYear - 100, -1)
|
for year in _.range(thisYear, thisYear - 100, -1)
|
||||||
|
|
|
@ -182,7 +182,8 @@ mixin studentsTab
|
||||||
th.checkbox-col.select-all.small.text-center
|
th.checkbox-col.select-all.small.text-center
|
||||||
span(data-i18n="teacher.select_all")
|
span(data-i18n="teacher.select_all")
|
||||||
.checkbox-flat
|
.checkbox-flat
|
||||||
input(type='checkbox' id='checkbox-all-students')
|
- var allStudentsChecked = _.all(state.get('checkboxStates'))
|
||||||
|
input(type='checkbox', id='checkbox-all-students', checked=allStudentsChecked)
|
||||||
label.checkmark(for='checkbox-all-students')
|
label.checkmark(for='checkbox-all-students')
|
||||||
th
|
th
|
||||||
+sortButtons
|
+sortButtons
|
||||||
|
@ -201,7 +202,7 @@ mixin studentRow(student)
|
||||||
tr.student-row.alternating-background
|
tr.student-row.alternating-background
|
||||||
td.checkbox-col.student-checkbox
|
td.checkbox-col.student-checkbox
|
||||||
.checkbox-flat
|
.checkbox-flat
|
||||||
input(type='checkbox' id='checkbox-student-' + student.id, data-student-id=student.id)
|
input(type='checkbox' id='checkbox-student-' + student.id, data-student-id=student.id, checked=state.get('checkboxStates')[student.id])
|
||||||
label.checkmark(for='checkbox-student-' + student.id)
|
label.checkmark(for='checkbox-student-' + student.id)
|
||||||
td.student-info-col
|
td.student-info-col
|
||||||
.student-info
|
.student-info
|
||||||
|
|
|
@ -132,7 +132,8 @@ block outer_content
|
||||||
|
|
||||||
div.tab-pane#editor-level-tasks-tab-view
|
div.tab-pane#editor-level-tasks-tab-view
|
||||||
|
|
||||||
div.tab-pane#editor-level-patches
|
div.tab-pane#editor-level-patches.nano
|
||||||
|
.nano-content
|
||||||
.patches-view
|
.patches-view
|
||||||
|
|
||||||
div.tab-pane#related-achievements-view
|
div.tab-pane#related-achievements-view
|
||||||
|
|
|
@ -73,6 +73,7 @@ module.exports = class AnalyticsView extends RootView
|
||||||
# Add campaign/classroom DAU 30-day averages and daily totals
|
# Add campaign/classroom DAU 30-day averages and daily totals
|
||||||
campaignDauTotals = []
|
campaignDauTotals = []
|
||||||
classroomDauTotals = []
|
classroomDauTotals = []
|
||||||
|
eventMap = {}
|
||||||
for entry in @activeUsers
|
for entry in @activeUsers
|
||||||
day = entry.day
|
day = entry.day
|
||||||
campaignDauTotal = 0
|
campaignDauTotal = 0
|
||||||
|
@ -82,18 +83,31 @@ module.exports = class AnalyticsView extends RootView
|
||||||
campaignDauTotal += count
|
campaignDauTotal += count
|
||||||
else if event.indexOf('DAU classroom') >= 0
|
else if event.indexOf('DAU classroom') >= 0
|
||||||
classroomDauTotal += count
|
classroomDauTotal += count
|
||||||
|
eventMap[event] = true
|
||||||
entry.events['DAU campaign total'] = campaignDauTotal
|
entry.events['DAU campaign total'] = campaignDauTotal
|
||||||
|
eventMap['DAU campaign total'] = true
|
||||||
campaignDauTotals.unshift(campaignDauTotal)
|
campaignDauTotals.unshift(campaignDauTotal)
|
||||||
campaignDauTotals.pop() while campaignDauTotals.length > 30
|
campaignDauTotals.pop() while campaignDauTotals.length > 30
|
||||||
if campaignDauTotals.length is 30
|
if campaignDauTotals.length is 30
|
||||||
entry.events['DAU campaign 30-day average'] = Math.round(_.reduce(campaignDauTotals, (a, b) -> a + b) / 30)
|
entry.events['DAU campaign 30-day average'] = Math.round(_.reduce(campaignDauTotals, (a, b) -> a + b) / 30)
|
||||||
|
eventMap['DAU campaign 30-day average'] = true
|
||||||
entry.events['DAU classroom total'] = classroomDauTotal
|
entry.events['DAU classroom total'] = classroomDauTotal
|
||||||
|
eventMap['DAU classroom total'] = true
|
||||||
classroomDauTotals.unshift(classroomDauTotal)
|
classroomDauTotals.unshift(classroomDauTotal)
|
||||||
classroomDauTotals.pop() while classroomDauTotals.length > 30
|
classroomDauTotals.pop() while classroomDauTotals.length > 30
|
||||||
if classroomDauTotals.length is 30
|
if classroomDauTotals.length is 30
|
||||||
entry.events['DAU classroom 30-day average'] = Math.round(_.reduce(classroomDauTotals, (a, b) -> a + b) / 30)
|
entry.events['DAU classroom 30-day average'] = Math.round(_.reduce(classroomDauTotals, (a, b) -> a + b) / 30)
|
||||||
|
eventMap['DAU classroom 30-day average'] = true
|
||||||
|
|
||||||
@activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
|
@activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
|
||||||
|
@activeUserEventNames = Object.keys(eventMap)
|
||||||
|
@activeUserEventNames.sort (a, b) ->
|
||||||
|
if a.indexOf('campaign') is b.indexOf('campaign') or a.indexOf('classroom') is b.indexOf('classroom')
|
||||||
|
a.localeCompare(b)
|
||||||
|
else if a.indexOf('campaign') > b.indexOf('campaign')
|
||||||
|
1
|
||||||
|
else
|
||||||
|
-1
|
||||||
|
|
||||||
@updateAllKPIChartData()
|
@updateAllKPIChartData()
|
||||||
@updateActiveUsersChartData()
|
@updateActiveUsersChartData()
|
||||||
|
@ -183,7 +197,7 @@ module.exports = class AnalyticsView extends RootView
|
||||||
@supermodel.addRequestResource({
|
@supermodel.addRequestResource({
|
||||||
url: '/db/prepaid/-/courses'
|
url: '/db/prepaid/-/courses'
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
data: {project: {maxRedeemers: 1, properties: 1, redeemers: 1}}
|
data: {project: {endDate: 1, maxRedeemers: 1, properties: 1, redeemers: 1}}
|
||||||
success: (prepaids) =>
|
success: (prepaids) =>
|
||||||
paidDayMaxMap = {}
|
paidDayMaxMap = {}
|
||||||
paidDayRedeemedMap = {}
|
paidDayRedeemedMap = {}
|
||||||
|
@ -201,14 +215,13 @@ module.exports = class AnalyticsView extends RootView
|
||||||
redeemDay = redeemer.date.substring(0, 10)
|
redeemDay = redeemer.date.substring(0, 10)
|
||||||
trialDayRedeemedMap[redeemDay] ?= 0
|
trialDayRedeemedMap[redeemDay] ?= 0
|
||||||
trialDayRedeemedMap[redeemDay]++
|
trialDayRedeemedMap[redeemDay]++
|
||||||
else
|
else if not prepaid.endDate? or new Date(prepaid.endDate) > new Date()
|
||||||
paidDayMaxMap[day] ?= 0
|
paidDayMaxMap[day] ?= 0
|
||||||
paidDayMaxMap[day] += prepaid.maxRedeemers
|
paidDayMaxMap[day] += prepaid.maxRedeemers
|
||||||
for redeemer in prepaid.redeemers
|
for redeemer in prepaid.redeemers
|
||||||
redeemDay = redeemer.date.substring(0, 10)
|
redeemDay = redeemer.date.substring(0, 10)
|
||||||
paidDayRedeemedMap[redeemDay] ?= 0
|
paidDayRedeemedMap[redeemDay] ?= 0
|
||||||
paidDayRedeemedMap[redeemDay]++
|
paidDayRedeemedMap[redeemDay]++
|
||||||
|
|
||||||
@dayEnrollmentsMap = {}
|
@dayEnrollmentsMap = {}
|
||||||
@paidCourseTotalEnrollments = []
|
@paidCourseTotalEnrollments = []
|
||||||
for day, count of paidDayMaxMap
|
for day, count of paidDayMaxMap
|
||||||
|
@ -368,6 +381,9 @@ module.exports = class AnalyticsView extends RootView
|
||||||
|
|
||||||
# Trim points preceding days
|
# Trim points preceding days
|
||||||
if points.length and days.length and points[0].day.localeCompare(days[0]) < 0
|
if points.length and days.length and points[0].day.localeCompare(days[0]) < 0
|
||||||
|
if points[points.length - 1].day.localeCompare(days[0]) < 0
|
||||||
|
points = []
|
||||||
|
else
|
||||||
for point, i in points
|
for point, i in points
|
||||||
if point.day.localeCompare(days[0]) >= 0
|
if point.day.localeCompare(days[0]) >= 0
|
||||||
points.splice(0, i)
|
points.splice(0, i)
|
||||||
|
@ -375,7 +391,7 @@ module.exports = class AnalyticsView extends RootView
|
||||||
|
|
||||||
# Ensure points for each day
|
# Ensure points for each day
|
||||||
for day, i in days
|
for day, i in days
|
||||||
if points.length <= i or points[i].day isnt day
|
if points.length <= i or points[i]?.day isnt day
|
||||||
prevY = if i > 0 then points[i - 1].y else 0.0
|
prevY = if i > 0 then points[i - 1].y else 0.0
|
||||||
points.splice i, 0,
|
points.splice i, 0,
|
||||||
day: day
|
day: day
|
||||||
|
@ -648,16 +664,14 @@ module.exports = class AnalyticsView extends RootView
|
||||||
dailyMax = 0
|
dailyMax = 0
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
total = 0
|
|
||||||
for entry in @paidCourseTotalEnrollments
|
for entry in @paidCourseTotalEnrollments
|
||||||
total += entry.count
|
|
||||||
data.push
|
data.push
|
||||||
day: entry.day
|
day: entry.day
|
||||||
value: total
|
value: entry.count
|
||||||
points = @createLineChartPoints(days, data)
|
points = @createLineChartPoints(days, data)
|
||||||
@enrollmentsChartLines.push
|
@enrollmentsChartLines.push
|
||||||
points: points
|
points: points
|
||||||
description: 'Total paid enrollments issued'
|
description: 'Paid enrollments issued'
|
||||||
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
||||||
strokeWidth: 1
|
strokeWidth: 1
|
||||||
min: 0
|
min: 0
|
||||||
|
@ -666,16 +680,14 @@ module.exports = class AnalyticsView extends RootView
|
||||||
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
|
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
total = 0
|
|
||||||
for entry in @paidCourseRedeemedEnrollments
|
for entry in @paidCourseRedeemedEnrollments
|
||||||
total += entry.count
|
|
||||||
data.push
|
data.push
|
||||||
day: entry.day
|
day: entry.day
|
||||||
value: total
|
value: entry.count
|
||||||
points = @createLineChartPoints(days, data)
|
points = @createLineChartPoints(days, data)
|
||||||
@enrollmentsChartLines.push
|
@enrollmentsChartLines.push
|
||||||
points: points
|
points: points
|
||||||
description: 'Total paid enrollments redeemed'
|
description: 'Paid enrollments redeemed'
|
||||||
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
||||||
strokeWidth: 1
|
strokeWidth: 1
|
||||||
min: 0
|
min: 0
|
||||||
|
@ -684,16 +696,14 @@ module.exports = class AnalyticsView extends RootView
|
||||||
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
|
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
total = 0
|
|
||||||
for entry in @trialCourseTotalEnrollments
|
for entry in @trialCourseTotalEnrollments
|
||||||
total += entry.count
|
|
||||||
data.push
|
data.push
|
||||||
day: entry.day
|
day: entry.day
|
||||||
value: total
|
value: entry.count
|
||||||
points = @createLineChartPoints(days, data)
|
points = @createLineChartPoints(days, data, true)
|
||||||
@enrollmentsChartLines.push
|
@enrollmentsChartLines.push
|
||||||
points: points
|
points: points
|
||||||
description: 'Total trial enrollments issued'
|
description: 'Trial enrollments issued'
|
||||||
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
||||||
strokeWidth: 1
|
strokeWidth: 1
|
||||||
min: 0
|
min: 0
|
||||||
|
@ -702,16 +712,14 @@ module.exports = class AnalyticsView extends RootView
|
||||||
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
|
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
total = 0
|
|
||||||
for entry in @trialCourseRedeemedEnrollments
|
for entry in @trialCourseRedeemedEnrollments
|
||||||
total += entry.count
|
|
||||||
data.push
|
data.push
|
||||||
day: entry.day
|
day: entry.day
|
||||||
value: total
|
value: entry.count
|
||||||
points = @createLineChartPoints(days, data)
|
points = @createLineChartPoints(days, data)
|
||||||
@enrollmentsChartLines.push
|
@enrollmentsChartLines.push
|
||||||
points: points
|
points: points
|
||||||
description: 'Total trial enrollments redeemed'
|
description: 'Trial enrollments redeemed'
|
||||||
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
||||||
strokeWidth: 1
|
strokeWidth: 1
|
||||||
min: 0
|
min: 0
|
||||||
|
|
|
@ -23,6 +23,7 @@ module.exports = class MainAdminView extends RootView
|
||||||
'submit #user-search-form': 'onSubmitUserSearchForm'
|
'submit #user-search-form': 'onSubmitUserSearchForm'
|
||||||
'click #stop-spying-btn': 'onClickStopSpyingButton'
|
'click #stop-spying-btn': 'onClickStopSpyingButton'
|
||||||
'click #increment-button': 'incrementUserAttribute'
|
'click #increment-button': 'incrementUserAttribute'
|
||||||
|
'click .user-spy-button': 'onClickUserSpyButton'
|
||||||
'click #user-search-result': 'onClickUserSearchResult'
|
'click #user-search-result': 'onClickUserSearchResult'
|
||||||
'click #create-free-sub-btn': 'onClickFreeSubLink'
|
'click #create-free-sub-btn': 'onClickFreeSubLink'
|
||||||
'click #terminal-create': 'onClickTerminalSubLink'
|
'click #terminal-create': 'onClickTerminalSubLink'
|
||||||
|
@ -31,31 +32,10 @@ module.exports = class MainAdminView extends RootView
|
||||||
getTitle: -> return $.i18n.t('account_settings.admin')
|
getTitle: -> return $.i18n.t('account_settings.admin')
|
||||||
|
|
||||||
initialize: ->
|
initialize: ->
|
||||||
@campaigns = new Campaigns()
|
|
||||||
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
|
||||||
|
|
||||||
if window.amActually
|
if window.amActually
|
||||||
@amActually = new User({_id: window.amActually})
|
@amActually = new User({_id: window.amActually})
|
||||||
@amActually.fetch()
|
@amActually.fetch()
|
||||||
@supermodel.trackModel(@amActually)
|
@supermodel.trackModel(@amActually)
|
||||||
if me.isAdmin()
|
|
||||||
@supermodel.trackRequest @campaigns.fetchByType('course', { data: { project: 'levels' } })
|
|
||||||
@supermodel.loadCollection(@courses, 'courses')
|
|
||||||
super()
|
|
||||||
|
|
||||||
onLoaded: ->
|
|
||||||
campaignCourseIndexMap = {}
|
|
||||||
for course, index in @courses.models
|
|
||||||
campaignCourseIndexMap[course.get('campaignID')] = index + 1
|
|
||||||
@courseLevels = []
|
|
||||||
for campaign in @campaigns.models
|
|
||||||
continue unless campaignCourseIndexMap[campaign.id]
|
|
||||||
for levelID, level of campaign.get('levels')
|
|
||||||
@courseLevels.push({
|
|
||||||
levelID
|
|
||||||
slug: level.slug
|
|
||||||
courseIndex: campaignCourseIndexMap[campaign.id]
|
|
||||||
})
|
|
||||||
super()
|
super()
|
||||||
|
|
||||||
onClickStopSpyingButton: ->
|
onClickStopSpyingButton: ->
|
||||||
|
@ -80,6 +60,18 @@ module.exports = class MainAdminView extends RootView
|
||||||
errors.showNotyNetworkError(arguments...)
|
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) ->
|
onSubmitUserSearchForm: (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
searchValue = @$el.find('#user-search').val()
|
searchValue = @$el.find('#user-search').val()
|
||||||
|
@ -97,7 +89,7 @@ module.exports = class MainAdminView extends RootView
|
||||||
forms.enableSubmit(@$('#user-search-button'))
|
forms.enableSubmit(@$('#user-search-button'))
|
||||||
result = ''
|
result = ''
|
||||||
if users.length
|
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>"
|
result = "<table class=\"table\">#{result.join('\n')}</table>"
|
||||||
@$el.find('#user-search-result').html(result)
|
@$el.find('#user-search-result').html(result)
|
||||||
|
|
||||||
|
@ -157,42 +149,31 @@ module.exports = class MainAdminView extends RootView
|
||||||
@supermodel.addRequestResource('create_prepaid', options, 0).load()
|
@supermodel.addRequestResource('create_prepaid', options, 0).load()
|
||||||
|
|
||||||
onClickExportProgress: ->
|
onClickExportProgress: ->
|
||||||
return unless @courseLevels?.length > 0
|
|
||||||
$('.classroom-progress-csv').prop('disabled', true)
|
$('.classroom-progress-csv').prop('disabled', true)
|
||||||
|
|
||||||
classCode = $('.classroom-progress-class-code').val()
|
classCode = $('.classroom-progress-class-code').val()
|
||||||
|
classroom = null
|
||||||
|
courseLevels = []
|
||||||
|
sessions = null
|
||||||
|
users = null
|
||||||
userMap = {}
|
userMap = {}
|
||||||
new Promise((resolve, reject) =>
|
Promise.resolve(new Classroom().fetchByCode(classCode))
|
||||||
new Classroom().fetchByCode(classCode, {
|
.then (model) =>
|
||||||
success: resolve
|
classroom = new Classroom({ _id: model.data._id })
|
||||||
error: (model, response, options) => reject(response)
|
Promise.resolve(classroom.fetch())
|
||||||
})
|
.then (model) =>
|
||||||
)
|
for course, index in classroom.get('courses')
|
||||||
.then (classroom) =>
|
for level in course.levels
|
||||||
new Promise((resolve, reject) =>
|
courseLevels.push
|
||||||
new Classroom({ _id: classroom.id }).fetch({
|
courseIndex: index + 1
|
||||||
success: resolve
|
levelID: level.original
|
||||||
error: (model, response, options) => reject(response)
|
slug: level.slug
|
||||||
})
|
users = new Users()
|
||||||
)
|
Promise.resolve($.when(users.fetchForClassroom(classroom)...))
|
||||||
.then (classroom) =>
|
.then (models) =>
|
||||||
new Promise((resolve, reject) =>
|
|
||||||
new Users().fetchForClassroom(classroom, {
|
|
||||||
success: (models, response, options) =>
|
|
||||||
resolve([classroom, models]) if models?.loaded
|
|
||||||
error: (models, response, options) => reject(response)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.then ([classroom, users]) =>
|
|
||||||
userMap[user.id] = user for user in users.models
|
userMap[user.id] = user for user in users.models
|
||||||
new Promise((resolve, reject) =>
|
sessions = new LevelSessions()
|
||||||
new LevelSessions().fetchForAllClassroomMembers(classroom, {
|
Promise.resolve($.when(sessions.fetchForAllClassroomMembers(classroom)...))
|
||||||
success: (models, response, options) =>
|
.then (models) =>
|
||||||
resolve(models) if models?.loaded
|
|
||||||
error: (models, response, options) => reject(response)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.then (sessions) =>
|
|
||||||
userLevelPlaytimeMap = {}
|
userLevelPlaytimeMap = {}
|
||||||
for session in sessions.models
|
for session in sessions.models
|
||||||
continue unless session.get('state')?.complete
|
continue unless session.get('state')?.complete
|
||||||
|
@ -205,7 +186,7 @@ module.exports = class MainAdminView extends RootView
|
||||||
userPlaytimes = []
|
userPlaytimes = []
|
||||||
for userID, user of userMap
|
for userID, user of userMap
|
||||||
playtimes = [user.get('name') ? 'Anonymous']
|
playtimes = [user.get('name') ? 'Anonymous']
|
||||||
for level in @courseLevels
|
for level in courseLevels
|
||||||
if userLevelPlaytimeMap[userID]?[level.levelID]?
|
if userLevelPlaytimeMap[userID]?[level.levelID]?
|
||||||
rawSeconds = parseInt(userLevelPlaytimeMap[userID][level.levelID])
|
rawSeconds = parseInt(userLevelPlaytimeMap[userID][level.levelID])
|
||||||
hours = Math.floor(rawSeconds / 60 / 60)
|
hours = Math.floor(rawSeconds / 60 / 60)
|
||||||
|
@ -222,7 +203,7 @@ module.exports = class MainAdminView extends RootView
|
||||||
columnLabels = "Username"
|
columnLabels = "Username"
|
||||||
currentLevel = 1
|
currentLevel = 1
|
||||||
lastCourseIndex = 1
|
lastCourseIndex = 1
|
||||||
for level in @courseLevels
|
for level in courseLevels
|
||||||
unless level.courseIndex is lastCourseIndex
|
unless level.courseIndex is lastCourseIndex
|
||||||
currentLevel = 1
|
currentLevel = 1
|
||||||
lastCourseIndex = level.courseIndex
|
lastCourseIndex = level.courseIndex
|
||||||
|
@ -238,3 +219,4 @@ module.exports = class MainAdminView extends RootView
|
||||||
.catch (error) ->
|
.catch (error) ->
|
||||||
$('.classroom-progress-csv').prop('disabled', false)
|
$('.classroom-progress-csv').prop('disabled', false)
|
||||||
console.error error
|
console.error error
|
||||||
|
throw error
|
||||||
|
|
|
@ -28,6 +28,7 @@ module.exports = class BasicInfoView extends CocoView
|
||||||
events:
|
events:
|
||||||
'change input[name="email"]': 'onChangeEmail'
|
'change input[name="email"]': 'onChangeEmail'
|
||||||
'change input[name="name"]': 'onChangeName'
|
'change input[name="name"]': 'onChangeName'
|
||||||
|
'change input[name="password"]': 'onChangePassword'
|
||||||
'click .back-button': 'onClickBackButton'
|
'click .back-button': 'onClickBackButton'
|
||||||
'submit form': 'onSubmitForm'
|
'submit form': 'onSubmitForm'
|
||||||
'click .use-suggested-name-link': 'onClickUseSuggestedNameLink'
|
'click .use-suggested-name-link': 'onClickUseSuggestedNameLink'
|
||||||
|
@ -51,7 +52,14 @@ module.exports = class BasicInfoView extends CocoView
|
||||||
@listenTo @signupState, 'change:facebookEnabled', -> @renderSelectors('.auth-network-logins')
|
@listenTo @signupState, 'change:facebookEnabled', -> @renderSelectors('.auth-network-logins')
|
||||||
@listenTo @signupState, 'change:gplusEnabled', -> @renderSelectors('.auth-network-logins')
|
@listenTo @signupState, 'change:gplusEnabled', -> @renderSelectors('.auth-network-logins')
|
||||||
|
|
||||||
onChangeEmail: ->
|
# These values are passed along to AuthModal if the user clicks "Sign In" (handled by CreateAccountModal)
|
||||||
|
updateAuthModalInitialValues: (values) ->
|
||||||
|
@signupState.set {
|
||||||
|
authModalInitialValues: _.merge @signupState.get('authModalInitialValues'), values
|
||||||
|
}, { silent: true }
|
||||||
|
|
||||||
|
onChangeEmail: (e) ->
|
||||||
|
@updateAuthModalInitialValues { email: @$(e.currentTarget).val() }
|
||||||
@checkEmail()
|
@checkEmail()
|
||||||
|
|
||||||
checkEmail: ->
|
checkEmail: ->
|
||||||
|
@ -85,7 +93,8 @@ module.exports = class BasicInfoView extends CocoView
|
||||||
})
|
})
|
||||||
return @state.get('checkEmailPromise')
|
return @state.get('checkEmailPromise')
|
||||||
|
|
||||||
onChangeName: ->
|
onChangeName: (e) ->
|
||||||
|
@updateAuthModalInitialValues { name: @$(e.currentTarget).val() }
|
||||||
@checkName()
|
@checkName()
|
||||||
|
|
||||||
checkName: ->
|
checkName: ->
|
||||||
|
@ -122,6 +131,9 @@ module.exports = class BasicInfoView extends CocoView
|
||||||
|
|
||||||
return @state.get('checkNamePromise')
|
return @state.get('checkNamePromise')
|
||||||
|
|
||||||
|
onChangePassword: (e) ->
|
||||||
|
@updateAuthModalInitialValues { password: @$(e.currentTarget).val() }
|
||||||
|
|
||||||
checkBasicInfo: (data) ->
|
checkBasicInfo: (data) ->
|
||||||
# TODO: Move this to somewhere appropriate
|
# TODO: Move this to somewhere appropriate
|
||||||
tv4.addFormat({
|
tv4.addFormat({
|
||||||
|
|
|
@ -62,6 +62,7 @@ module.exports = class CreateAccountModal extends ModalView
|
||||||
gplusEnabled: application.gplusHandler.apiLoaded
|
gplusEnabled: application.gplusHandler.apiLoaded
|
||||||
classCode
|
classCode
|
||||||
birthday: new Date('') # so that birthday.getTime() is NaN
|
birthday: new Date('') # so that birthday.getTime() is NaN
|
||||||
|
authModalInitialValues: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
{ startOnPath } = options
|
{ startOnPath } = options
|
||||||
|
@ -112,5 +113,4 @@ module.exports = class CreateAccountModal extends ModalView
|
||||||
document.location.reload()
|
document.location.reload()
|
||||||
|
|
||||||
onClickLoginLink: ->
|
onClickLoginLink: ->
|
||||||
# TODO: Make sure the right information makes its way into the state.
|
@openModalView(new AuthModal({ initialValues: @signupState.get('authModalInitialValues') }))
|
||||||
@openModalView(new AuthModal({ initialValues: @signupState.pick(['email', 'name', 'password']) }))
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ module.exports = class SegmentCheckView extends CocoView
|
||||||
events:
|
events:
|
||||||
'click .back-to-account-type': -> @trigger 'nav-back'
|
'click .back-to-account-type': -> @trigger 'nav-back'
|
||||||
'input .class-code-input': 'onInputClassCode'
|
'input .class-code-input': 'onInputClassCode'
|
||||||
'input .birthday-form-group': 'onInputBirthday'
|
'change .birthday-form-group': 'onInputBirthday'
|
||||||
'submit form.segment-check': 'onSubmitSegmentCheck'
|
'submit form.segment-check': 'onSubmitSegmentCheck'
|
||||||
'click .individual-path-button': -> @trigger 'choose-path', 'individual'
|
'click .individual-path-button': -> @trigger 'choose-path', 'individual'
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ module.exports = class TeacherClassView extends RootView
|
||||||
assigningToNobody: false
|
assigningToNobody: false
|
||||||
assigningToUnenrolled: false
|
assigningToUnenrolled: false
|
||||||
selectedCourse: undefined
|
selectedCourse: undefined
|
||||||
|
checkboxStates: {}
|
||||||
classStats:
|
classStats:
|
||||||
averagePlaytime: ""
|
averagePlaytime: ""
|
||||||
totalPlaytime: ""
|
totalPlaytime: ""
|
||||||
|
@ -145,6 +146,10 @@ module.exports = class TeacherClassView extends RootView
|
||||||
classStats = @calculateClassStats()
|
classStats = @calculateClassStats()
|
||||||
@state.set classStats: classStats if classStats
|
@state.set classStats: classStats if classStats
|
||||||
@state.set students: @students
|
@state.set students: @students
|
||||||
|
checkboxStates = {}
|
||||||
|
for student in @students.models
|
||||||
|
checkboxStates[student.id] = @state.get('checkboxStates')[student.id] or false
|
||||||
|
@state.set { checkboxStates }
|
||||||
@listenTo @students, 'sort', ->
|
@listenTo @students, 'sort', ->
|
||||||
@state.set students: @students
|
@state.set students: @students
|
||||||
@listenTo @, 'course-select:change', ({ selectedCourse }) ->
|
@listenTo @, 'course-select:change', ({ selectedCourse }) ->
|
||||||
|
@ -291,8 +296,7 @@ module.exports = class TeacherClassView extends RootView
|
||||||
@trigger 'course-select:change', { selectedCourse: @courses.get($(e.currentTarget).val()) }
|
@trigger 'course-select:change', { selectedCourse: @courses.get($(e.currentTarget).val()) }
|
||||||
|
|
||||||
getSelectedStudentIDs: ->
|
getSelectedStudentIDs: ->
|
||||||
@$('.student-row .checkbox-flat input:checked').map (index, checkbox) ->
|
Object.keys(_.pick @state.get('checkboxStates'), (checked) -> checked)
|
||||||
$(checkbox).data('student-id')
|
|
||||||
|
|
||||||
ensureInstance: (courseID) ->
|
ensureInstance: (courseID) ->
|
||||||
|
|
||||||
|
@ -304,7 +308,7 @@ module.exports = class TeacherClassView extends RootView
|
||||||
window.tracker?.trackEvent $(e.currentTarget).data('event-action'), category: 'Teachers', classroomID: @classroom.id, userID: userID, ['Mixpanel']
|
window.tracker?.trackEvent $(e.currentTarget).data('event-action'), category: 'Teachers', classroomID: @classroom.id, userID: userID, ['Mixpanel']
|
||||||
|
|
||||||
onClickBulkEnroll: ->
|
onClickBulkEnroll: ->
|
||||||
userIDs = @getSelectedStudentIDs().toArray()
|
userIDs = @getSelectedStudentIDs()
|
||||||
selectedUsers = new Users(@students.get(userID) for userID in userIDs)
|
selectedUsers = new Users(@students.get(userID) for userID in userIDs)
|
||||||
@enrollStudents(selectedUsers)
|
@enrollStudents(selectedUsers)
|
||||||
window.tracker?.trackEvent 'Teachers Class Students Enroll Selected', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
|
window.tracker?.trackEvent 'Teachers Class Students Enroll Selected', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
|
||||||
|
@ -385,10 +389,9 @@ module.exports = class TeacherClassView extends RootView
|
||||||
onClickBulkAssign: ->
|
onClickBulkAssign: ->
|
||||||
courseID = @$('.bulk-course-select').val()
|
courseID = @$('.bulk-course-select').val()
|
||||||
selectedIDs = @getSelectedStudentIDs()
|
selectedIDs = @getSelectedStudentIDs()
|
||||||
members = selectedIDs.filter((index, userID) =>
|
members = selectedIDs.filter (userID) =>
|
||||||
user = @students.get(userID)
|
user = @students.get(userID)
|
||||||
user.isEnrolled()
|
user.isEnrolled()
|
||||||
).toArray()
|
|
||||||
assigningToUnenrolled = _.any selectedIDs, (userID) =>
|
assigningToUnenrolled = _.any selectedIDs, (userID) =>
|
||||||
not @students.get(userID).isEnrolled()
|
not @students.get(userID).isEnrolled()
|
||||||
assigningToNobody = selectedIDs.length is 0
|
assigningToNobody = selectedIDs.length is 0
|
||||||
|
@ -417,23 +420,22 @@ module.exports = class TeacherClassView extends RootView
|
||||||
|
|
||||||
onClickSelectAll: (e) ->
|
onClickSelectAll: (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
checkboxes = @$('.student-checkbox input')
|
checkboxStates = _.clone @state.get('checkboxStates')
|
||||||
if _.all(checkboxes, 'checked')
|
if _.all(checkboxStates)
|
||||||
@$('.select-all input').prop('checked', false)
|
for studentID of checkboxStates
|
||||||
checkboxes.prop('checked', false)
|
checkboxStates[studentID] = false
|
||||||
else
|
else
|
||||||
@$('.select-all input').prop('checked', true)
|
for studentID of checkboxStates
|
||||||
checkboxes.prop('checked', true)
|
checkboxStates[studentID] = true
|
||||||
null
|
@state.set { checkboxStates }
|
||||||
|
|
||||||
onClickStudentCheckbox: (e) ->
|
onClickStudentCheckbox: (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
# $(e.target).$()
|
|
||||||
checkbox = $(e.currentTarget).find('input')
|
checkbox = $(e.currentTarget).find('input')
|
||||||
checkbox.prop('checked', not checkbox.prop('checked'))
|
studentID = checkbox.data('student-id')
|
||||||
# checkboxes.prop('checked', false)
|
checkboxStates = _.clone @state.get('checkboxStates')
|
||||||
checkboxes = @$('.student-checkbox input')
|
checkboxStates[studentID] = not checkboxStates[studentID]
|
||||||
@$('.select-all input').prop('checked', _.all(checkboxes, 'checked'))
|
@state.set { checkboxStates }
|
||||||
|
|
||||||
calculateClassStats: ->
|
calculateClassStats: ->
|
||||||
return {} unless @classroom.sessions?.loaded and @students.loaded
|
return {} unless @classroom.sessions?.loaded and @students.loaded
|
||||||
|
|
|
@ -16,6 +16,7 @@ module.exports = class SettingsTabView extends CocoView
|
||||||
'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals',
|
'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals',
|
||||||
'type', 'terrain', 'showsGuide', 'banner', 'employerDescription', 'loadingTip', 'requiresSubscription',
|
'type', 'terrain', 'showsGuide', 'banner', 'employerDescription', 'loadingTip', 'requiresSubscription',
|
||||||
'helpVideos', 'replayable', 'scoreTypes', 'concepts', 'picoCTFProblem', 'practice', 'practiceThresholdMinutes'
|
'helpVideos', 'replayable', 'scoreTypes', 'concepts', 'picoCTFProblem', 'practice', 'practiceThresholdMinutes'
|
||||||
|
'shareable'
|
||||||
]
|
]
|
||||||
|
|
||||||
subscriptions:
|
subscriptions:
|
||||||
|
|
|
@ -595,7 +595,7 @@ module.exports = class ThangsTabView extends CocoView
|
||||||
@level.set 'thangs', thangs
|
@level.set 'thangs', thangs
|
||||||
return if @editThangView
|
return if @editThangView
|
||||||
return if skipSerialization
|
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
|
try
|
||||||
@world.loadFromLevel serializedLevel, false
|
@world.loadFromLevel serializedLevel, false
|
||||||
catch error
|
catch error
|
||||||
|
|
|
@ -25,9 +25,9 @@ module.exports = class VerifierTest extends CocoClass
|
||||||
@loadStartTime = new Date()
|
@loadStartTime = new Date()
|
||||||
@god = new God maxAngels: 1, headless: true
|
@god = new God maxAngels: 1, headless: true
|
||||||
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, headless: true, fakeSessionConfig: {codeLanguage: @language, callback: @configureSession}
|
@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
|
# Called when we have enough to build the world, but not everything is loaded
|
||||||
@grabLevelLoaderData()
|
@grabLevelLoaderData()
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ module.exports = class VerifierTest extends CocoClass
|
||||||
@solution = @levelLoader.session.solution
|
@solution = @levelLoader.session.solution
|
||||||
|
|
||||||
setupGod: ->
|
setupGod: ->
|
||||||
@god.setLevel @level.serialize @supermodel, @session
|
@god.setLevel @level.serialize {@supermodel, @session, otherSession: null, headless: true, sessionless: false}
|
||||||
@god.setLevelSessionIDs [@session.id]
|
@god.setLevelSessionIDs [@session.id]
|
||||||
@god.setWorldClassMap @world.classMap
|
@god.setWorldClassMap @world.classMap
|
||||||
@god.lastFlagHistory = @session.get('state').flagHistory
|
@god.lastFlagHistory = @session.get('state').flagHistory
|
||||||
|
@ -122,8 +122,10 @@ module.exports = class VerifierTest extends CocoClass
|
||||||
setTimeout @cleanup, 100
|
setTimeout @cleanup, 100
|
||||||
|
|
||||||
cleanup: =>
|
cleanup: =>
|
||||||
|
if @levelLoader
|
||||||
|
@stopListening @levelLoader
|
||||||
|
@levelLoader.destroy()
|
||||||
if @god
|
if @god
|
||||||
@stopListening @god
|
@stopListening @god
|
||||||
@god.destroy()
|
@god.destroy()
|
||||||
|
|
||||||
@world = null
|
@world = null
|
||||||
|
|
|
@ -42,6 +42,7 @@ module.exports = class LadderView extends RootView
|
||||||
initialize: (options, @levelID, @leagueType, @leagueID) ->
|
initialize: (options, @levelID, @leagueType, @leagueID) ->
|
||||||
@level = @supermodel.loadModel(new Level(_id: @levelID)).model
|
@level = @supermodel.loadModel(new Level(_id: @levelID)).model
|
||||||
@level.once 'sync', =>
|
@level.once 'sync', =>
|
||||||
|
return if @destroyed
|
||||||
@levelDescription = marked(@level.get('description')) if @level.get('description')
|
@levelDescription = marked(@level.get('description')) if @level.get('description')
|
||||||
@teams = teamDataFromLevel @level
|
@teams = teamDataFromLevel @level
|
||||||
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(@levelID), 'your_sessions', {cache: false}).model
|
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(@levelID), 'your_sessions', {cache: false}).model
|
||||||
|
|
|
@ -69,7 +69,7 @@ module.exports = class SpectateLevelView extends RootView
|
||||||
@load()
|
@load()
|
||||||
|
|
||||||
setLevel: (@level, @supermodel) ->
|
setLevel: (@level, @supermodel) ->
|
||||||
serializedLevel = @level.serialize @supermodel, @session, @otherSession
|
serializedLevel = @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false}
|
||||||
@god?.setLevel serializedLevel
|
@god?.setLevel serializedLevel
|
||||||
if @world
|
if @world
|
||||||
@world.loadFromLevel serializedLevel, false
|
@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
|
#at this point, all requisite data is loaded, and sessions are not denormalized
|
||||||
team = @world.teamForPlayer(0)
|
team = @world.teamForPlayer(0)
|
||||||
@loadOpponentTeam(team)
|
@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.setLevelSessionIDs if @otherSession then [@session.id, @otherSession.id] else [@session.id]
|
||||||
@god.setWorldClassMap @world.classMap
|
@god.setWorldClassMap @world.classMap
|
||||||
@setTeam team
|
@setTeam team
|
||||||
|
|
|
@ -128,7 +128,7 @@ module.exports = class PlayLevelView extends RootView
|
||||||
@supermodel.collections = givenSupermodel.collections
|
@supermodel.collections = givenSupermodel.collections
|
||||||
@supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups
|
@supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups
|
||||||
|
|
||||||
serializedLevel = @level.serialize @supermodel, @session, @otherSession
|
serializedLevel = @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false}
|
||||||
@god?.setLevel serializedLevel
|
@god?.setLevel serializedLevel
|
||||||
if @world
|
if @world
|
||||||
@world.loadFromLevel serializedLevel, false
|
@world.loadFromLevel serializedLevel, false
|
||||||
|
@ -244,7 +244,7 @@ module.exports = class PlayLevelView extends RootView
|
||||||
@session.set 'multiplayer', false
|
@session.set 'multiplayer', false
|
||||||
|
|
||||||
setupGod: ->
|
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.setLevelSessionIDs if @otherSession then [@session.id, @otherSession.id] else [@session.id]
|
||||||
@god.setWorldClassMap @world.classMap
|
@god.setWorldClassMap @world.classMap
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,9 @@
|
||||||
ModalView = require 'views/core/ModalView'
|
ModalView = require 'views/core/ModalView'
|
||||||
template = require 'templates/play/level/modal/course-victory-modal'
|
template = require 'templates/play/level/modal/course-victory-modal'
|
||||||
Achievements = require 'collections/Achievements'
|
|
||||||
Level = require 'models/Level'
|
Level = require 'models/Level'
|
||||||
Course = require 'models/Course'
|
Course = require 'models/Course'
|
||||||
ThangType = require 'models/ThangType'
|
|
||||||
ThangTypes = require 'collections/ThangTypes'
|
|
||||||
LevelSessions = require 'collections/LevelSessions'
|
LevelSessions = require 'collections/LevelSessions'
|
||||||
EarnedAchievement = require 'models/EarnedAchievement'
|
|
||||||
LocalMongo = require 'lib/LocalMongo'
|
|
||||||
ProgressView = require './ProgressView'
|
ProgressView = require './ProgressView'
|
||||||
NewItemView = require './NewItemView'
|
|
||||||
Classroom = require 'models/Classroom'
|
Classroom = require 'models/Classroom'
|
||||||
utils = require 'core/utils'
|
utils = require 'core/utils'
|
||||||
|
|
||||||
|
@ -18,7 +12,6 @@ module.exports = class CourseVictoryModal extends ModalView
|
||||||
template: template
|
template: template
|
||||||
closesOnClickOutside: false
|
closesOnClickOutside: false
|
||||||
|
|
||||||
|
|
||||||
initialize: (options) ->
|
initialize: (options) ->
|
||||||
@courseID = options.courseID
|
@courseID = options.courseID
|
||||||
@courseInstanceID = options.courseInstanceID
|
@courseInstanceID = options.courseInstanceID
|
||||||
|
@ -26,20 +19,10 @@ module.exports = class CourseVictoryModal extends ModalView
|
||||||
|
|
||||||
@session = options.session
|
@session = options.session
|
||||||
@level = options.level
|
@level = options.level
|
||||||
@newItems = new ThangTypes()
|
|
||||||
@newHeroes = new ThangTypes()
|
|
||||||
|
|
||||||
if @courseInstanceID
|
if @courseInstanceID
|
||||||
@classroom = new Classroom()
|
@classroom = new Classroom()
|
||||||
@supermodel.trackRequest(@classroom.fetchForCourseInstance(@courseInstanceID))
|
@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'
|
@playSound 'victory'
|
||||||
@nextLevel = new Level()
|
@nextLevel = new Level()
|
||||||
|
@ -68,69 +51,12 @@ module.exports = class CourseVictoryModal extends ModalView
|
||||||
return
|
return
|
||||||
super(arguments...)
|
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: ->
|
onLoaded: ->
|
||||||
super()
|
super()
|
||||||
@views = []
|
@views = []
|
||||||
|
|
||||||
# TODO: Add main victory view
|
@levelSessions?.remove(@session)
|
||||||
# TODO: Add level up view
|
@levelSessions?.add(@session)
|
||||||
# TODO: Add new hero view?
|
|
||||||
|
|
||||||
for newItem in @newItems.models
|
|
||||||
@views.push(new NewItemView({item: newItem}))
|
|
||||||
|
|
||||||
progressView = new ProgressView({
|
progressView = new ProgressView({
|
||||||
level: @level
|
level: @level
|
||||||
nextLevel: @nextLevel
|
nextLevel: @nextLevel
|
||||||
|
|
|
@ -275,8 +275,7 @@ module.exports = class SpellView extends CocoView
|
||||||
e.editor.execCommand 'gotolineend'
|
e.editor.execCommand 'gotolineend'
|
||||||
return true
|
return true
|
||||||
|
|
||||||
if me.level() < 20 or aceConfig.indentGuides
|
# Add visual indent guides
|
||||||
# Add visual ident guides
|
|
||||||
language = @spell.language
|
language = @spell.language
|
||||||
ensureLineStartsBlock = (line) ->
|
ensureLineStartsBlock = (line) ->
|
||||||
return false unless language is "python"
|
return false unless language is "python"
|
||||||
|
|
|
@ -6,51 +6,49 @@
|
||||||
// Usage:
|
// Usage:
|
||||||
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
|
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
|
||||||
|
|
||||||
try {
|
// TODO: Does not handle course prepaid updates on a user
|
||||||
var logDB = new Mongo("localhost").getDB("analytics")
|
// TODO: Does not handle class membership changes
|
||||||
var scriptStartTime = new Date();
|
|
||||||
var analyticsStringCache = {};
|
|
||||||
|
|
||||||
var minClassSize = 12;
|
// TODO: Investigate abrupt trial drop off at 4/1/16. Showed up when fixing coursePrepaid.
|
||||||
var minActiveCount = 6;
|
|
||||||
|
|
||||||
var eventNamePaid = 'Active classes paid';
|
var analyticsDB = new Mongo("localhost").getDB("analytics")
|
||||||
var eventNameTrial = 'Active classes trial';
|
var scriptStartTime = new Date();
|
||||||
var eventNameFree = 'Active classes free';
|
var analyticsStringCache = {};
|
||||||
|
|
||||||
var numDays = 40;
|
var minClassSize = 12;
|
||||||
var daysInMonth = 30;
|
var minActiveCount = 6;
|
||||||
|
|
||||||
var startDay = new Date();
|
var eventNamePaid = 'Active classes paid';
|
||||||
var today = startDay.toISOString().substr(0, 10);
|
var eventNameTrial = 'Active classes trial';
|
||||||
startDay.setUTCDate(startDay.getUTCDate() - numDays);
|
var eventNameFree = 'Active classes free';
|
||||||
startDay = startDay.toISOString().substr(0, 10);
|
|
||||||
|
|
||||||
log("Today is " + today);
|
var numDays = 40;
|
||||||
log("Start day is " + startDay);
|
var daysInMonth = 30;
|
||||||
|
|
||||||
log("Getting active class counts..");
|
var startDay = new Date();
|
||||||
var activeClassCounts = getActiveClassCounts(startDay);
|
var today = startDay.toISOString().substr(0, 10);
|
||||||
// printjson(activeClassCounts);
|
startDay.setUTCDate(startDay.getUTCDate() - numDays);
|
||||||
log("Inserting active class counts..");
|
startDay = startDay.toISOString().substr(0, 10);
|
||||||
for (var event in activeClassCounts) {
|
|
||||||
|
log("Today is " + today);
|
||||||
|
log("Start day is " + startDay);
|
||||||
|
|
||||||
|
log("Getting active class counts..");
|
||||||
|
var activeClassCounts = getActiveClassCounts(startDay);
|
||||||
|
// printjson(activeClassCounts);
|
||||||
|
// log("Inserting active class counts..");
|
||||||
|
for (var event in activeClassCounts) {
|
||||||
for (var day in activeClassCounts[event]) {
|
for (var day in activeClassCounts[event]) {
|
||||||
if (today === day) continue; // Never save data for today because it's incomplete
|
if (today === day) continue; // Never save data for today because it's incomplete
|
||||||
// print(event, day, activeClassCounts[event][day]);
|
// print(event, day, activeClassCounts[event][day]);
|
||||||
insertEventCount(event, day, activeClassCounts[event][day]);
|
insertEventCount(event, day, activeClassCounts[event][day]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Script runtime: " + (new Date() - scriptStartTime));
|
log("Script runtime: " + (new Date() - scriptStartTime));
|
||||||
}
|
|
||||||
catch(err) {
|
|
||||||
log("ERROR: " + err);
|
|
||||||
printjson(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveClassCounts(startDay) {
|
function getActiveClassCounts(startDay) {
|
||||||
// Tally active classes per day, for paid, trial, and free
|
// Tally active classes per day, for paid, trial, and free
|
||||||
// TODO: does not handle class membership changes
|
|
||||||
|
|
||||||
if (!startDay) return {};
|
if (!startDay) return {};
|
||||||
|
|
||||||
|
@ -60,7 +58,7 @@ function getActiveClassCounts(startDay) {
|
||||||
// paid: at least one paid member
|
// paid: at least one paid member
|
||||||
// trial: not paid, at least one trial member
|
// trial: not paid, at least one trial member
|
||||||
// free: not paid, not free trial
|
// free: not paid, not free trial
|
||||||
// user.coursePrepaidID set means access to paid courses
|
// user.coursePrepaidID or user.coursePrepaid set means access to paid courses
|
||||||
// prepaid.properties.trialRequestID means access was via trial
|
// prepaid.properties.trialRequestID means access was via trial
|
||||||
|
|
||||||
// Find classroom users
|
// Find classroom users
|
||||||
|
@ -86,30 +84,46 @@ function getActiveClassCounts(startDay) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// log("DEBUG: Classroom users: " + classroomUserIDs.length);
|
||||||
|
|
||||||
log("Find user types..");
|
log("Find user types..");
|
||||||
var userEventMap = {};
|
var userEventEndDateMap = {};
|
||||||
var prepaidUsersMap = {};
|
var prepaidUsersMap = {};
|
||||||
var prepaidIDs = [];
|
var prepaidIDs = [];
|
||||||
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaidID: 1});
|
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaid: 1, coursePrepaidID: 1});
|
||||||
while (cursor.hasNext()) {
|
while (cursor.hasNext()) {
|
||||||
doc = cursor.next();
|
doc = cursor.next();
|
||||||
|
userEventEndDateMap[doc._id.valueOf()] = {};
|
||||||
|
userEventEndDateMap[doc._id.valueOf()][eventNameFree] = new Date();
|
||||||
|
if (doc.coursePrepaid) {
|
||||||
|
if (!doc.coursePrepaid.endDate) throw new Error("No endDate for new prepaid " + doc._id.valuOf());
|
||||||
|
userEventEndDateMap[doc._id.valueOf()][eventNamePaid] = new Date(doc.coursePrepaid.endDate);
|
||||||
|
if (!prepaidUsersMap[doc.coursePrepaid._id.valueOf()]) prepaidUsersMap[doc.coursePrepaid._id.valueOf()] = [];
|
||||||
|
prepaidUsersMap[doc.coursePrepaid._id.valueOf()].push(doc._id.valueOf());
|
||||||
|
prepaidIDs.push(doc.coursePrepaid._id);
|
||||||
|
}
|
||||||
if (doc.coursePrepaidID) {
|
if (doc.coursePrepaidID) {
|
||||||
userEventMap[doc._id.valueOf()] = eventNamePaid;
|
if (!userEventEndDateMap[doc._id.valueOf()][eventNamePaid]) {
|
||||||
|
userEventEndDateMap[doc._id.valueOf()][eventNamePaid] = new Date();
|
||||||
|
}
|
||||||
if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = [];
|
if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = [];
|
||||||
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
|
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
|
||||||
prepaidIDs.push(doc.coursePrepaidID);
|
prepaidIDs.push(doc.coursePrepaidID);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
userEventMap[doc._id.valueOf()] = eventNameFree;
|
|
||||||
}
|
}
|
||||||
}
|
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {endDate: 1, properties: 1});
|
||||||
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1});
|
|
||||||
while (cursor.hasNext()) {
|
while (cursor.hasNext()) {
|
||||||
doc = cursor.next();
|
doc = cursor.next();
|
||||||
if (doc.properties && doc.properties.trialRequestID) {
|
if (doc.properties && doc.properties.trialRequestID) {
|
||||||
|
var endDate = new Date();
|
||||||
|
if (doc.endDate) {
|
||||||
|
endDate = new Date(doc.endDate);
|
||||||
|
}
|
||||||
|
else if (doc.properties.endDate) {
|
||||||
|
endDate = new Date(doc.properties.endDate);
|
||||||
|
}
|
||||||
for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) {
|
for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) {
|
||||||
userEventMap[prepaidUsersMap[doc._id.valueOf()][i]] = eventNameTrial;
|
userEventEndDateMap[prepaidUsersMap[doc._id.valueOf()][i]][eventNameTrial] = endDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,18 +135,22 @@ function getActiveClassCounts(startDay) {
|
||||||
var endDate = ISODate(startDay + "T00:00:00.000Z");
|
var endDate = ISODate(startDay + "T00:00:00.000Z");
|
||||||
var todayDate = new Date(new Date().toISOString().substring(0, 10));
|
var todayDate = new Date(new Date().toISOString().substring(0, 10));
|
||||||
var startObj = objectIdWithTimestamp(startDate);
|
var startObj = objectIdWithTimestamp(startDate);
|
||||||
|
// Batch size test times: 10k 427005, 5k 361361, 1k 799068, 2k 791521
|
||||||
|
var batchSize = 5000;
|
||||||
|
for (var j = 0; j < classroomUserIDs.length / batchSize + 1; j++) {
|
||||||
|
// log("DEBUG: Fetching classroom events batch " + (j * batchSize) + " " + (j * batchSize + batchSize));
|
||||||
var queryParams = {$and: [
|
var queryParams = {$and: [
|
||||||
{_id: {$gte: startObj}},
|
{_id: {$gte: startObj}},
|
||||||
{user: {$in: classroomUserIDs}},
|
{user: {$in: classroomUserIDs.slice(j * batchSize, j * batchSize + batchSize)}},
|
||||||
{event: 'Started Level'}
|
{event: 'Started Level'}
|
||||||
]};
|
]};
|
||||||
cursor = logDB['log'].find(queryParams, {user: 1});
|
cursor = analyticsDB['log'].find(queryParams, {user: 1});
|
||||||
while (cursor.hasNext()) {
|
while (cursor.hasNext()) {
|
||||||
doc = cursor.next();
|
doc = cursor.next();
|
||||||
if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = [];
|
if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = [];
|
||||||
userPlayedMap[doc.user].push(doc._id.getTimestamp());
|
userPlayedMap[doc.user].push(doc._id.getTimestamp());
|
||||||
}
|
}
|
||||||
// printjson(userPlayedMap);
|
}
|
||||||
|
|
||||||
log("Calculate number of active members per classroom per day per event type..");
|
log("Calculate number of active members per classroom per day per event type..");
|
||||||
var classDayTypeMap = {};
|
var classDayTypeMap = {};
|
||||||
|
@ -159,7 +177,19 @@ function getActiveClassCounts(startDay) {
|
||||||
if (userPlayedMap[member]) {
|
if (userPlayedMap[member]) {
|
||||||
for (var k = 0; k < userPlayedMap[member].length; k++) {
|
for (var k = 0; k < userPlayedMap[member].length; k++) {
|
||||||
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
|
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
|
||||||
classDayTypeMap[classroom][endDay][userEventMap[member]]++;
|
if (userEventEndDateMap[member][eventNameTrial] > endDate) {
|
||||||
|
classDayTypeMap[classroom][endDay][eventNameTrial]++;
|
||||||
|
}
|
||||||
|
else if (userEventEndDateMap[member][eventNamePaid] > endDate) {
|
||||||
|
classDayTypeMap[classroom][endDay][eventNamePaid]++;
|
||||||
|
}
|
||||||
|
else if (userEventEndDateMap[member][eventNameFree] > endDate) {
|
||||||
|
classDayTypeMap[classroom][endDay][eventNameFree]++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
print("ERROR: no event for " + member);
|
||||||
|
printjson(userEventEndDateMap[member]);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
// Usage:
|
// Usage:
|
||||||
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
|
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
|
||||||
|
|
||||||
|
// TODO: classroom paid active users before 4/13/16 not correct
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var logDB = new Mongo("localhost").getDB("analytics")
|
var logDB = new Mongo("localhost").getDB("analytics")
|
||||||
var scriptStartTime = new Date();
|
var scriptStartTime = new Date();
|
||||||
|
@ -129,32 +131,47 @@ function getActiveUserCounts(startDay, endDay, activeUserEvents) {
|
||||||
log("Classroom user count: " + classroomUserObjectIds.length);
|
log("Classroom user count: " + classroomUserObjectIds.length);
|
||||||
|
|
||||||
// Classrooms free/trial/paid
|
// Classrooms free/trial/paid
|
||||||
// Paid user: user.coursePrepaidID set means access to paid courses
|
// Paid user: user.coursePrepaid or user.coursePrepaidID set means access to paid courses
|
||||||
// Trial user: prepaid.properties.trialRequestID means access was via trial
|
// Trial user: prepaid.properties.trialRequestID means access was via trial
|
||||||
// Free: not paid, not trial
|
// Free: not paid, not trial
|
||||||
log("Finding classroom users free/trial/paid status..");
|
log("Finding classroom users free/trial/paid status..");
|
||||||
var classroomUserEventMap = {};
|
var classroomUserEventEndDateMap = {};
|
||||||
var prepaidUsersMap = {};
|
var prepaidUsersMap = {};
|
||||||
var prepaidIDs = [];
|
var prepaidIDs = [];
|
||||||
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaidID: 1});
|
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaid: 1, coursePrepaidID: 1});
|
||||||
while (cursor.hasNext()) {
|
while (cursor.hasNext()) {
|
||||||
doc = cursor.next();
|
doc = cursor.next();
|
||||||
|
classroomUserEventEndDateMap[doc._id.valueOf()] = {};
|
||||||
|
classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom free'] = new Date();
|
||||||
|
if (doc.coursePrepaid) {
|
||||||
|
if (!doc.coursePrepaid.endDate) throw new Error("No endDate for new prepaid " + doc._id.valuOf());
|
||||||
|
classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom paid'] = new Date(doc.coursePrepaid.endDate);
|
||||||
|
if (!prepaidUsersMap[doc.coursePrepaid._id.valueOf()]) prepaidUsersMap[doc.coursePrepaid._id.valueOf()] = [];
|
||||||
|
prepaidUsersMap[doc.coursePrepaid._id.valueOf()].push(doc._id.valueOf());
|
||||||
|
prepaidIDs.push(doc.coursePrepaid._id);
|
||||||
|
}
|
||||||
if (doc.coursePrepaidID) {
|
if (doc.coursePrepaidID) {
|
||||||
classroomUserEventMap[doc._id.valueOf()] = 'DAU classroom paid';
|
if (!classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom paid']) {
|
||||||
|
classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom paid'] = new Date();
|
||||||
|
}
|
||||||
if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = [];
|
if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = [];
|
||||||
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
|
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
|
||||||
prepaidIDs.push(doc.coursePrepaidID);
|
prepaidIDs.push(doc.coursePrepaidID);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
classroomUserEventMap[doc._id.valueOf()] = 'DAU classroom free';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1});
|
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1});
|
||||||
while (cursor.hasNext()) {
|
while (cursor.hasNext()) {
|
||||||
doc = cursor.next();
|
doc = cursor.next();
|
||||||
if (doc.properties && doc.properties.trialRequestID) {
|
if (doc.properties && doc.properties.trialRequestID) {
|
||||||
|
var endDate = new Date();
|
||||||
|
if (doc.endDate) {
|
||||||
|
endDate = new Date(doc.endDate);
|
||||||
|
}
|
||||||
|
else if (doc.properties.endDate) {
|
||||||
|
endDate = new Date(doc.properties.endDate);
|
||||||
|
}
|
||||||
for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) {
|
for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) {
|
||||||
classroomUserEventMap[prepaidUsersMap[doc._id.valueOf()][i]] = 'DAU classroom trial';
|
classroomUserEventEndDateMap[prepaidUsersMap[doc._id.valueOf()][i]]['DAU classroom trial'] = endDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,7 +216,22 @@ function getActiveUserCounts(startDay, endDay, activeUserEvents) {
|
||||||
var userDayEventMap = {}
|
var userDayEventMap = {}
|
||||||
for (day in dayUserActiveMap) {
|
for (day in dayUserActiveMap) {
|
||||||
for (var user in dayUserActiveMap[day]) {
|
for (var user in dayUserActiveMap[day]) {
|
||||||
var event = classroomUserEventMap[user] || (dayCampaignUserPaidMap[day] && dayCampaignUserPaidMap[day][user] ? 'DAU campaign paid' : 'DAU campaign free');
|
var event = null;
|
||||||
|
var endDate = new Date(day + "T00:00:00.000Z");
|
||||||
|
if (classroomUserEventEndDateMap[user]) {
|
||||||
|
if (classroomUserEventEndDateMap[user]['DAU classroom trial'] > endDate) {
|
||||||
|
event = 'DAU classroom trial';
|
||||||
|
}
|
||||||
|
else if (classroomUserEventEndDateMap[user]['DAU classroom paid'] > endDate) {
|
||||||
|
event = 'DAU classroom paid';
|
||||||
|
}
|
||||||
|
else if (classroomUserEventEndDateMap[user]['DAU classroom free'] > endDate) {
|
||||||
|
event = 'DAU classroom free';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!event) {
|
||||||
|
event = dayCampaignUserPaidMap[day] && dayCampaignUserPaidMap[day][user] ? 'DAU campaign paid' : 'DAU campaign free';
|
||||||
|
}
|
||||||
dailyEventNames[event] = true;
|
dailyEventNames[event] = true;
|
||||||
if (!activeUsersCounts[day]) activeUsersCounts[day] = {};
|
if (!activeUsersCounts[day]) activeUsersCounts[day] = {};
|
||||||
if (!activeUsersCounts[day][event]) activeUsersCounts[day][event] = 0;
|
if (!activeUsersCounts[day][event]) activeUsersCounts[day][event] = 0;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Follow up on Close.io leads
|
// Follow up on Close.io leads
|
||||||
|
|
||||||
'use strict';
|
'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>");
|
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();
|
process.exit();
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,8 @@ const demoRequestEmailTemplatesAuto2 = ['tmpl_HJ5zebh1SqC1QydDto05VPUMu4F7i5M35L
|
||||||
|
|
||||||
const scriptStartTime = new Date();
|
const scriptStartTime = new Date();
|
||||||
const closeIoApiKey = process.argv[2];
|
const closeIoApiKey = process.argv[2];
|
||||||
const closeIoMailApiKeys = [process.argv[3], process.argv[4], process.argv[5]]; // Automatic mails sent as API owners
|
const closeIoMailApiKeys = [process.argv[3], process.argv[4], process.argv[5], process.argv[6]]; // Automatic mails sent as API owners
|
||||||
const mongoConnUrl = process.argv[6];
|
const mongoConnUrl = process.argv[7];
|
||||||
const MongoClient = require('mongodb').MongoClient;
|
const MongoClient = require('mongodb').MongoClient;
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const request = require('request');
|
const request = require('request');
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// Upsert new lead data into Close.io
|
// Upsert new lead data into Close.io
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
if (process.argv.length !== 9) {
|
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 EU mail API key> <Intercom 'App ID:API key'> <mongo connection Url>");
|
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();
|
process.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,18 +64,22 @@ const closeIoMailApiKeys = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiKey: process.argv[4],
|
apiKey: process.argv[4],
|
||||||
weight: .25
|
weight: .20
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiKey: process.argv[5],
|
apiKey: process.argv[5],
|
||||||
weight: .05
|
weight: .05
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
apiKey: process.argv[6],
|
||||||
|
weight: .05
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const closeIoEuMailApiKey = process.argv[6];
|
const closeIoEuMailApiKey = process.argv[7];
|
||||||
const intercomAppIdApiKey = process.argv[7];
|
const intercomAppIdApiKey = process.argv[8];
|
||||||
const intercomAppId = intercomAppIdApiKey.split(':')[0];
|
const intercomAppId = intercomAppIdApiKey.split(':')[0];
|
||||||
const intercomApiKey = intercomAppIdApiKey.split(':')[1];
|
const intercomApiKey = intercomAppIdApiKey.split(':')[1];
|
||||||
const mongoConnUrl = process.argv[8];
|
const mongoConnUrl = process.argv[9];
|
||||||
const MongoClient = require('mongodb').MongoClient;
|
const MongoClient = require('mongodb').MongoClient;
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const countryData = require('country-data');
|
const countryData = require('country-data');
|
||||||
|
|
|
@ -142,13 +142,13 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
||||||
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
|
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
return @sendNotFoundError(res) unless courseInstance
|
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 @sendDatabaseError(res, err) if err
|
||||||
return @sendNotFoundError(res) unless course
|
return @sendNotFoundError(res) unless classroom
|
||||||
Campaign.findById course.get('campaignID'), (err, campaign) =>
|
levelIDs = []
|
||||||
return @sendDatabaseError(res, err) if err
|
for course in classroom.get('courses') when course._id.equals(courseInstance.get('courseID'))
|
||||||
return @sendNotFoundError(res) unless campaign
|
for level in course.levels when not _.contains(level.type, 'ladder')
|
||||||
levelIDs = (levelID for levelID, level of campaign.get('levels') when not _.contains(level.type, 'ladder'))
|
levelIDs.push(level.original + "")
|
||||||
query = {$and: [{creator: req.user.id}, {'level.original': {$in: levelIDs}}]}
|
query = {$and: [{creator: req.user.id}, {'level.original': {$in: levelIDs}}]}
|
||||||
cursor = LevelSession.find(query)
|
cursor = LevelSession.find(query)
|
||||||
cursor = cursor.select(req.query.project) if req.query.project
|
cursor = cursor.select(req.query.project) if req.query.project
|
||||||
|
|
|
@ -39,6 +39,7 @@ LevelHandler = class LevelHandler extends Handler
|
||||||
'requiresSubscription'
|
'requiresSubscription'
|
||||||
'adventurer'
|
'adventurer'
|
||||||
'practice'
|
'practice'
|
||||||
|
'shareable'
|
||||||
'adminOnly'
|
'adminOnly'
|
||||||
'disableSpaces'
|
'disableSpaces'
|
||||||
'hidesSubmitUntilRun'
|
'hidesSubmitUntilRun'
|
||||||
|
|
|
@ -154,7 +154,7 @@ module.exports =
|
||||||
levels = _.sortBy(levels, 'campaignIndex')
|
levels = _.sortBy(levels, 'campaignIndex')
|
||||||
for level in levels
|
for level in levels
|
||||||
levelData = { original: mongoose.Types.ObjectId(level.original) }
|
levelData = { original: mongoose.Types.ObjectId(level.original) }
|
||||||
_.extend(levelData, _.pick(level, 'type', 'slug', 'name', 'practice', 'practiceThresholdMinutes'))
|
_.extend(levelData, _.pick(level, 'type', 'slug', 'name', 'practice', 'practiceThresholdMinutes', 'shareable'))
|
||||||
courseData.levels.push(levelData)
|
courseData.levels.push(levelData)
|
||||||
coursesData.push(courseData)
|
coursesData.push(courseData)
|
||||||
classroom.set('courses', coursesData)
|
classroom.set('courses', coursesData)
|
||||||
|
|
|
@ -13,6 +13,8 @@ User = require '../models/User'
|
||||||
Classroom = require '../models/Classroom'
|
Classroom = require '../models/Classroom'
|
||||||
facebook = require '../lib/facebook'
|
facebook = require '../lib/facebook'
|
||||||
gplus = require '../lib/gplus'
|
gplus = require '../lib/gplus'
|
||||||
|
TrialRequest = require '../models/TrialRequest'
|
||||||
|
log = require 'winston'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
fetchByGPlusID: wrap (req, res, next) ->
|
fetchByGPlusID: wrap (req, res, next) ->
|
||||||
|
@ -133,16 +135,7 @@ module.exports =
|
||||||
throw new errors.Conflict('Email already taken')
|
throw new errors.Conflict('Email already taken')
|
||||||
|
|
||||||
req.user.set({ password, email, anonymous: false })
|
req.user.set({ password, email, anonymous: false })
|
||||||
try
|
yield module.exports.finishSignup(req, res)
|
||||||
yield req.user.save()
|
|
||||||
catch e
|
|
||||||
if e.code is 11000 # Duplicate key error
|
|
||||||
throw new errors.Conflict('Email already taken')
|
|
||||||
else
|
|
||||||
throw e
|
|
||||||
|
|
||||||
req.user.sendWelcomeEmail()
|
|
||||||
res.status(200).send(req.user.toObject({req: req}))
|
|
||||||
|
|
||||||
signupWithFacebook: wrap (req, res) ->
|
signupWithFacebook: wrap (req, res) ->
|
||||||
unless req.user.isAnonymous()
|
unless req.user.isAnonymous()
|
||||||
|
@ -159,16 +152,7 @@ module.exports =
|
||||||
throw new errors.UnprocessableEntity('Invalid facebookAccessToken')
|
throw new errors.UnprocessableEntity('Invalid facebookAccessToken')
|
||||||
|
|
||||||
req.user.set({ facebookID, email, anonymous: false })
|
req.user.set({ facebookID, email, anonymous: false })
|
||||||
try
|
yield module.exports.finishSignup(req, res)
|
||||||
yield req.user.save()
|
|
||||||
catch e
|
|
||||||
if e.code is 11000 # Duplicate key error
|
|
||||||
throw new errors.Conflict('Email already taken')
|
|
||||||
else
|
|
||||||
throw e
|
|
||||||
|
|
||||||
req.user.sendWelcomeEmail()
|
|
||||||
res.status(200).send(req.user.toObject({req: req}))
|
|
||||||
|
|
||||||
signupWithGPlus: wrap (req, res) ->
|
signupWithGPlus: wrap (req, res) ->
|
||||||
unless req.user.isAnonymous()
|
unless req.user.isAnonymous()
|
||||||
|
@ -186,6 +170,9 @@ module.exports =
|
||||||
throw new errors.UnprocessableEntity('Invalid gplusAccessToken')
|
throw new errors.UnprocessableEntity('Invalid gplusAccessToken')
|
||||||
|
|
||||||
req.user.set({ gplusID, email, anonymous: false })
|
req.user.set({ gplusID, email, anonymous: false })
|
||||||
|
yield module.exports.finishSignup(req, res)
|
||||||
|
|
||||||
|
finishSignup: co.wrap (req, res) ->
|
||||||
try
|
try
|
||||||
yield req.user.save()
|
yield req.user.save()
|
||||||
catch e
|
catch e
|
||||||
|
@ -194,5 +181,21 @@ module.exports =
|
||||||
else
|
else
|
||||||
throw e
|
throw e
|
||||||
|
|
||||||
|
# post-successful account signup tasks
|
||||||
|
|
||||||
req.user.sendWelcomeEmail()
|
req.user.sendWelcomeEmail()
|
||||||
|
|
||||||
|
# If person A creates a trial request without creating an account, then person B uses that computer
|
||||||
|
# to create an account, then person A's trial request is associated with person B's account. To prevent
|
||||||
|
# this, we check that the signup email matches the trial request email, for every signup. If they do
|
||||||
|
# not match, the trial request applicant field is cleared, disassociating the trial request from this
|
||||||
|
# account.
|
||||||
|
trialRequest = yield TrialRequest.findOne({applicant: req.user._id})
|
||||||
|
if trialRequest
|
||||||
|
email = trialRequest.get('properties')?.email or ''
|
||||||
|
emailLower = email.toLowerCase()
|
||||||
|
if emailLower and emailLower isnt req.user.get('emailLower')
|
||||||
|
log.warn('User submitted trial request and created account with different emails. Disassociating trial request.')
|
||||||
|
yield trialRequest.update({$unset: {applicant: ''}})
|
||||||
|
|
||||||
res.status(200).send(req.user.toObject({req: req}))
|
res.status(200).send(req.user.toObject({req: req}))
|
||||||
|
|
|
@ -31,6 +31,6 @@ AnalyticsLogEventSchema.statics.logEvent = (user, event, properties={}) ->
|
||||||
unless config.proxy
|
unless config.proxy
|
||||||
analyticsMongoose = mongoose.createConnection()
|
analyticsMongoose = mongoose.createConnection()
|
||||||
analyticsMongoose.open "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}", (error) ->
|
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)
|
module.exports = AnalyticsLogEvent = analyticsMongoose.model('analytics.log.event', AnalyticsLogEventSchema, config.mongo.analytics_collection)
|
||||||
|
|
|
@ -106,4 +106,32 @@ LevelSessionSchema.set('toObject', {
|
||||||
return ret
|
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
|
||||||
|
|
|
@ -26,6 +26,11 @@ config.mongo =
|
||||||
mongoose_tokyo_replica_string: process.env.COCO_MONGO_MONGOOSE_TOKYO_REPLICA_STRING or ''
|
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 ''
|
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
|
if config.tokyo or config.saoPaulo
|
||||||
|
|
|
@ -3,6 +3,7 @@ utils = require '../utils'
|
||||||
urlUser = '/db/user'
|
urlUser = '/db/user'
|
||||||
User = require '../../../server/models/User'
|
User = require '../../../server/models/User'
|
||||||
Classroom = require '../../../server/models/Classroom'
|
Classroom = require '../../../server/models/Classroom'
|
||||||
|
TrialRequest = require '../../../server/models/TrialRequest'
|
||||||
Prepaid = require '../../../server/models/Prepaid'
|
Prepaid = require '../../../server/models/Prepaid'
|
||||||
request = require '../request'
|
request = require '../request'
|
||||||
facebook = require '../../../server/lib/facebook'
|
facebook = require '../../../server/lib/facebook'
|
||||||
|
@ -707,6 +708,32 @@ describe 'POST /db/user/:handle/signup-with-password', ->
|
||||||
expect(res.statusCode).toBe(409)
|
expect(res.statusCode).toBe(409)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
it 'disassociates the user from their trial request if the trial request email and signup email do not match', utils.wrap (done) ->
|
||||||
|
user = yield utils.becomeAnonymous()
|
||||||
|
trialRequest = yield utils.makeTrialRequest({ properties: { email: 'one@email.com' } })
|
||||||
|
expect(trialRequest.get('applicant').equals(user._id)).toBe(true)
|
||||||
|
url = getURL("/db/user/#{user.id}/signup-with-password")
|
||||||
|
email = 'two@email.com'
|
||||||
|
json = { email, password: '12345' }
|
||||||
|
[res, body] = yield request.postAsync({url, json})
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
trialRequest = yield TrialRequest.findById(trialRequest.id)
|
||||||
|
expect(trialRequest.get('applicant')).toBeUndefined()
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'does NOT disassociate the user from their trial request if the trial request email and signup email DO match', utils.wrap (done) ->
|
||||||
|
user = yield utils.becomeAnonymous()
|
||||||
|
trialRequest = yield utils.makeTrialRequest({ properties: { email: 'one@email.com' } })
|
||||||
|
expect(trialRequest.get('applicant').equals(user._id)).toBe(true)
|
||||||
|
url = getURL("/db/user/#{user.id}/signup-with-password")
|
||||||
|
email = 'one@email.com'
|
||||||
|
json = { email, password: '12345' }
|
||||||
|
[res, body] = yield request.postAsync({url, json})
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
trialRequest = yield TrialRequest.findById(trialRequest.id)
|
||||||
|
expect(trialRequest.get('applicant').equals(user._id)).toBe(true)
|
||||||
|
done()
|
||||||
|
|
||||||
|
|
||||||
describe 'POST /db/user/:handle/signup-with-facebook', ->
|
describe 'POST /db/user/:handle/signup-with-facebook', ->
|
||||||
facebookID = '12345'
|
facebookID = '12345'
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
Course = require 'models/Course'
|
Course = require 'models/Course'
|
||||||
Level = require 'models/Level'
|
Level = require 'models/Level'
|
||||||
LevelSession = require 'models/LevelSession'
|
LevelSession = require 'models/LevelSession'
|
||||||
Achievements = require 'collections/Achievements'
|
|
||||||
CourseVictoryModal = require 'views/play/level/modal/CourseVictoryModal'
|
CourseVictoryModal = require 'views/play/level/modal/CourseVictoryModal'
|
||||||
NewItemView = require 'views/play/level/modal/NewItemView'
|
|
||||||
ProgressView = require 'views/play/level/modal/ProgressView'
|
ProgressView = require 'views/play/level/modal/ProgressView'
|
||||||
factories = require 'test/app/factories'
|
factories = require 'test/app/factories'
|
||||||
|
|
||||||
|
@ -21,7 +19,6 @@ describe 'CourseVictoryModal', ->
|
||||||
course: factories.makeCourse()
|
course: factories.makeCourse()
|
||||||
level: level
|
level: level
|
||||||
session: factories.makeLevelSession({ state: { complete: true } }, { level })
|
session: factories.makeLevelSession({ state: { complete: true } }, { level })
|
||||||
achievements: new Achievements([factories.makeLevelCompleteAchievement({}, {level: level})])
|
|
||||||
nextLevel: factories.makeLevel()
|
nextLevel: factories.makeLevel()
|
||||||
courseInstanceID: courseInstance.id
|
courseInstanceID: courseInstance.id
|
||||||
courseID: course.id
|
courseID: course.id
|
||||||
|
@ -31,11 +28,6 @@ describe 'CourseVictoryModal', ->
|
||||||
|
|
||||||
handleRequests = (modal) ->
|
handleRequests = (modal) ->
|
||||||
requests = jasmine.Ajax.requests.all()
|
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.levelSessions.fakeRequests[0].respondWith({ status: 200, responseText: '[]' })
|
||||||
modal.classroom.fakeRequests[0].respondWith({
|
modal.classroom.fakeRequests[0].respondWith({
|
||||||
status: 200, responseText: factories.makeClassroom().stringify()
|
status: 200, responseText: factories.makeClassroom().stringify()
|
||||||
|
@ -106,32 +98,3 @@ describe 'CourseVictoryModal', ->
|
||||||
expect(application.router.navigate).toHaveBeenCalled()
|
expect(application.router.navigate).toHaveBeenCalled()
|
||||||
|
|
||||||
it '(demo)', -> jasmine.demoModal(modal)
|
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)
|
|
||||||
|
|
Loading…
Reference in a new issue