Merge branch 'master' into production

This commit is contained in:
Nick Winter 2015-11-24 13:54:14 -08:00
commit 7b88eb772a
24 changed files with 296 additions and 273 deletions

View file

@ -1,15 +1,18 @@
publishableKey = if application.isProduction() then 'pk_live_27jQZozjDGN1HSUTnSuM578g' else 'pk_test_zG5UwVu6Ww8YhtE9ZYh0JO6a'
module.exports = handler = StripeCheckout.configure({
key: publishableKey
name: 'CodeCombat'
email: me.get('email')
image: "https://codecombat.com/images/pages/base/logo_square_250.png"
token: (token) ->
console.log 'trigger?', handler.trigger
handler.trigger 'received-token', { token: token }
Backbone.Mediator.publish 'stripe:received-token', { token: token }
locale: 'auto'
})
if StripeCheckout?
module.exports = handler = StripeCheckout.configure({
key: publishableKey
name: 'CodeCombat'
email: me.get('email')
image: "https://codecombat.com/images/pages/base/logo_square_250.png"
token: (token) ->
console.log 'trigger?', handler.trigger
handler.trigger 'received-token', { token: token }
Backbone.Mediator.publish 'stripe:received-token', { token: token }
locale: 'auto'
})
else
module.exports = {}
console.error "Failure loading StripeCheckout API, returning empty object."
_.extend(handler, Backbone.Events)

View file

@ -123,6 +123,7 @@ module.exports = class LevelBus extends Bus
onWinnabilityUpdated: (e) ->
return unless @onPoint() and e.winnable
return unless e.level.get('slug') in ['ace-of-coders'] # Mirror matches don't otherwise show victory, so we win here.
return if @session.get('state')?.complete
@onVictory()
onNewWorldCreated: (e) ->

View file

@ -616,11 +616,15 @@
cost_premium_server: "CodeCombat is free for the first five levels, after which it costs $9.99 USD per month for access to our other 190+ levels on our exclusive country-specific servers."
free_1: "There are 110+ FREE levels which cover every concept."
free_2: "A monthly subscription provides access to video tutorials and extra practice levels."
teacher_subs_title: "Teachers get free subscriptions!"
teacher_subs_0: "We offer free subscriptions to teachers for evaluation purposes."
free_3: "The CodeCombat content is divided into"
free_4: "courses"
free_5: ". The first course is free, and about an hour of material."
free_6: "Access to the additional courses can be unlocked with a one-time purchase."
teacher_subs_title: "Teachers get a free trial!" # {change}
teacher_subs_0: "We offer free trials to teachers." # {change}
teacher_subs_1: "Please fill out our"
teacher_subs_2: "Teacher Survey"
teacher_subs_3: "to set up your subscription."
teacher_subs_3: "to try out the paid courses." # {change}
sub_includes_title: "What is included in the subscription?"
sub_includes_1: "In addition to the 110+ basic levels, students with a monthly subscription get access to these additional features:"
sub_includes_2: "80+ practice levels"
@ -664,16 +668,20 @@
title: "Teacher Survey"
must_be_logged: "You must be logged in first. Please create an account or log in from the menu above."
retrieving: "Retrieving information..."
being_reviewed_1: "Your application for a free trial subscription is being"
being_reviewed_1: "Your application for a free trial is being" # {change}
being_reviewed_2: "reviewed."
approved_1: "Your application for a free trial subscription was"
approved_1: "Your application for a free trial was" # {change}
approved_2: "approved."
approved_3: "Further instructions have been sent to"
denied_1: "Your application for a free trial subscription has been"
approved_4: "Enroll your students on the"
approved_5: "courses"
approved_6: "page."
denied_1: "Your application for a free trial has been" # {change}
denied_2: "denied."
contact_1: "Please contact"
contact_2: "if you have further questions."
description_1: "We offer free subscriptions to teachers for evaluation purposes. You can find more information on our"
description_1: "We offer free trials to teachers. You will be given 2 free enrollments which can be used to enroll students in paid courses." # {change}
description_1b: "You can find more information on our"
description_2: "teachers"
description_3: "page."
description_4: "Please fill out this quick survey and well email you setup instructions."

View file

@ -845,19 +845,19 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
playtime: "Tempo de Jogo"
last_played: "Último Jogo"
leagues_explanation: "Jogar em um campeonato contra outros membros do clã nestes casos de arena multiplayer."
# track_concepts1: "Track concepts"
# track_concepts2a: "learned by each student"
# track_concepts2b: "learned by each member"
# track_concepts3a: "Track levels completed for each student"
# track_concepts3b: "Track levels completed for each member"
# track_concepts4a: "See your students'"
# track_concepts4b: "See your members'"
# track_concepts5: "solutions"
# track_concepts6a: "Sort students by name or progress"
# track_concepts6b: "Sort members by name or progress"
# track_concepts7: "Requires invitation"
# track_concepts8: "to join"
# private_require_sub: "Private clans require a subscription to create or join."
track_concepts1: "Rastrear conceitos"
track_concepts2a: "aprendido por cada estudante"
track_concepts2b: "aprendido por cada membro"
track_concepts3a: "Rastrear níveis completados por cada estudante"
track_concepts3b: "Rastrear níveis completados por cada membro"
track_concepts4a: "Ver seus alunos'"
track_concepts4b: "Ver seus membros'"
track_concepts5: "soluções"
track_concepts6a: "Classificar alunos por nome ou progresso"
track_concepts6b: "Classificar membros por nome ou progresso"
track_concepts7: "Requer convite"
track_concepts8: "para se juntar"
private_require_sub: "Clãs particulares requerem uma assinatura para criar ou juntar-se."
# courses:
# course: "Course"

View file

@ -6,13 +6,14 @@ module.exports = class Prepaid extends CocoModel
urlRoot: '/db/prepaid'
openSpots: ->
@get('maxRedeemers') - @get('redeemers')?.length
return @get('maxRedeemers') - @get('redeemers')?.length if @get('redeemers')?
@get('maxRedeemers')
userHasRedeemed: (userID) ->
for redeemer in @get('redeemers')
return redeemer.date if redeemer.userID is userID
return null
initialize: ->
@listenTo @, 'add', ->
maxRedeemers = @get('maxRedeemers')

View file

@ -1,8 +1,5 @@
#teachers-free-trial-view
.input-email-address
width: 40%
.input-school
width: 40%
@ -18,6 +15,9 @@
.thanks-submit
display: none
.email-address
margin-right: 12px
.error-message
display: none
color: red

View file

@ -30,7 +30,7 @@
if level.get('type', true) == 'hero-ladder'
td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{(session.get('heroConfig') || {}).thangType || '529ffbf1cf1818f2be000001'}/portrait.png)")
td.rank-cell= rank + 1
td.score-cell= Math.round(sessionStats.totalScore * 100)
td.score-cell= Math.round((sessionStats.totalScore || session.get('totalScore') / 2) * 100)
td(class='name-col-cell' + ((new RegExp('(Simple|Shaman|Brawler|Chieftain|Thoktar) AI')).test(session.get('creatorName')) ? ' ai' : ''))= session.get('creatorName') || "Anonymous"
td.age-cell= moment(session.get('submitDate')).fromNow().replace('a few ', '')
td.fight-cell
@ -50,7 +50,7 @@
if level.get('type', true) == 'hero-ladder'
td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{(session.get('heroConfig') || {}).thangType || '529ffbf1cf1818f2be000001'}/portrait.png)")
td.rank-cell= session.rank
td.score-cell= Math.round(sessionStats.totalScore * 100)
td.score-cell= Math.round((sessionStats.totalScore || session.get('totalScore') / 2) * 100)
td(class='name-col-cell' + ((new RegExp('(Simple|Shaman|Brawler|Chieftain|Thoktar) AI')).test(session.get('creatorName')) ? ' ai' : ''))= session.get('creatorName') || "Anonymous"
td.age-cell= moment(session.get('submitDate')).fromNow().replace('a few ', '')
td.fight-cell

View file

@ -5,11 +5,12 @@ block modal-header-content
block modal-body-content
h4.language-selection(data-i18n="ladder.select_your_language") Select your language!
.form-group.select-group
select#tome-language(name="language")
for option in languages
option(value=option.id selected=(language === option.id))= option.name
if view.level.get('type') != 'course-ladder'
h4.language-selection(data-i18n="ladder.select_your_language") Select your language!
.form-group.select-group
select#tome-language(name="language")
for option in languages
option(value=option.id selected=(language === option.id))= option.name
div#noob-view.secret
a(href="/play/level/#{levelID}-tutorial" + (league ? "?league=" + league.id : "")).btn.btn-success.btn-block.btn-lg

View file

@ -11,3 +11,7 @@ if !observing
button.btn.btn-lg.btn-illustrated.btn-success.done-button.secret
span(data-i18n="play_level.done") Done
if view.autoSubmitsToLadder
.hidden
.ladder-submission-view

View file

@ -16,10 +16,14 @@ block content
strong(data-i18n="teachers_survey.being_reviewed_2")
else if existingRequest.get('status') === 'approved'
p
span.spr(data-i18n="teachers_survey.approved_1")
strong.spr(data-i18n="teachers_survey.approved_2")
span.spr(data-i18n="teachers_survey.approved_3")
strong= existingRequest.get('properties').email
span.spr(data-i18n="teachers_survey.approved_1")
strong.spr(data-i18n="teachers_survey.approved_2")
span.spr(data-i18n="teachers_survey.approved_3")
strong= existingRequest.get('properties').email
p
span.spr(data-i18n="teachers_survey.approved_4")
a(href='/courses', data-i18n="teachers_survey.approved_5")
span.spl(data-i18n="teachers_survey.approved_6")
else
p
span.spr(data-i18n="teachers_survey.denied_1")
@ -29,15 +33,20 @@ block content
a(href='mailto:team@codecombat.com') team@codecombat.com
span.spl(data-i18n="teachers_survey.contact_2")
else
p(data-i18n="teachers_survey.description_1")
p
strong.spr Hour of Code Special!
span Complete the survey by December 31st and enroll all your students in the paid courses for 2 months.
p
span.spr(data-i18n="teachers_survey.description_1")
span.spr(data-i18n="teachers_survey.description_1b")
a(href='/teachers', data-i18n="teachers_survey.description_2")
span.spl(data-i18n="teachers_survey.description_3")
p(data-i18n="teachers_survey.description_4")
p.container-email-address
label.control-label(data-i18n="teachers_survey.email")
br
input.control-label.input-email-address(type='text', value=view.email)
span.email-address= view.email
a(href='/account/settings') Change
p.container-school
label.control-label(data-i18n="teachers_survey.school")
br

View file

@ -6,17 +6,10 @@ block content
p
strong Hi Teachers!
p We're excited to participate in Hour of Code this year!
p We've set up an Introduction to Computer Science course, just for you.
p
strong How to use CodeCombat with your students:
ol
li
span.spr Navigate to the
a(href='/courses/teachers?hoc=true') Courses
span.spl page
li Click the green 'Get FREE course' button under Introduction to Computer Science
li Follow the enrollment instructions
li Add students via the 'Add Students' tab
span.spr Navigate to the
a(href='/courses/teachers?hoc=true') CodeCombat Courses
span.spl page to get started.
p
span.spr If you have any problems, please email
a(href='mailto:team@codecombat.com') team@codecombat.com
@ -25,14 +18,17 @@ block content
h2(data-i18n="teachers.more_info")
p(data-i18n="teachers.intro_1")
p(data-i18n="teachers.intro_2")
h3(data-i18n="teachers.free_title")
if me.isOnPremiumServer()
p(data-i18n="teachers.cost_premium_server")
else
p(data-i18n="teachers.free_1")
p(data-i18n="teachers.free_2")
p
span.spr(data-i18n="teachers.free_3")
a(href='/courses', data-i18n="teachers.free_4")
span(data-i18n="teachers.free_5")
p(data-i18n="teachers.free_6")
p
span.spr For more details, please email
a(href='mailto:team@codecombat.com') team@codecombat.com
h3.teachers-title(data-i18n="teachers.teacher_subs_title")
p(data-i18n="teachers.teacher_subs_0")
p
@ -40,129 +36,6 @@ block content
a(href='/teachers/freetrial', data-i18n="teachers.teacher_subs_2")
span.spl(data-i18n="teachers.teacher_subs_3")
h3(data-i18n="teachers.sub_includes_title")
p(data-i18n="teachers.sub_includes_1")
ul
li(data-i18n="teachers.sub_includes_2")
li(data-i18n="teachers.sub_includes_3")
li(data-i18n="teachers.sub_includes_4")
li(data-i18n="teachers.sub_includes_5")
li(data-i18n="teachers.sub_includes_6")
li(data-i18n="teachers.sub_includes_7")
h3(data-i18n="teachers.who_for_title")
p(data-i18n="teachers.who_for_1")
p(data-i18n="teachers.who_for_2")
h3(data-i18n="teachers.monitor_progress_title")
p
span.spr(data-i18n="teachers.monitor_progress_1")
a(href='/clans', data-i18n="clans.clan")
span.spl(data-i18n="teachers.monitor_progress_2")
p
span.spr(data-i18n="teachers.monitor_progress_3")
a(href='/clans', data-i18n="clans.clans")
span.spl(data-i18n="teachers.monitor_progress_4")
p(data-i18n="teachers.monitor_progress_5")
h4(data-i18n="teachers.sub_includes_7")
ul
li
strong(data-i18n="clans.track_concepts1")
span.spl(data-i18n="clans.track_concepts2a")
li(data-i18n="clans.track_concepts3a")
li
span(data-i18n="clans.track_concepts4a")
strong.spl(data-i18n="clans.track_concepts5")
li(data-i18n="clans.track_concepts6a")
li
strong(data-i18n="clans.track_concepts7")
span.spl(data-i18n="clans.track_concepts8")
p
img(src='/images/pages/clans/dashboard_preview.png' height='400')
p
span.spr(data-i18n="teachers.private_clans_2")
a(href='/clans', data-i18n="clans.clan")
span(data-i18n="teachers.private_clans_3")
p(data-i18n="clans.private_require_sub")
h3(data-i18n="teachers.material_title")
if me.isOnPremiumServer()
p(data-i18n="teachers.material_premium_server")
else
p(data-i18n="teachers.material_1")
h3(data-i18n="teachers.concepts_title")
//- TODO: i18n for concepts?
table.table.table-condensed.concepts-table
thead
tr
th
a(href='/play/dungeon') Kithgard Dungeon
th
a(href='/play/forest') Backwoods Forest
th
a(href='/play/desert') Sarven Desert
th
a(href='/play/mountain') Cloudrip Mountain
tbody
tr
td Basic Syntax
td If Statements
td Arithmetic
td Object Literals
tr
td Methods
td Relational Operators
td While Loops
td For Loops
tr
td Parameters
td Object Properties
td Break Statements
td Functions
tr
td Strings
td Input Handling
td Arrays
td Drawing
tr
td Loops
td
td String Comparison
td Modulo
tr
td Variables
td
td Finding Min/Max
td
h3(data-i18n="teachers.how_much_title")
p
span(data-i18n="teachers.how_much_1")
span.spr.spl
a(href='/account/subscription', data-i18n="teachers.how_much_2")
span.spr.spl(data-i18n="teachers.how_much_3")
p
span.spr(data-i18n="teachers.how_much_5")
a(href='mailto:team@codecombat.com') team@codecombat.com
span.spl(data-i18n="teachers.how_much_6")
p(data-i18n="teachers.how_much_4")
h4
a(href='/account/subscription', data-i18n="subscribe.group_discounts")
p(data-i18n="subscribe.group_discounts_1")
table.table.table-condensed.discount-table
tr
td(data-i18n="subscribe.group_discounts_1st")
td(data-i18n="subscribe.group_discounts_full")
tr
td(data-i18n="subscribe.group_discounts_2nd")
td(data-i18n="subscribe.group_discounts_20")
tr
td(data-i18n="subscribe.group_discounts_12th")
td(data-i18n="subscribe.group_discounts_40")
h3(data-i18n="teachers.more_info_title")
p
span.spr(data-i18n="teachers.more_info_1")

View file

@ -30,7 +30,6 @@ module.exports = class TeachersFreeTrialView extends RootView
$('.radio-other').prop("checked", true)
onClickSubmit: (e) ->
email = $('.input-email-address').val()
school = $('.input-school').val()
location = $('.input-location').val()
age = $('input[name=age]:checked').val()
@ -46,10 +45,6 @@ module.exports = class TeachersFreeTrialView extends RootView
$('.container-num-students').removeClass('has-error')
$('.container-heard-about').removeClass('has-error')
$('.error-message').hide()
unless email
$('.container-email-address').addClass('has-error')
$('.error-message').show()
return
unless school
$('.container-school').addClass('has-error')
$('.error-message').show()
@ -75,7 +70,7 @@ module.exports = class TeachersFreeTrialView extends RootView
trialRequest = new TrialRequest
type: 'subscription'
properties:
email: email
email: @email
school: school
location: location
age: age

View file

@ -4,9 +4,3 @@ template = require 'templates/teachers'
module.exports = class TeachersView extends RootView
id: 'teachers-view'
template: template
constructor: ->
super()
_.defer ->
# Redirect to HoC version of /courses/teachers until we update the /teachers landing page
application.router.navigate "/courses/teachers?hoc=true", trigger: true

View file

@ -100,7 +100,7 @@ module.exports = class DiplomatView extends ContributeClassView
fr: ['Anon', 'Armaldio', 'ChrisLightman', 'Elfisen', 'Feugy', 'MartinDelille', 'Oaugereau', 'Xeonarno', 'dc55028', 'jaybi', 'pstweb', 'veritable', 'xavismeh'] # français, French
ja: ['Coderaulic', 'g1itch', 'kengos', 'treby'] # , Japanese
ar: ['5y', 'ahmed80dz'] # العربية, Arabic
'pt-BR': ['Bia41', 'Gutenberg Barros', 'Kieizroe', 'Matthew Burt', 'brunoporto', 'cassiocardoso', 'jklemm'] # português do Brasil, Portuguese (Brazil)
'pt-BR': ['Bia41', 'Gutenberg Barros', 'Kieizroe', 'Matthew Burt', 'brunoporto', 'cassiocardoso', 'jklemm', 'Arkhad'] # português do Brasil, Portuguese (Brazil)
'pt-PT': ['Imperadeiro98', 'Matthew Burt', 'ProgramadorLucas', 'ReiDuKuduro', 'batista', 'gutierri'] # Português (Portugal), Portuguese (Portugal)
pl: ['Anon', 'Kacper Ciepielewski', 'TigroTigro', 'kvasnyk'] # język polski, Polish
it: ['AlessioPaternoster', 'flauta', 'Atomk'] # italiano, Italian

View file

@ -161,7 +161,9 @@ module.exports = class TeacherCoursesView extends RootView
return
user = @usersToRedeem.first()
prepaid = @prepaids.find (prepaid) -> prepaid.openSpots()
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties').endDate? and prepaid.openSpots())
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots()) unless prepaid
$.ajax({
method: 'POST'
url: _.result(prepaid, 'url') + '/redeemers'

View file

@ -280,7 +280,10 @@ module.exports = class LadderTabView extends CocoView
consolidateFriends: ->
allFriendSessions = (@facebookFriendSessions or []).concat(@gplusFriendSessions or [])
sessions = _.uniq allFriendSessions, false, (session) -> session._id
sessions = _.sortBy sessions, 'totalScore'
if @options.league
sessions = _.sortBy sessions, (session) -> _.find(session.leagues, leagueID: @options.league.id)?.stats.totalScore ? (session.totalScore / 2)
else
sessions = _.sortBy sessions, 'totalScore'
sessions.reverse()
sessions

View file

@ -120,8 +120,10 @@ module.exports = class HeroVictoryModal extends ModalView
@thangTypes[thangTypeOriginal] = @supermodel.loadModel(thangType, 'thang').model
@newEarnedAchievements = []
hadOneCompleted = false
for achievement in @achievements.models
continue unless achievement.completed
hadOneCompleted = true
ea = new EarnedAchievement({
collection: achievement.get('collection')
triggeredBy: @session.id
@ -137,7 +139,7 @@ module.exports = class HeroVictoryModal extends ModalView
@updateSavingProgressStatus()
me.fetch cache: false unless me.loading
@readyToContinue = true if not @achievements.models.length
@readyToContinue = true unless hadOneCompleted
# 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

View file

@ -31,6 +31,7 @@ module.exports = class CastButtonView extends CocoView
@updateReplayabilityInterval = setInterval @updateReplayability, 1000
@observing = options.session.get('creator') isnt me.id
@loadMirrorSession() if @options.level.get('slug') in ['ace-of-coders']
@autoSubmitsToLadder = @options.level.get('slug') in ['wakka-maul']
destroy: ->
clearInterval @updateReplayabilityInterval
@ -101,10 +102,10 @@ module.exports = class CastButtonView extends CocoView
@casting = false
if @hasCastOnce # Don't play this sound the first time
@playSound 'cast-end', 0.5
# Worked great for live Ace of Coders tournament, but probably annoying for asynchronous tournament mode.
#myHeroID = if me.team is 'ogres' then 'Hero Placeholder 1' else 'Hero Placeholder'
#if @ladderSubmissionView and not e.world.thangMap[myHeroID]?.errorsOut
# _.delay (=> @ladderSubmissionView?.rankSession()), 1000 if @ladderSubmissionView
# Worked great for live beginner tournaments, but probably annoying for asynchronous tournament mode.
myHeroID = if me.team is 'ogres' then 'Hero Placeholder 1' else 'Hero Placeholder'
if @autoSubmitsToLadder and not e.world.thangMap[myHeroID]?.errorsOut
_.delay (=> @ladderSubmissionView?.rankSession()), 1000 if @ladderSubmissionView
@hasCastOnce = true
@updateCastButton()
@world = e.world

View file

@ -237,11 +237,8 @@ LevelHandler = class LevelHandler extends Handler
getLeaderboard: (req, res, id) ->
sessionsQueryParameters = @makeLeaderboardQueryParameters(req, id)
sortParameters =
'totalScore': req.query.order
selectProperties = ['totalScore', 'creatorName', 'creator', 'submittedCodeLanguage', 'heroConfig', 'leagues.leagueID', 'leagues.stats.totalScore', 'submitDate']
sortParameters = totalScore: req.query.order
selectProperties = ['totalScore', 'creatorName', 'creator', 'submittedCodeLanguage', 'heroConfig', 'leagues.leagueID', 'leagues.stats.totalScore', 'submitDate', 'team']
query = Session
.find(sessionsQueryParameters)
.limit(req.query.limit)
@ -252,7 +249,13 @@ LevelHandler = class LevelHandler extends Handler
query.exec (err, resultSessions) =>
return @sendDatabaseError(res, err) if err
resultSessions ?= []
@sendSuccess res, resultSessions
leaderboardOptions = find: sessionsQueryParameters, limit: req.query.limit, sort: sortParameters, select: selectProperties
@interleaveAILeaderboardSessions leaderboardOptions, resultSessions, (err, resultSessions) =>
return @sendDatabaseError(res, err) if err
if league = req.query['leagues.leagueID']
resultSessions = _.sortBy resultSessions, (session) -> _.find(session.get('leagues'), leagueID: league)?.stats.totalScore ? session.get('totalScore') / 2
resultSessions.reverse() if sortParameters.totalScore is -1
@sendSuccess res, resultSessions
getMyLeaderboardRank: (req, res, id) ->
req.query.order = 1
@ -283,6 +286,36 @@ LevelHandler = class LevelHandler extends Handler
req.query.team ?= 'humans'
req.query.limit = parseInt(req.query.limit) ? 20
ladderBenchmarkAIs: [
'564ba6cea33967be1312ae59'
'564ba830a33967be1312ae61'
'564ba91aa33967be1312ae65'
'564ba95ca33967be1312ae69'
'564ba9b7a33967be1312ae6d'
]
interleaveAILeaderboardSessions: (leaderboardOptions, sessions, cb) ->
return cb null, sessions unless leaderboardOptions.find['leagues.leagueID']
return cb null, sessions if leaderboardOptions.limit < 10 # Don't put them in when we're fetching sessions around another session
# Get our list of benchmark AI sessions
benchmarkSessions = Session
.find(level: leaderboardOptions.find.level, creator: {$in: @ladderBenchmarkAIs})
.sort(leaderboardOptions.sort)
.select(leaderboardOptions.select.join ' ')
.cache() # TODO: How long does this cache? We will probably want these to be pretty long.
.exec (err, aiSessions) ->
return cb err if err
matchingAISessions = _.filter aiSessions, (aiSession) ->
return false unless aiSession.get('team') is leaderboardOptions.find.team
return false if $gt = leaderboardOptions.find.totalScore.$gt and aiSession.get('totalScore') <= $gt
return false if $lt = leaderboardOptions.find.totalScore.$lt and aiSession.get('totalScore') >= $lt
true
# TODO: these aren't real league scores for AIs, but rather the general leaderboard scores, which will make most AI scores artificially high. So we divide by 2 for AI scores not part of the league. Pretty weak, I know. Eventually we'd want them to actually play league matches as if they were in all leagues, but without having infinite space requirements or something? Or change the UI to take them out of the main league table and into their separate area.
sessions = _.sortBy sessions.concat(matchingAISessions), (session) -> _.find(session.get('leagues'), leagueID: leaderboardOptions.find['leagues.leagueID'])?.stats.totalScore ? session.get('totalScore') / 2
sessions.reverse() if leaderboardOptions.sort.totalScore is -1
sessions = sessions.slice 0, leaderboardOptions.limit
return cb null, sessions
getLeaderboardFacebookFriends: (req, res, id) -> @getLeaderboardFriends(req, res, id, 'facebookID')
getLeaderboardGPlusFriends: (req, res, id) -> @getLeaderboardFriends(req, res, id, 'gplusID')
getLeaderboardFriends: (req, res, id, serviceProperty) ->

View file

@ -74,8 +74,9 @@ PrepaidHandler = class PrepaidHandler extends Handler
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) if not prepaid
return @sendForbiddenError(res) if prepaid.get('creator').toString() isnt req.user.id
return @sendForbiddenError(res) if _.size(prepaid.get('redeemers')) >= prepaid.get('maxRedeemers')
return @sendForbiddenError(res) if prepaid.get('redeemers')? and _.size(prepaid.get('redeemers')) >= prepaid.get('maxRedeemers')
return @sendForbiddenError(res) unless prepaid.get('type') is 'course'
return @sendForbiddenError(res) if prepaid.get('properties')?.endDate < new Date()
User.findById(req.body.userID).exec (err, user) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'User for given ID not found') if not user
@ -85,14 +86,14 @@ PrepaidHandler = class PrepaidHandler extends Handler
query =
_id: prepaid.get('_id')
'redeemers.userID': { $ne: user.get('_id') }
$where: "this.redeemers.length < #{prepaid.get('maxRedeemers')}"
$where: "this.maxRedeemers > 0 && (!this.redeemers || this.redeemers.length < #{prepaid.get('maxRedeemers')})"
update = { $push: { redeemers : { date: new Date(), userID: userID } }}
Prepaid.update query, update, (err, nMatched) =>
return @sendDatabaseError(res, err) if err
if nMatched is 0
@logError(req.user, "POST prepaid redeemer lost race on maxRedeemers")
return @sendForbiddenError(res)
user.set('coursePrepaidID', prepaid.get('_id'))
user.save (err, user) =>
return @sendDatabaseError(res, err) if err
@ -100,7 +101,7 @@ PrepaidHandler = class PrepaidHandler extends Handler
redeemers = _.clone(prepaid.get('redeemers') or [])
redeemers.push({ date: new Date(), userID: userID })
prepaid.set('redeemers', redeemers)
@sendSuccess(res, @formatEntity(req, prepaid))
@sendSuccess(res, @formatEntity(req, prepaid))
createPrepaid: (user, type, maxRedeemers, properties, done) ->
Prepaid.generateNewCode (code) =>
@ -241,12 +242,15 @@ PrepaidHandler = class PrepaidHandler extends Handler
return @sendBadInputError(res, 'Bad creator') unless utils.isID creator
q = {
_id: {$gt: cutoffID}
creator: mongoose.Types.ObjectId(creator),
creator: mongoose.Types.ObjectId(creator)
type: 'course'
}
Prepaid.find q, (err, prepaids) =>
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, (@formatEntity(req, prepaid) for prepaid in prepaids))
documents = []
for prepaid in prepaids
documents.push(@formatEntity(req, prepaid)) unless prepaid.get('properties')?.endDate < new Date()
return @sendSuccess(res, documents)
else
super(arguments...)
@ -254,5 +258,5 @@ PrepaidHandler = class PrepaidHandler extends Handler
prepaid = super(req)
prepaid.set('redeemers', [])
return prepaid
module.exports = new PrepaidHandler()

View file

@ -11,7 +11,6 @@ if config.unittest
module.exports.api.send = ->
module.exports.templates =
parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud'
setup_free_sub_email: 'tem_sqdvLCZRwoDQc6jAf5RrQE'
share_progress_email: 'tem_VHE3ihhGmVa3727qds9zY8'
welcome_email: 'utnGaBHuSU4Hmsi7qrAypU'
ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4'
@ -23,4 +22,5 @@ module.exports.templates =
plain_text_email: 'tem_85UvKDCCNPXsFckERTig6Y'
next_steps_email: 'tem_RDHhTG5inXQi8pthyqWr5D'
course_invite_email: 'tem_u6D2EFWYC5Ptk38bSykjsU'
teacher_free_trial: 'tem_sqdvLCZRwoDQc6jAf5RrQE'
teacher_free_trial_hoc: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc'

View file

@ -11,51 +11,41 @@ TrialRequestSchema = new mongoose.Schema {}, {strict: false, minimize: false, re
TrialRequestSchema.pre 'save', (next) ->
return next() unless @get('status') is 'approved'
# Add subscription
Prepaid.generateNewCode (code) =>
unless code
log.error "Trial request pre save prepaid gen new code failure"
return next()
# Add 2 course headcount
prepaid = new Prepaid
creator: @get('applicant')
type: 'course'
maxRedeemers: 2
properties:
trialRequestID: @get('_id')
prepaid.save (err) =>
if err
log.error "Trial request prepaid creation error: #{err}"
# Special HoC trial: Add 500 course headcount with end date
endDate = new Date()
endDate.setUTCMonth(endDate.getUTCMonth() + 2)
prepaid = new Prepaid
creator: @get('reviewer')
type: 'subscription'
maxRedeemers: 1
code: code
creator: @get('applicant')
type: 'course'
maxRedeemers: 500
properties:
couponID: 'free'
endDate: endDate
trialRequestID: @get('_id')
prepaid.save (err) =>
if err
log.error "Trial request prepaid creation error: #{err}"
return next()
@set('prepaidCode', code)
# Add 2 course headcount
prepaid = new Prepaid
creator: @get('applicant')
type: 'course'
maxRedeemers: 2
properties:
trialRequestID: @get('_id')
prepaid.save (err) =>
if err
log.error "Trial request prepaid creation error: #{err}"
next()
next()
TrialRequestSchema.post 'save', (doc) ->
if doc.get('status') is 'submitted'
msg = "<a href=\"http://codecombat.com/admin/trial-requests\">Trial Request</a> submitted by #{doc.get('properties').email}"
msg = "<a href=\"http://codecombat.com/admin/trial-requests\">Trial Request</a> submitted by #{doc.get('properties')?.email}"
hipchat.sendHipChatMessage msg, ['tower']
else if doc.get('status') is 'approved'
ppc = doc.get('prepaidCode')
unless ppc
log.error 'Trial request post save no ppc'
return
emailParams =
recipient:
address: doc.get('properties')?.email
email_id: sendwithus.templates.setup_free_sub_email
email_data:
url: "https://codecombat.com/account/subscription?_ppc=#{ppc}";
email_id: sendwithus.templates.teacher_free_trial_hoc
sendwithus.api.send emailParams, (err, result) =>
log.error "sendwithus trial request approved error: #{err}, result: #{result}" if err

View file

@ -33,7 +33,7 @@ describe '/db/prepaid', ->
clearModels [Course, CourseInstance, Payment, Prepaid, User], (err) ->
throw err if err
done()
describe 'POST /db/prepaid/<id>/redeemers', ->
it 'adds a given user to the redeemers property', (done) ->
@ -60,7 +60,7 @@ describe '/db/prepaid', ->
User.findById otherUser.id, (err, user) ->
expect(user.get('coursePrepaidID').equals(prepaid.get('_id'))).toBe(true)
done()
it 'does not allow more redeemers than maxRedeemers', (done) ->
loginNewUser (user1) ->
prepaid = new Prepaid({
@ -97,7 +97,7 @@ describe '/db/prepaid', ->
request.post {uri: url, json: redeemer }, (err, res, body) ->
expect(res.statusCode).toBe(403)
done()
it 'is idempotent across prepaids collection', (done) ->
loginNewUser (user1) ->
otherUser = new User({
@ -125,7 +125,7 @@ describe '/db/prepaid', ->
return done() unless res.statusCode is 200
expect(body.redeemers.length).toBe(0)
done()
it 'is idempotent to itself for a user other than the creator', (done) ->
loginNewUser (user1) ->
prepaid = new Prepaid({
@ -184,6 +184,101 @@ describe '/db/prepaid', ->
expect(res.statusCode).toBe(200)
done()
it 'return terminal prepaids', (done) ->
endDate = new Date()
endDate.setUTCMonth(endDate.getUTCMonth() + 2)
loginNewUser (user1) ->
prepaid = new Prepaid({
maxRedeemers: 500,
redeemers: [],
creator: user1.get('_id')
type: 'course'
properties:
endDate: endDate
})
prepaid.save (err, prepaid) ->
expect(err).toBeNull()
url = getURL("/db/prepaid?creator=#{user1.id}")
request.get {uri: url}, (err, res, body) ->
expect(res.statusCode).toBe(200)
documents = JSON.parse(body)
expect(documents.length).toEqual(1)
return done() unless documents.length is 1
expect(documents[0]?.properties?.endDate).toEqual(endDate.toISOString())
done()
it 'do not return expired terminal prepaids', (done) ->
endDate = new Date()
endDate.setUTCMonth(endDate.getUTCMonth() - 1)
loginNewUser (user1) ->
prepaid = new Prepaid({
maxRedeemers: 500,
redeemers: [],
creator: user1.get('_id')
type: 'course'
properties:
endDate: endDate
})
prepaid.save (err, prepaid) ->
expect(err).toBeNull()
url = getURL("/db/prepaid?creator=#{user1.id}")
request.get {uri: url}, (err, res, body) ->
expect(res.statusCode).toBe(200)
documents = JSON.parse(body)
expect(documents.length).toEqual(0)
done()
it 'redeem terminal prepaids', (done) ->
endDate = new Date()
endDate.setUTCMonth(endDate.getUTCMonth() + 2)
loginNewUser (user1) ->
prepaid = new Prepaid({
maxRedeemers: 500,
redeemers: [],
creator: user1.get('_id')
type: 'course'
properties:
endDate: endDate
})
prepaid.save (err, prepaid) ->
expect(err).toBeNull()
otherUser = new User()
otherUser.save (err, otherUser) ->
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
redeemer = { userID: otherUser.id }
request.post {uri: url, json: redeemer }, (err, res, body) ->
expect(body.redeemers?.length).toBe(1)
expect(res.statusCode).toBe(200)
return done() unless res.statusCode is 200
prepaid = Prepaid.findById body._id, (err, prepaid) ->
expect(err).toBeNull()
expect(prepaid.get('redeemers').length).toBe(1)
User.findById otherUser.id, (err, user) ->
expect(user.get('coursePrepaidID').equals(prepaid.get('_id'))).toBe(true)
done()
it 'do not redeem expired terminal prepaids', (done) ->
endDate = new Date()
endDate.setUTCMonth(endDate.getUTCMonth() - 1)
loginNewUser (user1) ->
prepaid = new Prepaid({
maxRedeemers: 500,
redeemers: [],
creator: user1.get('_id')
type: 'course'
properties:
endDate: endDate
})
prepaid.save (err, prepaid) ->
expect(err).toBeNull()
otherUser = new User()
otherUser.save (err, otherUser) ->
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
redeemer = { userID: otherUser.id }
request.post {uri: url, json: redeemer }, (err, res, body) ->
expect(res.statusCode).toBe(403)
done()
it 'Clear database', (done) ->
clearModels [Course, CourseInstance, Payment, Prepaid, User], (err) ->
throw err if err
@ -304,7 +399,7 @@ describe '/db/prepaid', ->
expect(err).toBeNull()
expect(res.statusCode).toBe(422)
done()
it 'Standard user purchases a prepaid for 1 seat', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
@ -324,7 +419,7 @@ describe '/db/prepaid', ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
verifyCoursePrepaid(user1, prepaid, done)
describe 'Purchase terminal_subscription', ->
it 'Anonymous submits a prepaid purchase', (done) ->
stripe.tokens.create {
@ -567,16 +662,16 @@ describe '/db/prepaid', ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginJoe (joe) ->
loginNewUser (user) ->
purchasePrepaid 'terminal_subscription', months: 1, 3, token.id, (err, res, prepaid) ->
request.get "#{getURL('/db/user')}/#{joe.id}/prepaid_codes", (err, res) ->
request.get "#{getURL('/db/user')}/#{user.id}/prepaid_codes", (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200);
codes = JSON.parse res.body
expect(codes.length).toEqual(2)
expect(codes.length).toEqual(1)
expect(codes[0].maxRedeemers).toEqual(3)
expect(codes[0].properties).toBeDefined()
expect(codes[0].properties.months).toEqual(3)
expect(codes[0].properties.months).toEqual(1)
done()
it 'Test for injection', (done) ->

View file

@ -120,20 +120,24 @@ describe 'Trial Requests', ->
expect(body.reviewDate).toBeDefined()
expect(new Date(body.reviewDate)).toBeLessThan(new Date())
expect(body.reviewer).toEqual(admin.id)
expect(body.prepaidCode).toBeDefined()
TrialRequest.findById body._id, (err, doc) ->
expect(err).toBeNull()
expect(doc.get('status')).toEqual('approved')
expect(doc.get('reviewDate')).toBeDefined()
expect(new Date(doc.get('reviewDate'))).toBeLessThan(new Date())
expect(doc.get('reviewer')).toEqual(admin._id)
expect(doc.get('prepaidCode')).toBeDefined()
Prepaid.findOne {'properties.trialRequestID': doc.get('_id')}, (err, doc) ->
Prepaid.find {'properties.trialRequestID': doc.get('_id')}, (err, prepaids) ->
expect(err).toBeNull()
return done(err) if err
expect(doc.get('type')).toEqual('course')
expect(doc.get('creator')).toEqual(user.get('_id'))
expect(doc.get('maxRedeemers')).toEqual(2)
expect(prepaids.length).toEqual(2)
for prepaid in prepaids
expect(prepaid.get('type')).toEqual('course')
expect(prepaid.get('creator')).toEqual(user.get('_id'))
if prepaid.get('properties').endDate
expect(prepaid.get('maxRedeemers')).toEqual(500)
expect(prepaid.get('properties').endDate).toBeGreaterThan(new Date())
else
expect(prepaid.get('maxRedeemers')).toEqual(2)
done()
it 'Deny trial request', (done) ->