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")
![Michael Schmatz](http://codecombat.com/images/pages/about/michael_small.png "Michael Schmatz")
![Josh Lee](http://codecombat.com/images/pages/about/josh_small.png "Josh Lee")
![Dan TDM](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Dan_TDM/dan_tdm_100.png "Dan TDM")
![Alex Cotsarelis](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Cotsarelis/alex_100.png "Alex Cotsarelis")
![Alex Crooks](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Crooks/alex_100.png "Alex Crooks")
![Alexandru Caciulescu](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alexandru%20Caciulescu/alexandru_100.png "Alexandru Caciulescu")

View file

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

View file

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

View file

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

View file

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

View file

@ -267,7 +267,7 @@ LevelSchema = c.object {
victory: {}
type: 'hero'
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}
]
concepts: ['basic_syntax']

View file

@ -15,6 +15,16 @@
.form-group
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
// Undo previous opacity-toggling hover behavior

View file

@ -1,8 +1,16 @@
@import "app/styles/style-flat-variables"
#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
margin-top: 15px
display: flex
.path-card ~ .path-card
@ -13,7 +21,8 @@
flex-direction: column
justify-content: space-between
width: 235px
min-height: 340px
height: 340px
max-height: 42vh
border-style: solid
border-width: thin
border-radius: 5px
@ -35,6 +44,7 @@
align-items: center
justify-content: center
height: 50px
max-height: 5vh
color: white
font-weight: bold
text-align: center
@ -43,7 +53,8 @@
flex-grow: 1
display: flex
flex-direction: column
margin: 50px 20px 0
justify-content: center
margin: 0 20px
ul
align-self: center
@ -55,14 +66,26 @@
left: -5px
.card-footer
margin: 20px
margin: 0 20px 20px
min-height: 62px
display: flex
flex-direction: column
justify-content: flex-end
.individual-section
margin-top: 50px
display: flex
flex-grow: 1
flex-direction: column
align-items: center
justify-content: center
max-width: 425px
.individual-title
font-weight: bold
.individual-description
margin: 0
flex-grow: 0.2
.text-h6
color: white

View file

@ -17,6 +17,7 @@
display: flex
flex-direction: column
height: 850px
max-height: 90vh
width: 850px
text-align: center
padding: 0
@ -44,6 +45,7 @@
align-items: center
justify-content: flex-end
height: 100px
max-height: 10.5vh
padding: 0
background-color: $navy

View file

@ -18,7 +18,7 @@ block content
.col-md-5.big-stat.classroom-active-users
div.description Classroom Monthly Active Users
if view.activeUsers.length > 0
- var classroomBigMAU = 0;
- var classroomBigMAU = 0;
each count, event in view.activeUsers[0].events
if event.indexOf('MAU classroom') >= 0
- classroomBigMAU += count;
@ -26,24 +26,24 @@ block content
.col-md-5.big-stat.campaign-active-users
div.description Campaign Monthly Active Users
if view.activeUsers.length > 0
- var campaignBigMAU = 0;
- var campaignBigMAU = 0;
each count, event in view.activeUsers[0].events
if event.indexOf('MAU campaign') >= 0
- campaignBigMAU += count;
div.count= campaignBigMAU
ul.nav.nav-tabs
li.active
li.active
a(data-target="#tab_kpis", data-toggle="tab") KPIs
li
li
a(data-target="#tab_active_classes", data-toggle="tab") Active Classes
li
li
a(data-target="#tab_revenue", data-toggle="tab") Revenue
li
li
a(data-target="#tab_classroom", data-toggle="tab") Classroom
li
li
a(data-target="#tab_campaign", data-toggle="tab") Campaign
li
li
a(data-target="#tab_campaign_vs_classroom", data-toggle="tab") Campaign vs Classroom
.tab-content
@ -165,7 +165,7 @@ block content
.small Paid teacher: at least one paid student in course instance
.small Trial teacher: at least one trial student in course instance, and no paid students
.small Free teacher: no paid students, no trial students
.small Paid status takes precedent over furthest course, so teacher furthest course is furthest course of highest paid status student
.small Paid status takes precedent over furthest course, so teacher furthest course is furthest course of highest paid status student
if view.courseDistributionsRecent
table.table.table-striped.table-condensed
tr
@ -267,9 +267,9 @@ block content
h1 Active Users
if view.activeUsers.length > 0
- var eventNames = [];
each count, event in view.activeUsers[0].events
each event in view.activeUserEventNames
if event.indexOf('classroom') >= 0
- eventNames.push(event)
- eventNames.push(event);
- eventNames.sort(function (a, b) {return a.localeCompare(b);});
table.table.table-striped.table-condensed
tr
@ -293,7 +293,7 @@ block content
each day in view.enrollmentDays
tr
td= day
if view.dayEnrollmentsMap[day]
if view.dayEnrollmentsMap[day]
td= view.dayEnrollmentsMap[day].paidIssued || 0
td= view.dayEnrollmentsMap[day].paidRedeemed || 0
td= view.dayEnrollmentsMap[day].trialIssued || 0
@ -322,9 +322,9 @@ block content
h1 Active Users
if view.activeUsers.length > 0
- var eventNames = [];
each count, event in view.activeUsers[0].events
each event in view.activeUserEventNames
if event.indexOf('campaign') >= 0
- eventNames.push(event)
- eventNames.push(event);
- eventNames.sort(function (a, b) {return a.localeCompare(b);});
table.table.table-striped.table-condensed
tr
@ -346,27 +346,13 @@ block content
h1 Active Users
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
tr
th(style='min-width:85px;') Day
each eventName in eventNames
each eventName in view.activeUserEventNames
th= eventName
each activeUser in view.activeUsers
tr
td= activeUser.day
each eventName in eventNames
each eventName in view.activeUserEventNames
td= activeUser.events[eventName] || 0

View file

@ -1,5 +1,5 @@
.modal-body-content
h4
h4.choose-type-title
span(data-i18n="signup.choose_type")
.path-cards
.path-card.navy

View file

@ -10,7 +10,7 @@
else
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")
.signup-info-box.text-center
if me.get('name')
@ -32,4 +32,4 @@
span(data-i18n="general.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")
for day in _.range(1,32)
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")
- var thisYear = new Date().getFullYear()
for year in _.range(thisYear, thisYear - 100, -1)

View file

@ -182,7 +182,8 @@ mixin studentsTab
th.checkbox-col.select-all.small.text-center
span(data-i18n="teacher.select_all")
.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')
th
+sortButtons
@ -201,7 +202,7 @@ mixin studentRow(student)
tr.student-row.alternating-background
td.checkbox-col.student-checkbox
.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)
td.student-info-col
.student-info

View file

@ -73,6 +73,7 @@ module.exports = class AnalyticsView extends RootView
# Add campaign/classroom DAU 30-day averages and daily totals
campaignDauTotals = []
classroomDauTotals = []
eventMap = {}
for entry in @activeUsers
day = entry.day
campaignDauTotal = 0
@ -82,18 +83,31 @@ module.exports = class AnalyticsView extends RootView
campaignDauTotal += count
else if event.indexOf('DAU classroom') >= 0
classroomDauTotal += count
eventMap[event] = true;
entry.events['DAU campaign total'] = campaignDauTotal
eventMap['DAU campaign total'] = true;
campaignDauTotals.unshift(campaignDauTotal)
campaignDauTotals.pop() while campaignDauTotals.length > 30
if campaignDauTotals.length is 30
entry.events['DAU campaign 30-day average'] = Math.round(_.reduce(campaignDauTotals, (a, b) -> a + b) / 30)
eventMap['DAU campaign 30-day average'] = true;
entry.events['DAU classroom total'] = classroomDauTotal
eventMap['DAU classroom total'] = true;
classroomDauTotals.unshift(classroomDauTotal)
classroomDauTotals.pop() while classroomDauTotals.length > 30
if classroomDauTotals.length is 30
entry.events['DAU classroom 30-day average'] = Math.round(_.reduce(classroomDauTotals, (a, b) -> a + b) / 30)
eventMap['DAU classroom 30-day average'] = true;
@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()
@updateActiveUsersChartData()
@ -134,13 +148,13 @@ module.exports = class AnalyticsView extends RootView
return unless @revenue.length > 0
# Add monthly recurring revenue values
# For each daily group, add up monthly values walking forward through time, and add to revenue groups
monthlyDailyGroupMap = {}
dailyGroupIndexMap = {}
for group, i in @revenueGroups
monthlyDailyGroupMap[group.replace('DRR', 'MRR')] = group
dailyGroupIndexMap[group] = i
dailyGroupIndexMap[group] = i
for monthlyGroup, dailyGroup of monthlyDailyGroupMap
monthlyValues = []
for i in [@revenue.length-1..0]
@ -183,7 +197,7 @@ module.exports = class AnalyticsView extends RootView
@supermodel.addRequestResource({
url: '/db/prepaid/-/courses'
method: 'POST'
data: {project: {maxRedeemers: 1, properties: 1, redeemers: 1}}
data: {project: {endDate: 1, maxRedeemers: 1, properties: 1, redeemers: 1}}
success: (prepaids) =>
paidDayMaxMap = {}
paidDayRedeemedMap = {}
@ -201,14 +215,13 @@ module.exports = class AnalyticsView extends RootView
redeemDay = redeemer.date.substring(0, 10)
trialDayRedeemedMap[redeemDay] ?= 0
trialDayRedeemedMap[redeemDay]++
else
else if not prepaid.endDate? or new Date(prepaid.endDate) > new Date()
paidDayMaxMap[day] ?= 0
paidDayMaxMap[day] += prepaid.maxRedeemers
for redeemer in prepaid.redeemers
redeemDay = redeemer.date.substring(0, 10)
paidDayRedeemedMap[redeemDay] ?= 0
paidDayRedeemedMap[redeemDay]++
@dayEnrollmentsMap = {}
@paidCourseTotalEnrollments = []
for day, count of paidDayMaxMap
@ -218,7 +231,7 @@ module.exports = class AnalyticsView extends RootView
@paidCourseTotalEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@paidCourseRedeemedEnrollments = []
for day, count of paidDayRedeemedMap
@paidCourseRedeemedEnrollments.push({day: day, count: count})
@paidCourseRedeemedEnrollments.push({day: day, count: count})
@dayEnrollmentsMap[day] ?= {paidIssued: 0, paidRedeemed: 0, trialIssued: 0, trialRedeemed: 0}
@dayEnrollmentsMap[day].paidRedeemed += count
@paidCourseRedeemedEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@ -239,7 +252,7 @@ module.exports = class AnalyticsView extends RootView
}, 0).load()
@courses = new CocoCollection([], { url: "/db/course", model: Course})
@courses.comparator = "_id"
@courses.comparator = "_id"
@listenToOnce @courses, 'sync', @onCoursesSync
@supermodel.loadCollection(@courses)
@ -276,7 +289,7 @@ module.exports = class AnalyticsView extends RootView
studentFurthestCourseMap = {}
studentPaidStatusMap = {}
for courseInstance in data.courseInstances
continue if utils.objectIdToDate(courseInstance._id) < startDate
continue if utils.objectIdToDate(courseInstance._id) < startDate
courseID = courseInstance.courseID
teacherID = courseInstance.ownerID
for studentID in courseInstance.members
@ -306,7 +319,7 @@ module.exports = class AnalyticsView extends RootView
# Paid teacher: at least one paid student
# Trial teacher: at least one trial student in course instance, and no paid students
# Free teacher: no paid students, no trial students
# Teacher furthest course is furthest course of highest paid status student
# Teacher furthest course is furthest course of highest paid status student
teacherFurthestCourseMap = {}
teacherPaidStatusMap = {}
for teacher, students of teacherStudentsMap
@ -368,14 +381,17 @@ module.exports = class AnalyticsView extends RootView
# Trim points preceding days
if points.length and days.length and points[0].day.localeCompare(days[0]) < 0
for point, i in points
if point.day.localeCompare(days[0]) >= 0
points.splice(0, i)
break
if points[points.length - 1].day.localeCompare(days[0]) < 0
points = []
else
for point, i in points
if point.day.localeCompare(days[0]) >= 0
points.splice(0, i)
break
# Ensure points for each day
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
points.splice i, 0,
day: day
@ -534,7 +550,7 @@ module.exports = class AnalyticsView extends RootView
day = entry.day
for event, count of entry.events
eventDataMap[event] ?= []
eventDataMap[event].push
eventDataMap[event].push
day: entry.day
value: count
@ -550,7 +566,7 @@ module.exports = class AnalyticsView extends RootView
lines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: showYScale
@ -577,7 +593,7 @@ module.exports = class AnalyticsView extends RootView
day = entry.day
for event, count of entry.events
eventDataMap[event] ?= []
eventDataMap[event].push
eventDataMap[event].push
day: entry.day
value: count
@ -591,7 +607,7 @@ module.exports = class AnalyticsView extends RootView
@campaignVsClassroomMonthlyActiveUsersRecentChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: true
@ -601,7 +617,7 @@ module.exports = class AnalyticsView extends RootView
@campaignVsClassroomMonthlyActiveUsersRecentChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: false
@ -619,7 +635,7 @@ module.exports = class AnalyticsView extends RootView
@campaignVsClassroomMonthlyActiveUsersChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: true
@ -629,7 +645,7 @@ module.exports = class AnalyticsView extends RootView
@campaignVsClassroomMonthlyActiveUsersChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: false
@ -648,16 +664,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = 0
data = []
total = 0
for entry in @paidCourseTotalEnrollments
total += entry.count
data.push
day: entry.day
value: total
value: entry.count
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Total paid enrollments issued'
description: 'Paid enrollments issued'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
@ -666,16 +680,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
total = 0
for entry in @paidCourseRedeemedEnrollments
total += entry.count
data.push
day: entry.day
value: total
value: entry.count
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Total paid enrollments redeemed'
description: 'Paid enrollments redeemed'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
@ -684,16 +696,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
total = 0
for entry in @trialCourseTotalEnrollments
total += entry.count
data.push
day: entry.day
value: total
points = @createLineChartPoints(days, data)
value: entry.count
points = @createLineChartPoints(days, data, true)
@enrollmentsChartLines.push
points: points
description: 'Total trial enrollments issued'
description: 'Trial enrollments issued'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
@ -702,16 +712,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
total = 0
for entry in @trialCourseRedeemedEnrollments
total += entry.count
data.push
day: entry.day
value: total
value: entry.count
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Total trial enrollments redeemed'
description: 'Trial enrollments redeemed'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0

View file

@ -31,31 +31,10 @@ module.exports = class MainAdminView extends RootView
getTitle: -> return $.i18n.t('account_settings.admin')
initialize: ->
@campaigns = new Campaigns()
@courses = new CocoCollection([], { url: "/db/course", model: Course})
if window.amActually
@amActually = new User({_id: window.amActually})
@amActually.fetch()
@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()
onClickStopSpyingButton: ->
@ -157,42 +136,31 @@ module.exports = class MainAdminView extends RootView
@supermodel.addRequestResource('create_prepaid', options, 0).load()
onClickExportProgress: ->
return unless @courseLevels?.length > 0
$('.classroom-progress-csv').prop('disabled', true)
classCode = $('.classroom-progress-class-code').val()
classroom = null
courseLevels = []
sessions = null
users = null
userMap = {}
new Promise((resolve, reject) =>
new Classroom().fetchByCode(classCode, {
success: resolve
error: (model, response, options) => reject(response)
})
)
.then (classroom) =>
new Promise((resolve, reject) =>
new Classroom({ _id: classroom.id }).fetch({
success: resolve
error: (model, response, options) => reject(response)
})
)
.then (classroom) =>
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]) =>
Promise.resolve(new Classroom().fetchByCode(classCode))
.then (model) =>
classroom = new Classroom({ _id: model.data._id })
Promise.resolve(classroom.fetch())
.then (model) =>
for course, index in classroom.get('courses')
for level in course.levels
courseLevels.push
courseIndex: index + 1
levelID: level.original
slug: level.slug
users = new Users()
Promise.resolve($.when(users.fetchForClassroom(classroom)...))
.then (models) =>
userMap[user.id] = user for user in users.models
new Promise((resolve, reject) =>
new LevelSessions().fetchForAllClassroomMembers(classroom, {
success: (models, response, options) =>
resolve(models) if models?.loaded
error: (models, response, options) => reject(response)
})
)
.then (sessions) =>
sessions = new LevelSessions()
Promise.resolve($.when(sessions.fetchForAllClassroomMembers(classroom)...))
.then (models) =>
userLevelPlaytimeMap = {}
for session in sessions.models
continue unless session.get('state')?.complete
@ -205,7 +173,7 @@ module.exports = class MainAdminView extends RootView
userPlaytimes = []
for userID, user of userMap
playtimes = [user.get('name') ? 'Anonymous']
for level in @courseLevels
for level in courseLevels
if userLevelPlaytimeMap[userID]?[level.levelID]?
rawSeconds = parseInt(userLevelPlaytimeMap[userID][level.levelID])
hours = Math.floor(rawSeconds / 60 / 60)
@ -222,7 +190,7 @@ module.exports = class MainAdminView extends RootView
columnLabels = "Username"
currentLevel = 1
lastCourseIndex = 1
for level in @courseLevels
for level in courseLevels
unless level.courseIndex is lastCourseIndex
currentLevel = 1
lastCourseIndex = level.courseIndex
@ -238,3 +206,4 @@ module.exports = class MainAdminView extends RootView
.catch (error) ->
$('.classroom-progress-csv').prop('disabled', false)
console.error error
throw error

View file

@ -28,6 +28,7 @@ module.exports = class BasicInfoView extends CocoView
events:
'change input[name="email"]': 'onChangeEmail'
'change input[name="name"]': 'onChangeName'
'change input[name="password"]': 'onChangePassword'
'click .back-button': 'onClickBackButton'
'submit form': 'onSubmitForm'
'click .use-suggested-name-link': 'onClickUseSuggestedNameLink'
@ -50,8 +51,15 @@ module.exports = class BasicInfoView extends CocoView
@listenTo @state, 'change:error', -> @renderSelectors('.error-area')
@listenTo @signupState, 'change:facebookEnabled', -> @renderSelectors('.auth-network-logins')
@listenTo @signupState, 'change:gplusEnabled', -> @renderSelectors('.auth-network-logins')
# 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: ->
onChangeEmail: (e) ->
@updateAuthModalInitialValues { email: @$(e.currentTarget).val() }
@checkEmail()
checkEmail: ->
@ -85,7 +93,8 @@ module.exports = class BasicInfoView extends CocoView
})
return @state.get('checkEmailPromise')
onChangeName: ->
onChangeName: (e) ->
@updateAuthModalInitialValues { name: @$(e.currentTarget).val() }
@checkName()
checkName: ->
@ -120,7 +129,10 @@ module.exports = class BasicInfoView extends CocoView
)
})
return @state.get('checkNamePromise')
return @state.get('checkNamePromise')
onChangePassword: (e) ->
@updateAuthModalInitialValues { password: @$(e.currentTarget).val() }
checkBasicInfo: (data) ->
# TODO: Move this to somewhere appropriate

View file

@ -62,6 +62,7 @@ module.exports = class CreateAccountModal extends ModalView
gplusEnabled: application.gplusHandler.apiLoaded
classCode
birthday: new Date('') # so that birthday.getTime() is NaN
authModalInitialValues: {}
}
{ startOnPath } = options
@ -112,5 +113,4 @@ module.exports = class CreateAccountModal extends ModalView
document.location.reload()
onClickLoginLink: ->
# TODO: Make sure the right information makes its way into the state.
@openModalView(new AuthModal({ initialValues: @signupState.pick(['email', 'name', 'password']) }))
@openModalView(new AuthModal({ initialValues: @signupState.get('authModalInitialValues') }))

View file

@ -11,7 +11,7 @@ module.exports = class SegmentCheckView extends CocoView
events:
'click .back-to-account-type': -> @trigger 'nav-back'
'input .class-code-input': 'onInputClassCode'
'input .birthday-form-group': 'onInputBirthday'
'change .birthday-form-group': 'onInputBirthday'
'submit form.segment-check': 'onSubmitSegmentCheck'
'click .individual-path-button': -> @trigger 'choose-path', 'individual'

View file

@ -56,6 +56,7 @@ module.exports = class TeacherClassView extends RootView
assigningToNobody: false
assigningToUnenrolled: false
selectedCourse: undefined
checkboxStates: {}
classStats:
averagePlaytime: ""
totalPlaytime: ""
@ -145,6 +146,10 @@ module.exports = class TeacherClassView extends RootView
classStats = @calculateClassStats()
@state.set classStats: classStats if classStats
@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', ->
@state.set students: @students
@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()) }
getSelectedStudentIDs: ->
@$('.student-row .checkbox-flat input:checked').map (index, checkbox) ->
$(checkbox).data('student-id')
Object.keys(_.pick @state.get('checkboxStates'), (checked) -> checked)
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']
onClickBulkEnroll: ->
userIDs = @getSelectedStudentIDs().toArray()
userIDs = @getSelectedStudentIDs()
selectedUsers = new Users(@students.get(userID) for userID in userIDs)
@enrollStudents(selectedUsers)
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: ->
courseID = @$('.bulk-course-select').val()
selectedIDs = @getSelectedStudentIDs()
members = selectedIDs.filter((index, userID) =>
members = selectedIDs.filter (userID) =>
user = @students.get(userID)
user.isEnrolled()
).toArray()
assigningToUnenrolled = _.any selectedIDs, (userID) =>
not @students.get(userID).isEnrolled()
assigningToNobody = selectedIDs.length is 0
@ -417,23 +420,22 @@ module.exports = class TeacherClassView extends RootView
onClickSelectAll: (e) ->
e.preventDefault()
checkboxes = @$('.student-checkbox input')
if _.all(checkboxes, 'checked')
@$('.select-all input').prop('checked', false)
checkboxes.prop('checked', false)
checkboxStates = _.clone @state.get('checkboxStates')
if _.all(checkboxStates)
for studentID of checkboxStates
checkboxStates[studentID] = false
else
@$('.select-all input').prop('checked', true)
checkboxes.prop('checked', true)
null
for studentID of checkboxStates
checkboxStates[studentID] = true
@state.set { checkboxStates }
onClickStudentCheckbox: (e) ->
e.preventDefault()
# $(e.target).$()
checkbox = $(e.currentTarget).find('input')
checkbox.prop('checked', not checkbox.prop('checked'))
# checkboxes.prop('checked', false)
checkboxes = @$('.student-checkbox input')
@$('.select-all input').prop('checked', _.all(checkboxes, 'checked'))
studentID = checkbox.data('student-id')
checkboxStates = _.clone @state.get('checkboxStates')
checkboxStates[studentID] = not checkboxStates[studentID]
@state.set { checkboxStates }
calculateClassStats: ->
return {} unless @classroom.sessions?.loaded and @students.loaded

View file

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

View file

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

View file

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

View file

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

View file

@ -128,7 +128,7 @@ module.exports = class PlayLevelView extends RootView
@supermodel.collections = givenSupermodel.collections
@supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups
serializedLevel = @level.serialize @supermodel, @session, @otherSession
serializedLevel = @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false}
@god?.setLevel serializedLevel
if @world
@world.loadFromLevel serializedLevel, false
@ -244,7 +244,7 @@ module.exports = class PlayLevelView extends RootView
@session.set 'multiplayer', false
setupGod: ->
@god.setLevel @level.serialize @supermodel, @session, @otherSession
@god.setLevel @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false}
@god.setLevelSessionIDs if @otherSession then [@session.id, @otherSession.id] else [@session.id]
@god.setWorldClassMap @world.classMap

View file

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

View file

@ -275,78 +275,77 @@ module.exports = class SpellView extends CocoView
e.editor.execCommand 'gotolineend'
return true
if me.level() < 20 or aceConfig.indentGuides
# Add visual ident guides
language = @spell.language
ensureLineStartsBlock = (line) ->
return false unless language is "python"
match = /^\s*([^#]+)/.exec(line)
return false if not match?
return /:\s*$/.test(match[1])
# Add visual indent guides
language = @spell.language
ensureLineStartsBlock = (line) ->
return false unless language is "python"
match = /^\s*([^#]+)/.exec(line)
return false if not match?
return /:\s*$/.test(match[1])
@aceSession.addDynamicMarker
update: (html, markerLayer, session, config) =>
Range = ace.require('ace/range').Range
@aceSession.addDynamicMarker
update: (html, markerLayer, session, config) =>
Range = ace.require('ace/range').Range
foldWidgets = @aceSession.foldWidgets
return if not foldWidgets?
foldWidgets = @aceSession.foldWidgets
return if not foldWidgets?
lines = @aceDoc.getAllLines()
startOfRow = (r) ->
str = lines[r]
ar = str.match(/^\s*/)
ar.pop().length
lines = @aceDoc.getAllLines()
startOfRow = (r) ->
str = lines[r]
ar = str.match(/^\s*/)
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()]
foldWidgets[row] = @aceSession.getFoldWidget(row) unless foldWidgets[row]?
continue unless foldWidgets? and foldWidgets[row] is "start"
try
docRange = @aceSession.getFoldWidgetRange(row)
catch error
console.warn "Couldn't find fold widget docRange for row #{row}:", error
if not docRange?
guess = startOfRow(row)
docRange = new Range(row,guess,row,guess+4)
for row in [0..@aceSession.getLength()]
foldWidgets[row] = @aceSession.getFoldWidget(row) unless foldWidgets[row]?
continue unless foldWidgets? and foldWidgets[row] is "start"
try
docRange = @aceSession.getFoldWidgetRange(row)
catch error
console.warn "Couldn't find fold widget docRange for row #{row}:", error
if not docRange?
guess = startOfRow(row)
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]
docRange.end.row += 1
if /^\s+$/.test lines[docRange.end.row+1]
docRange.end.row += 1
xstart = startOfRow(row)
if language is 'python'
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]
unless requiredIndent.test lines[crow]
docRange.end.row = crow - 1
break
xstart = startOfRow(row)
if language is 'python'
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]
unless requiredIndent.test lines[crow]
docRange.end.row = crow - 1
break
rstart = @aceSession.documentToScreenPosition docRange.start.row, docRange.start.column
rend = @aceSession.documentToScreenPosition docRange.end.row, docRange.end.column
range = new Range rstart.row, rstart.column, rend.row, rend.column
level = Math.floor(xstart / 4)
color = colors[level % colors.length]
bw = 3
to = markerLayer.$getTop(range.start.row, config)
t = markerLayer.$getTop(range.start.row + 1, config)
h = config.lineHeight * (range.end.row - range.start.row)
l = markerLayer.$padding + xstart * config.characterWidth
# w = (data.i - data.b) * config.characterWidth
w = 4 * config.characterWidth
fw = config.characterWidth * ( @aceSession.getScreenLastRowColumn(range.start.row) - xstart )
rstart = @aceSession.documentToScreenPosition docRange.start.row, docRange.start.column
rend = @aceSession.documentToScreenPosition docRange.end.row, docRange.end.column
range = new Range rstart.row, rstart.column, rend.row, rend.column
level = Math.floor(xstart / 4)
color = colors[level % colors.length]
bw = 3
to = markerLayer.$getTop(range.start.row, config)
t = markerLayer.$getTop(range.start.row + 1, config)
h = config.lineHeight * (range.end.row - range.start.row)
l = markerLayer.$padding + xstart * config.characterWidth
# w = (data.i - data.b) * config.characterWidth
w = 4 * config.characterWidth
fw = config.characterWidth * ( @aceSession.getScreenLastRowColumn(range.start.row) - xstart )
html.push """
<div style=
"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;"
></div>
<div style=
"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);"
></div>
"""
html.push """
<div style=
"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;"
></div>
<div style=
"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);"
></div>
"""
fillACE: ->
@ace.setValue @spell.source

View file

@ -6,51 +6,49 @@
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
try {
var logDB = new Mongo("localhost").getDB("analytics")
var scriptStartTime = new Date();
var analyticsStringCache = {};
// TODO: Does not handle course prepaid updates on a user
// TODO: Does not handle class membership changes
var minClassSize = 12;
var minActiveCount = 6;
// TODO: Investigate abrupt trial drop off at 4/1/16. Showed up when fixing coursePrepaid.
var eventNamePaid = 'Active classes paid';
var eventNameTrial = 'Active classes trial';
var eventNameFree = 'Active classes free';
var analyticsDB = new Mongo("localhost").getDB("analytics")
var scriptStartTime = new Date();
var analyticsStringCache = {};
var numDays = 40;
var daysInMonth = 30;
var minClassSize = 12;
var minActiveCount = 6;
var startDay = new Date();
var today = startDay.toISOString().substr(0, 10);
startDay.setUTCDate(startDay.getUTCDate() - numDays);
startDay = startDay.toISOString().substr(0, 10);
var eventNamePaid = 'Active classes paid';
var eventNameTrial = 'Active classes trial';
var eventNameFree = 'Active classes free';
log("Today is " + today);
log("Start day is " + startDay);
var numDays = 40;
var daysInMonth = 30;
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]);
}
var startDay = new Date();
var today = startDay.toISOString().substr(0, 10);
startDay.setUTCDate(startDay.getUTCDate() - numDays);
startDay = startDay.toISOString().substr(0, 10);
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]) {
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));
}
catch(err) {
log("ERROR: " + err);
printjson(err);
}
log("Script runtime: " + (new Date() - scriptStartTime));
function getActiveClassCounts(startDay) {
// Tally active classes per day, for paid, trial, and free
// TODO: does not handle class membership changes
if (!startDay) return {};
@ -60,7 +58,7 @@ function getActiveClassCounts(startDay) {
// paid: at least one paid member
// trial: not paid, at least one trial member
// 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
// Find classroom users
@ -86,30 +84,46 @@ function getActiveClassCounts(startDay) {
}
}
}
// log("DEBUG: Classroom users: " + classroomUserIDs.length);
log("Find user types..");
var userEventMap = {};
var userEventEndDateMap = {};
var prepaidUsersMap = {};
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()) {
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) {
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()] = [];
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
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()) {
doc = cursor.next();
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++) {
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 todayDate = new Date(new Date().toISOString().substring(0, 10));
var startObj = objectIdWithTimestamp(startDate);
var queryParams = {$and: [
{_id: {$gte: startObj}},
{user: {$in: classroomUserIDs}},
{event: 'Started Level'}
]};
cursor = logDB['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());
// 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: [
{_id: {$gte: startObj}},
{user: {$in: classroomUserIDs.slice(j * batchSize, j * batchSize + batchSize)}},
{event: 'Started Level'}
]};
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..");
var classDayTypeMap = {};
@ -159,7 +177,19 @@ function getActiveClassCounts(startDay) {
if (userPlayedMap[member]) {
for (var k = 0; k < userPlayedMap[member].length; k++) {
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;
}
}

View file

@ -8,6 +8,8 @@
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
// TODO: classroom paid active users before 4/13/16 not correct
try {
var logDB = new Mongo("localhost").getDB("analytics")
var scriptStartTime = new Date();
@ -129,32 +131,47 @@ function getActiveUserCounts(startDay, endDay, activeUserEvents) {
log("Classroom user count: " + classroomUserObjectIds.length);
// 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
// Free: not paid, not trial
log("Finding classroom users free/trial/paid status..");
var classroomUserEventMap = {};
var classroomUserEventEndDateMap = {};
var prepaidUsersMap = {};
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()) {
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) {
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()] = [];
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaidID);
}
else {
classroomUserEventMap[doc._id.valueOf()] = 'DAU classroom free';
}
}
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1});
while (cursor.hasNext()) {
doc = cursor.next();
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++) {
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 = {}
for (day in dayUserActiveMap) {
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;
if (!activeUsersCounts[day]) activeUsersCounts[day] = {};
if (!activeUsersCounts[day][event]) activeUsersCounts[day][event] = 0;

View file

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

View file

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

View file

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

View file

@ -13,6 +13,8 @@ User = require '../models/User'
Classroom = require '../models/Classroom'
facebook = require '../lib/facebook'
gplus = require '../lib/gplus'
TrialRequest = require '../models/TrialRequest'
log = require 'winston'
module.exports =
fetchByGPlusID: wrap (req, res, next) ->
@ -133,16 +135,7 @@ module.exports =
throw new errors.Conflict('Email already taken')
req.user.set({ password, email, anonymous: false })
try
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}))
yield module.exports.finishSignup(req, res)
signupWithFacebook: wrap (req, res) ->
unless req.user.isAnonymous()
@ -159,16 +152,7 @@ module.exports =
throw new errors.UnprocessableEntity('Invalid facebookAccessToken')
req.user.set({ facebookID, email, anonymous: false })
try
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}))
yield module.exports.finishSignup(req, res)
signupWithGPlus: wrap (req, res) ->
unless req.user.isAnonymous()
@ -186,6 +170,9 @@ module.exports =
throw new errors.UnprocessableEntity('Invalid gplusAccessToken')
req.user.set({ gplusID, email, anonymous: false })
yield module.exports.finishSignup(req, res)
finishSignup: co.wrap (req, res) ->
try
yield req.user.save()
catch e
@ -194,5 +181,21 @@ module.exports =
else
throw e
# post-successful account signup tasks
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}))

View file

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

View file

@ -106,4 +106,32 @@ LevelSessionSchema.set('toObject', {
return ret
})
module.exports = LevelSession = mongoose.model('level.session', LevelSessionSchema, 'level.sessions')
if config.mongo.level_session_replica_string?
levelSessionMongo = mongoose.createConnection()
levelSessionMongo.open config.mongo.level_session_replica_string, (error) ->
if error
log.error "Couldnt connect to session mongo!", error
else
log.info "Connected to seperate level session server with string", config.mongo.level_session_replica_string
else
levelSessionMongo = mongoose
LevelSession = levelSessionMongo.model('level.session', LevelSessionSchema, 'level.sessions')
if config.mongo.level_session_aux_replica_string?
auxLevelSessionMongo = mongoose.createConnection()
auxLevelSessionMongo.open config.mongo.level_session_aux_replica_string, (error) ->
if error
log.error "Couldnt connect to AUX session mongo!", error
else
log.info "Connected to seperate level AUX session server with string", config.mongo.level_session_aux_replica_string
auxLevelSession = auxLevelSessionMongo.model('level.session', LevelSessionSchema, 'level.sessions')
LevelSessionSchema.post 'save', (d) ->
return unless d instanceof LevelSession
o = d.toObject {transform: ((x, r) -> r), virtuals: false}
auxLevelSession.collection.save o, {w:1}, (err, v) ->
log.error err.stack if err
module.exports = LevelSession

View file

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

View file

@ -3,6 +3,7 @@ utils = require '../utils'
urlUser = '/db/user'
User = require '../../../server/models/User'
Classroom = require '../../../server/models/Classroom'
TrialRequest = require '../../../server/models/TrialRequest'
Prepaid = require '../../../server/models/Prepaid'
request = require '../request'
facebook = require '../../../server/lib/facebook'
@ -706,6 +707,32 @@ describe 'POST /db/user/:handle/signup-with-password', ->
[res, body] = yield request.postAsync({url, json})
expect(res.statusCode).toBe(409)
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', ->

View file

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