Merge branch 'master' into web-dev-levels

This commit is contained in:
Nick Winter 2016-07-14 08:26:27 -07:00
commit 87ed53b24b
38 changed files with 499 additions and 470 deletions

View file

@ -56,6 +56,7 @@ so we can accept your pull requests. It is easy.
![Josh Callebaut](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Josh%20Callebaut/josh_callebaut_100.png "Josh Callebaut") ![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")

View file

@ -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"

View file

@ -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

View file

@ -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}

View file

@ -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

View file

@ -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']

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -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

View file

@ -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,14 +381,17 @@ 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
for point, i in points if points[points.length - 1].day.localeCompare(days[0]) < 0
if point.day.localeCompare(days[0]) >= 0 points = []
points.splice(0, i) else
break for point, i in points
if point.day.localeCompare(days[0]) >= 0
points.splice(0, i)
break
# 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

View file

@ -31,31 +31,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: ->
@ -157,42 +136,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 +173,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 +190,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 +206,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

View file

@ -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({

View file

@ -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']) }))

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -275,78 +275,77 @@ 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" match = /^\s*([^#]+)/.exec(line)
match = /^\s*([^#]+)/.exec(line) return false if not match?
return false if not match? return /:\s*$/.test(match[1])
return /:\s*$/.test(match[1])
@aceSession.addDynamicMarker @aceSession.addDynamicMarker
update: (html, markerLayer, session, config) => update: (html, markerLayer, session, config) =>
Range = ace.require('ace/range').Range Range = ace.require('ace/range').Range
foldWidgets = @aceSession.foldWidgets foldWidgets = @aceSession.foldWidgets
return if not foldWidgets? return if not foldWidgets?
lines = @aceDoc.getAllLines() lines = @aceDoc.getAllLines()
startOfRow = (r) -> startOfRow = (r) ->
str = lines[r] str = lines[r]
ar = str.match(/^\s*/) ar = str.match(/^\s*/)
ar.pop().length ar.pop().length
colors = [{border: '74,144,226', fill: '108,162,226'}, {border: '132,180,235', fill: '230,237,245'}] colors = [{border: '74,144,226', fill: '108,162,226'}, {border: '132,180,235', fill: '230,237,245'}]
for row in [0..@aceSession.getLength()] for row in [0..@aceSession.getLength()]
foldWidgets[row] = @aceSession.getFoldWidget(row) unless foldWidgets[row]? foldWidgets[row] = @aceSession.getFoldWidget(row) unless foldWidgets[row]?
continue unless foldWidgets? and foldWidgets[row] is "start" continue unless foldWidgets? and foldWidgets[row] is "start"
try try
docRange = @aceSession.getFoldWidgetRange(row) docRange = @aceSession.getFoldWidgetRange(row)
catch error catch error
console.warn "Couldn't find fold widget docRange for row #{row}:", error console.warn "Couldn't find fold widget docRange for row #{row}:", error
if not docRange? if not docRange?
guess = startOfRow(row) guess = startOfRow(row)
docRange = new Range(row,guess,row,guess+4) docRange = new Range(row,guess,row,guess+4)
continue unless ensureLineStartsBlock(lines[row]) continue unless ensureLineStartsBlock(lines[row])
if /^\s+$/.test lines[docRange.end.row+1] if /^\s+$/.test lines[docRange.end.row+1]
docRange.end.row += 1 docRange.end.row += 1
xstart = startOfRow(row) xstart = startOfRow(row)
if language is 'python' if language is 'python'
requiredIndent = new RegExp '^' + new Array(Math.floor(xstart / 4 + 1)).join('( |\t)') + '( |\t)+(\\S|\\s*$)' requiredIndent = new RegExp '^' + new Array(Math.floor(xstart / 4 + 1)).join('( |\t)') + '( |\t)+(\\S|\\s*$)'
for crow in [docRange.start.row+1..docRange.end.row] for crow in [docRange.start.row+1..docRange.end.row]
unless requiredIndent.test lines[crow] unless requiredIndent.test lines[crow]
docRange.end.row = crow - 1 docRange.end.row = crow - 1
break break
rstart = @aceSession.documentToScreenPosition docRange.start.row, docRange.start.column rstart = @aceSession.documentToScreenPosition docRange.start.row, docRange.start.column
rend = @aceSession.documentToScreenPosition docRange.end.row, docRange.end.column rend = @aceSession.documentToScreenPosition docRange.end.row, docRange.end.column
range = new Range rstart.row, rstart.column, rend.row, rend.column range = new Range rstart.row, rstart.column, rend.row, rend.column
level = Math.floor(xstart / 4) level = Math.floor(xstart / 4)
color = colors[level % colors.length] color = colors[level % colors.length]
bw = 3 bw = 3
to = markerLayer.$getTop(range.start.row, config) to = markerLayer.$getTop(range.start.row, config)
t = markerLayer.$getTop(range.start.row + 1, config) t = markerLayer.$getTop(range.start.row + 1, config)
h = config.lineHeight * (range.end.row - range.start.row) h = config.lineHeight * (range.end.row - range.start.row)
l = markerLayer.$padding + xstart * config.characterWidth l = markerLayer.$padding + xstart * config.characterWidth
# w = (data.i - data.b) * config.characterWidth # w = (data.i - data.b) * config.characterWidth
w = 4 * config.characterWidth w = 4 * config.characterWidth
fw = config.characterWidth * ( @aceSession.getScreenLastRowColumn(range.start.row) - xstart ) fw = config.characterWidth * ( @aceSession.getScreenLastRowColumn(range.start.row) - xstart )
html.push """ html.push """
<div style= <div style=
"position: absolute; top: #{to}px; left: #{l}px; width: #{fw+bw}px; height: #{config.lineHeight}px; "position: absolute; top: #{to}px; left: #{l}px; width: #{fw+bw}px; height: #{config.lineHeight}px;
border: #{bw}px solid rgba(#{color.border},1); border-left: none;" border: #{bw}px solid rgba(#{color.border},1); border-left: none;"
></div> ></div>
<div style= <div style=
"position: absolute; top: #{t}px; left: #{l}px; width: #{w}px; height: #{h}px; background-color: rgba(#{color.fill},0.5); "position: absolute; top: #{t}px; left: #{l}px; width: #{w}px; height: #{h}px; background-color: rgba(#{color.fill},0.5);
border-right: #{bw}px solid rgba(#{color.border},1); border-bottom: #{bw}px solid rgba(#{color.border},1);" border-right: #{bw}px solid rgba(#{color.border},1); border-bottom: #{bw}px solid rgba(#{color.border},1);"
></div> ></div>
""" """
fillACE: -> fillACE: ->
@ace.setValue @spell.source @ace.setValue @spell.source

View file

@ -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) {
for (var day in activeClassCounts[event]) { log("Today is " + today);
if (today === day) continue; // Never save data for today because it's incomplete log("Start day is " + startDay);
// print(event, day, activeClassCounts[event][day]);
insertEventCount(event, day, activeClassCounts[event][day]); 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]) {
if (today === day) continue; // Never save data for today because it's incomplete
// print(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}}, {properties: 1}); cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {endDate: 1, 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);
var queryParams = {$and: [ // Batch size test times: 10k 427005, 5k 361361, 1k 799068, 2k 791521
{_id: {$gte: startObj}}, var batchSize = 5000;
{user: {$in: classroomUserIDs}}, for (var j = 0; j < classroomUserIDs.length / batchSize + 1; j++) {
{event: 'Started Level'} // log("DEBUG: Fetching classroom events batch " + (j * batchSize) + " " + (j * batchSize + batchSize));
]}; var queryParams = {$and: [
cursor = logDB['log'].find(queryParams, {user: 1}); {_id: {$gte: startObj}},
while (cursor.hasNext()) { {user: {$in: classroomUserIDs.slice(j * batchSize, j * batchSize + batchSize)}},
doc = cursor.next(); {event: 'Started Level'}
if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = []; ]};
userPlayedMap[doc.user].push(doc._id.getTimestamp()); cursor = analyticsDB['log'].find(queryParams, {user: 1});
while (cursor.hasNext()) {
doc = cursor.next();
if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = [];
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;
} }
} }

View file

@ -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;

View file

@ -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');

View file

@ -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');

View file

@ -142,20 +142,20 @@ 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
cursor.exec (err, documents) => cursor.exec (err, documents) =>
return @sendDatabaseError(res, err) if err? return @sendDatabaseError(res, err) if err?
cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents) cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
@sendSuccess(res, cleandocs) @sendSuccess(res, cleandocs)
getMembersAPI: (req, res, courseInstanceID) -> getMembersAPI: (req, res, courseInstanceID) ->
return @sendUnauthorizedError(res) if not req.user? return @sendUnauthorizedError(res) if not req.user?

View file

@ -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}))

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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)