Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-03-10 22:05:23 -07:00
commit 3576805145
8 changed files with 153 additions and 102 deletions

View file

@ -265,7 +265,7 @@ module.exports = nativeDescription: "limba română", englishDescription: "Roman
message: "Mesaj" message: "Mesaj"
about: about:
who_is_codecombat: "Cine este CodeCombat?" # I assume you meant (what) who_is_codecombat: "Cine este CodeCombat?"
why_codecombat: "De ce CodeCombat?" why_codecombat: "De ce CodeCombat?"
who_description_prefix: "au pornit împreuna CodeCombat în 2013. Tot noi am creat " who_description_prefix: "au pornit împreuna CodeCombat în 2013. Tot noi am creat "
who_description_suffix: "în 2008, dezvoltând aplicația web si iOS #1 de învățat cum să scri caractere Japoneze si Chinezești." who_description_suffix: "în 2008, dezvoltând aplicația web si iOS #1 de învățat cum să scri caractere Japoneze si Chinezești."
@ -277,75 +277,75 @@ module.exports = nativeDescription: "limba română", englishDescription: "Roman
why_paragraph_3_center: "ci" why_paragraph_3_center: "ci"
why_paragraph_3_italic_caps: "TREBUIE SĂ TERMIN ACEST NIVEL!" why_paragraph_3_italic_caps: "TREBUIE SĂ TERMIN ACEST NIVEL!"
why_paragraph_3_suffix: "De aceea CodeCombat este un joc multiplayer, nu un curs transfigurat în joc. Nu ne vom opri până când tu nu te poți opri--și de data asta, e de bine." why_paragraph_3_suffix: "De aceea CodeCombat este un joc multiplayer, nu un curs transfigurat în joc. Nu ne vom opri până când tu nu te poți opri--și de data asta, e de bine."
# why_paragraph_4: "If you're going to get addicted to some game, get addicted to this one and become one of the wizards of the tech age." why_paragraph_4: "Dacă e să devi dependent de vreun joc, devino dependent de acesta și fi un vrăjitor al noii ere tehnologice."
# why_ending: "And hey, it's free. " why_ending: "Nu uita, este totul gratis. "
# why_ending_url: "Start wizarding now!" why_ending_url: "Devino un vrăjitor acum!"
# george_description: "CEO, business guy, web designer, game designer, and champion of beginning programmers everywhere." george_description: "CEO, business guy, web designer, game designer, și campion al programatorilor începători."
# scott_description: "Programmer extraordinaire, software architect, kitchen wizard, and master of finances. Scott is the reasonable one." scott_description: "Programmer extraordinaire, software architect, kitchen wizard, și maestru al finanțelor. Scott este cel rezonabil."
# nick_description: "Programming wizard, eccentric motivation mage, and upside-down experimenter. Nick can do anything and chooses to build CodeCombat." nick_description: "Programming wizard, eccentric motivation mage, and upside-down experimenter. Nick poate să facă orice si a ales să dezvolte CodeCombat."
# jeremy_description: "Customer support mage, usability tester, and community organizer; you've probably already spoken with Jeremy." jeremy_description: "Customer support mage, usability tester, and community organizer; probabil ca ați vorbit deja cu Jeremy."
# michael_description: "Programmer, sys-admin, and undergrad technical wunderkind, Michael is the person keeping our servers online." michael_description: "Programmer, sys-admin, and undergrad technical wunderkind, Michael este cel care ține serverele in picioare."
# legal: legal:
# page_title: "Legal" page_title: "Aspecte Legale"
# opensource_intro: "CodeCombat is free to play and completely open source." opensource_intro: "CodeCombat este free-to-play și complet open source."
# opensource_description_prefix: "Check out " opensource_description_prefix: "Vizitează "
# github_url: "our GitHub" github_url: "pagina noastră de GitHub"
# opensource_description_center: "and help out if you like! CodeCombat is built on dozens of open source projects, and we love them. See " opensource_description_center: "și ajută-ne dacă îți place! CodeCombat este construit peste o mulțime de proiecte open source, care noi le iubim. Vizitați"
# archmage_wiki_url: "our Archmage wiki" archmage_wiki_url: "Archmage wiki"
# opensource_description_suffix: "for a list of the software that makes this game possible." opensource_description_suffix: "pentru o listă cu software-ul care face acest joc posibil."
# practices_title: "Respectful Best Practices" # practices_title: "Respectful Best Practices" #not sure what you mean here? other word for /practices/?
# practices_description: "These are our promises to you, the player, in slightly less legalese." practices_description: "Acestea sunt promisiunile noastre către tine, jucătorul, fără așa mulți termeni legali."
# privacy_title: "Privacy" privacy_title: "Confidenţialitate şi termeni"
# privacy_description: "We will not sell any of your personal information. We intend to make money through recruitment eventually, but rest assured we will not distribute your personal information to interested companies without your explicit consent." privacy_description: "Noi nu vom vinde nici o informație personală. Intenționăm să obținem profit prin recrutare eventual, dar stați liniștiți , nu vă vom vinde informațiile personale companiilor interesate fără consimțământul vostru explicit."
# security_title: "Security" security_title: "Securitate"
# security_description: "We strive to keep your personal information safe. As an open source project, our site is freely open to anyone to review and improve our security systems." security_description: "Ne străduim să vă protejăm informațiile personale. Fiind un proiect open-source, site-ul nostru oferă oricui posibilitatea de a ne revizui și îmbunătăți sistemul de securitate."
# email_title: "Email" email_title: "Email"
# email_description_prefix: "We will not inundate you with spam. Through" email_description_prefix: "Noi nu vă vom inunda cu spam. Prin"
# email_settings_url: "your email settings" email_settings_url: "setările tale de email"
# email_description_suffix: "or through links in the emails we send, you can change your preferences and easily unsubscribe at any time." email_description_suffix: " sau prin link-urile din email-urile care vi le trimitem, puteți să schimbați preferințele și să vâ dezabonați oricând."
# cost_title: "Cost" cost_title: "Cost"
# cost_description: "Currently, CodeCombat is 100% free! One of our main goals is to keep it that way, so that as many people can play as possible, regardless of place in life. If the sky darkens, we might have to charge subscriptions or for some content, but we'd rather not. With any luck, we'll be able to sustain the company with:" cost_description: "Momentan, CodeCombat este 100% gratis! Unul dintre obiectele noastre principale este să îl menținem așa, astfel încât să poată juca cât mai mulți oameni. Dacă va fi nevoie , s-ar putea să percepem o plată pentru o pentru anumite servici,dar am prefera să nu o facem. Cu puțin noroc, vom putea susține compania cu:"
# recruitment_title: "Recruitment" recruitment_title: "Recrutare"
# recruitment_description_prefix: "Here on CodeCombat, you're going to become a powerful wizardnot just in the game, but also in real life." recruitment_description_prefix: "Aici la CodeCombat, vei deveni un vrăjitor puternic nu doar în joc , ci și în viața reală."
# url_hire_programmers: "No one can hire programmers fast enough" url_hire_programmers: "Nimeni nu poate angaja programatori destul de rapid"
# recruitment_description_suffix: "so once you've sharpened your skills and if you agree, we will demo your best coding accomplishments to the thousands of employers who are drooling for the chance to hire you. They pay us a little, they pay you" recruitment_description_suffix: "așa că odată ce ți-ai dezvoltat abilitățile și esti de acord, noi vom trimite un demo cu cele mai bune realizări ale tale către miile de angajatori care se omoară să pună mâna pe tine. Pe noi ne plătesc puțin, pe tine te vor plăti"
# recruitment_description_italic: "a lot" recruitment_description_italic: "mult"
# recruitment_description_ending: "the site remains free and everybody's happy. That's the plan." recruitment_description_ending: "site-ul rămâne gratis și toată lumea este fericită. Acesta este planul."
# copyrights_title: "Copyrights and Licenses" copyrights_title: "Drepturi de autor și licențe"
# contributor_title: "Contributor License Agreement" contributor_title: "Acord de licență Contributor"
# contributor_description_prefix: "All contributions, both on the site and on our GitHub repository, are subject to our" contributor_description_prefix: "Toți contribuitorii, atât pe site cât și pe GitHub-ul nostru, sunt supuși la"
# cla_url: "CLA" cla_url: "ALC"
# contributor_description_suffix: "to which you should agree before contributing." contributor_description_suffix: "la care trebuie să fi de accord înainte să poți contribui."
# code_title: "Code - MIT" code_title: "Code - MIT"
# code_description_prefix: "All code owned by CodeCombat or hosted on codecombat.com, both in the GitHub repository or in the codecombat.com database, is licensed under the" code_description_prefix: "Tot codul deținut de CodeCombat sau hostat pe codecombat.com, atât pe GitHub cât și în baza de date codecombat.com, este licențiată sub"
# mit_license_url: "MIT license" mit_license_url: "MIT license"
# code_description_suffix: "This includes all code in Systems and Components that are made available by CodeCombat for the purpose of creating levels." code_description_suffix: "Asta include tot codul din Systems și Components care este oferit de către CodeCombat cu scopul de a crea nivele."
# art_title: "Art/Music - Creative Commons " art_title: "Artă/Muzică - Conținut Comun "
# art_description_prefix: "All common content is available under the" art_description_prefix: "Tot conținutul creativ/artistic este valabil sub"
# cc_license_url: "Creative Commons Attribution 4.0 International License" cc_license_url: "Creative Commons Attribution 4.0 International License"
# art_description_suffix: "Common content is anything made generally available by CodeCombat for the purpose of creating Levels. This includes:" art_description_suffix: "Conținut comun este orice făcut general valabil de către CodeCombat cu scopul de a crea nivele. Asta include:"
# art_music: "Music" art_music: "Muzică"
# art_sound: "Sound" art_sound: "Sunet"
# art_artwork: "Artwork" art_artwork: "Artwork"
# art_sprites: "Sprites" art_sprites: "Sprites" #can t be translated, either suggest alternative name or must be left like this
# art_other: "Any and all other non-code creative works that are made available when creating Levels." art_other: "Orice si toate celelalte creații non-cod care sunt disponibile când se crează nivele."
# art_access: "Currently there is no universal, easy system for fetching these assets. In general, fetch them from the URLs as used by the site, contact us for assistance, or help us in extending the site to make these assets more easily accessible." art_access: "Momentan nu există nici un sistem universal,ușor pentru preluarea acestor bunuri. În general, preluați-le precum site-ul din URL-urile folosite, contactați-ne pentru asistență, sau ajutați-ne sa extindem site-ul pentru a face aceste bunuri mai ușor accesibile."
# art_paragraph_1: "For attribution, please name and link to codecombat.com near where the source is used or where appropriate for the medium. For example:" art_paragraph_1: "Pentru atribuire, vă rugăm numiți și lăsați referire link la codecombat.com unde este folosită sursa sau unde este adecvat pentru mediu. De exemplu:"
# use_list_1: "If used in a movie or another game, include codecombat.com in the credits." use_list_1: "Dacă este folosit într-un film sau alt joc, includeți codecombat.com la credite."
# use_list_2: "If used on a website, include a link near the usage, for example underneath an image, or in a general attributions page where you might also mention other Creative Commons works and open source software being used on the site. Something that's already clearly referencing CodeCombat, such as a blog post mentioning CodeCombat, does not need some separate attribution." use_list_2: "Dacă este folosit pe un site, includeți un link in apropiere, de exemplu sub o imagine, sau in pagina generală de atribuiri unde menționați și alte Bunuri Creative și software open source folosit pe site. Ceva care face referință explicit la CodeCombat, precum o postare pe un blog care menționează CodeCombat, nu trebuie să facă o atribuire separată."
# art_paragraph_2: "If the content being used is created not by CodeCombat but instead by a user of codecombat.com, attribute them instead, and follow attribution directions provided in that resource's description if there are any." art_paragraph_2: "Dacă conținutul folosit nu este creat de către CodeCombat ci de către un utilizator al codecombat.com,atunci faceți referință către ei, și urmăriți indicațiile de atribuire prevăzute în descrierea resursei dacă există."
# rights_title: "Rights Reserved" rights_title: "Drepturi rezervate"
# rights_desc: "All rights are reserved for Levels themselves. This includes" rights_desc: "Toate drepturile sunt rezervate pentru Nivele în sine. Asta include"
# rights_scripts: "Scripts" rights_scripts: "Script-uri"
# rights_unit: "Unit configuration" rights_unit: "Configurații de unități"
# rights_description: "Description" rights_description: "Descriere"
# rights_writings: "Writings" rights_writings: "Scrieri"
# rights_media: "Media (sounds, music) and any other creative content made specifically for that Level and not made generally available when creating Levels." rights_media: "Media (sunete, muzică) și orice alt conținut creativ dezvoltat special pentru acel nivel care nu este valabil în mod normal pentru creat nivele."
# rights_clarification: "To clarify, anything that is made available in the Level Editor for the purpose of making levels is under CC, whereas the content created with the Level Editor or uploaded in the course of creation of Levels is not." rights_clarification: "Pentru a clarifica, orice este valabil in Editorul de Nivele pentru scopul de a crea nivele se află sub CC,pe când conținutul creat cu Editorul de Nivele sau încărcat pentru a face nivelul nu se află." #CC stands for...?
# nutshell_title: "In a Nutshell" nutshell_title: "Pe scurt"
# nutshell_description: "Any resources we provide in the Level Editor are free to use as you like for creating Levels. But we reserve the right to restrict distribution of the Levels themselves (that are created on codecombat.com) so that they may be charged for in the future, if that's what ends up happening." nutshell_description: "Orice resurse vă punem la dispoziție în Editorul de Nivele puteți folosi liber cum vreți pentru a crea nivele. Dar ne rezervăm dreptul de a rezerva distribuția de nivele în sine (care sunt create pe codecombat.com) astfel încât să se poată percepe o taxă pentru ele pe vitor, dacă se va ajunge la așa ceva."
# canonical: "The English version of this document is the definitive, canonical version. If there are any discrepencies between translations, the English document takes precedence." canonical: "The English version of this document is the definitive, canonical version. If there are any discrepencies between translations, the English document takes precedence."
# contribute: # contribute:
# page_title: "Contributing" # page_title: "Contributing"

View file

@ -21,6 +21,11 @@ div#columns.row
span.ranked.hidden Submitted for Ranking span.ranked.hidden Submitted for Ranking
span.failed.hidden Failed to Rank span.failed.hidden Failed to Rank
if team.chartData
tr
th(colspan=4, style="color: #{team.primaryColor}")
img(src="https://chart.googleapis.com/chart?chs=450x125&cht=lxy&chco=#{team.chartColor}&chtt=Score%3A+#{team.currentScore}&chts=#{team.chartColor},16,c&chf=a,s,000000FF&chls=2&chm=o,#{team.chartColor},0,4&chd=t:#{team.chartData}")
tr tr
th Result th Result
th Opponent th Opponent

View file

@ -35,7 +35,7 @@ module.exports = class MyMatchesTabView extends CocoView
for session in @sessions.models for session in @sessions.models
for match in session.get('matches') or [] for match in session.get('matches') or []
opponent = match.opponents[0] opponent = match.opponents[0]
@nameMap[opponent.userID] = nameMap[opponent.userID] @nameMap[opponent.userID] ?= nameMap[opponent.userID]
@finishRendering() @finishRendering()
$.ajax('/db/user/-/names', { $.ajax('/db/user/-/names', {
@ -76,6 +76,17 @@ module.exports = class MyMatchesTabView extends CocoView
team.wins = _.filter(team.matches, {state: 'win'}).length team.wins = _.filter(team.matches, {state: 'win'}).length
team.ties = _.filter(team.matches, {state: 'tie'}).length team.ties = _.filter(team.matches, {state: 'tie'}).length
team.losses = _.filter(team.matches, {state: 'loss'}).length team.losses = _.filter(team.matches, {state: 'loss'}).length
team.scoreHistory = team.session?.get('scoreHistory')
if team.scoreHistory?.length > 1
team.currentScore = Math.round team.scoreHistory[team.scoreHistory.length - 1][1] * 100
team.chartColor = team.primaryColor.replace '#', ''
times = (s[0] for s in team.scoreHistory)
times = ((100 * (t - times[0]) / (times[times.length - 1] - times[0])).toFixed(1) for t in times)
scores = (s[1] for s in team.scoreHistory)
lowest = _.min scores
highest = _.max scores
scores = (Math.round(100 * (s - lowest) / (highest - lowest)) for s in scores)
team.chartData = times.join(',') + '|' + scores.join(',')
ctx ctx

View file

@ -65,7 +65,7 @@ module.exports = class LadderView extends RootView
return if @startsLoading return if @startsLoading
@insertSubView(@ladderTab = new LadderTabView({}, @level, @sessions)) @insertSubView(@ladderTab = new LadderTabView({}, @level, @sessions))
@insertSubView(@myMatchesTab = new MyMatchesTabView({}, @level, @sessions)) @insertSubView(@myMatchesTab = new MyMatchesTabView({}, @level, @sessions))
@refreshInterval = setInterval(@fetchSessionsAndRefreshViews.bind(@), 10000) @refreshInterval = setInterval(@fetchSessionsAndRefreshViews.bind(@), 10 * 1000)
hash = document.location.hash[1..] if document.location.hash hash = document.location.hash[1..] if document.location.hash
if hash and not (hash in ['my-matches', 'simulate', 'ladder']) if hash and not (hash in ['my-matches', 'simulate', 'ladder'])
@showPlayModal(hash) if @sessions.loaded @showPlayModal(hash) if @sessions.loaded

View file

@ -71,7 +71,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
current_directory = os.path.dirname(os.path.realpath(sys.argv[0])) current_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
allowedMongoVersions = ["v2.5.4","v2.5.5"] allowedMongoVersions = ["v2.5.4","v2.5.5","v2.6.0-rc1"]
if which("mongod") and any(i in subprocess.check_output("mongod --version",shell=True) for i in allowedMongoVersions): if which("mongod") and any(i in subprocess.check_output("mongod --version",shell=True) for i in allowedMongoVersions):
mongo_executable = "mongod" mongo_executable = "mongod"
else: else:

View file

@ -123,7 +123,9 @@ module.exports = class Handler
# Keeping it simple for now and just allowing access to the first FETCH_LIMIT results. # Keeping it simple for now and just allowing access to the first FETCH_LIMIT results.
query = {'original': mongoose.Types.ObjectId(id)} query = {'original': mongoose.Types.ObjectId(id)}
sort = {'created': -1} sort = {'created': -1}
@modelClass.find(query).limit(FETCH_LIMIT).sort(sort).exec (err, results) => selectString = 'slug name version commitMessage created' # Is this even working?
@modelClass.find(query).select(selectString).lean().limit(FETCH_LIMIT).sort(sort).exec (err, results) =>
return @sendDatabaseError(res, err) if err
for doc in results for doc in results
return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, doc) return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, doc)
res.send(results) res.send(results)

View file

@ -2,6 +2,7 @@ authentication = require('passport')
LocalStrategy = require('passport-local').Strategy LocalStrategy = require('passport-local').Strategy
User = require('../users/User') User = require('../users/User')
UserHandler = require('../users/user_handler') UserHandler = require('../users/user_handler')
LevelSession = require '../levels/sessions/LevelSession'
config = require '../../server_config' config = require '../../server_config'
errors = require '../commons/errors' errors = require '../commons/errors'
mail = require '../commons/mail' mail = require '../commons/mail'
@ -133,6 +134,16 @@ module.exports.setup = (app) ->
unless req.query.email unless req.query.email
return errors.badInput res, 'No email provided to unsubscribe.' return errors.badInput res, 'No email provided to unsubscribe.'
if req.query.session
# Unsubscribe from just one session's notifications instead.
return LevelSession.findOne({_id: req.query.session}).exec (err, session) ->
return errors.serverError res, 'Could not unsubscribe: #{req.query.session}, #{req.query.email}: #{err}' if err
session.set 'unsubscribed', true
session.save (err) ->
return errors.serverError res, 'Database failure.' if err
res.send "Unsubscribed #{req.query.email} from CodeCombat emails for #{session.levelName} #{session.team} ladder updates. Sorry to see you go! <p><a href='/play/ladder/#{session.levelID}#my-matches'>Ladder preferences</a></p>"
res.end()
User.findOne({emailLower:req.query.email.toLowerCase()}).exec (err, user) -> User.findOne({emailLower:req.query.email.toLowerCase()}).exec (err, user) ->
if not user if not user
return errors.notFound res, "No user found with email '#{req.query.email}'" return errors.notFound res, "No user found with email '#{req.query.email}'"

View file

@ -42,11 +42,11 @@ handleLadderUpdate = (req, res) ->
for daysAgo in emailDays for daysAgo in emailDays
# Get every session that was submitted in a 5-minute window after the time. # Get every session that was submitted in a 5-minute window after the time.
startTime = getTimeFromDaysAgo daysAgo startTime = getTimeFromDaysAgo daysAgo
#endTime = startTime + 5 * 60 * 1000 endTime = startTime + 5 * 60 * 1000
endTime = startTime + 1 * 60 * 60 * 1000 # Debugging: make sure there's something to send #endTime = startTime + 1.5 * 60 * 60 * 1000 # Debugging: make sure there's something to send
findParameters = {submitted: true, submitDate: {$gt: new Date(startTime), $lte: new Date(endTime)}} findParameters = {submitted: true, submitDate: {$gt: new Date(startTime), $lte: new Date(endTime)}}
# TODO: think about putting screenshots in the email # TODO: think about putting screenshots in the email
selectString = "creator team levelName levelID totalScore matches submitted submitDate numberOfWinsAndTies numberOfLosses" selectString = "creator team levelName levelID totalScore matches submitted submitDate scoreHistory"
query = LevelSession.find(findParameters) query = LevelSession.find(findParameters)
.select(selectString) .select(selectString)
.lean() .lean()
@ -63,41 +63,49 @@ sendLadderUpdateEmail = (session, daysAgo) ->
if err if err
log.error "Couldn't find user for #{session.creator} from session #{session._id}" log.error "Couldn't find user for #{session.creator} from session #{session._id}"
return return
if not user.email or not ('notification' in user.emailSubscriptions) unless user.email and ('notification' in user.emailSubscriptions) and not session.unsubscribed
log.info "Not sending email to #{user.email} #{user.name} because they only want emails about #{user.emailSubscriptions}" log.info "Not sending email to #{user.email} #{user.name} because they only want emails about #{user.emailSubscriptions} - session unsubscribed: #{session.unsubscribed}"
return return
name = if user.firstName and user.lastName then "#{user.firstName} #{user.lastName}" else user.name unless session.levelName
log.info "Not sending email to #{user.email} #{user.name} because the session had no levelName in it."
return
name = if user.firstName and user.lastName then "#{user.firstName}" else user.name
name = "Wizard" if not name or name is "Anoner" name = "Wizard" if not name or name is "Anoner"
# Fetch the most recent defeat and victory, if there are any.
# (We could look at strongest/weakest, but we'd have to fetch everyone, or denormalize more.)
matches = _.filter session.matches, (match) -> match.date >= (new Date() - 86400 * 1000 * daysAgo)
defeats = _.filter matches, (match) -> match.metrics.rank is 1 and match.opponents[0].metrics.rank is 0
victories = _.filter matches, (match) -> match.metrics.rank is 0
defeat = _.last defeats
victory = _.last victories
sendEmail = (defeatContext, victoryContext) -> sendEmail = (defeatContext, victoryContext) ->
# TODO: do something with the preferredLanguage? # TODO: do something with the preferredLanguage?
context = context =
email_id: sendwithus.templates.ladder_update_email email_id: sendwithus.templates.ladder_update_email
recipient: recipient:
#address: user.email address: user.email
address: 'nick@codecombat.com' # Debugging #address: 'nick@codecombat.com' # Debugging
name: name name: name
email_data: email_data:
name: name name: name
days_ago: daysAgo days_ago: daysAgo
wins: session.numberOfWinsAndTies wins: victories.length
losses: session.numberOfLosses losses: defeats.length
total_score: Math.round(session.totalScore * 100) total_score: Math.round(session.totalScore * 100)
team: session.team team: session.team
team_name: session.team[0].toUpperCase() + session.team.substr(1)
level_name: session.levelName level_name: session.levelName
session_id: session._id
ladder_url: "http://codecombat.com/play/ladder/#{session.levelID}#my-matches" ladder_url: "http://codecombat.com/play/ladder/#{session.levelID}#my-matches"
score_history_graph_url: getScoreHistoryGraphURL session, daysAgo
defeat: defeatContext defeat: defeatContext
victory: victoryContext victory: victoryContext
log.info "Sending ladder update email to #{context.recipient.address} with #{context.email_data.wins} wins and #{context.email_data.losses} since #{daysAgo} day(s) ago." log.info "Sending ladder update email to #{context.recipient.address} with #{context.email_data.wins} wins and #{context.email_data.losses} since #{daysAgo} day(s) ago."
sendwithus.api.send context, (err, result) -> sendwithus.api.send context, (err, result) ->
log.error "Error sending ladder update email: #{err} with result #{result}" if err log.error "Error sending ladder update email: #{err} with result #{result}" if err
# Fetch the most recent defeat and victory, if there are any.
# (We could look at strongest/weakest, but we'd have to fetch everyone, or denormalize more.)
defeats = _.filter session.matches, (match) -> match.metrics.rank is 1 and match.opponents[0].metrics.rank is 0
victories = _.filter session.matches, (match) -> match.metrics.rank is 0
defeat = _.last defeats
victory = _.last victories
urlForMatch = (match) -> urlForMatch = (match) ->
"http://codecombat.com/play/level/#{session.levelID}?team=#{session.team}&session=#{session._id}&opponent=#{match.opponents[0].sessionID}" "http://codecombat.com/play/level/#{session.levelID}?team=#{session.team}&session=#{session._id}&opponent=#{match.opponents[0].sessionID}"
@ -124,6 +132,20 @@ sendLadderUpdateEmail = (session, daysAgo) ->
else else
onFetchedDefeatedOpponent null, null onFetchedDefeatedOpponent null, null
getScoreHistoryGraphURL = (session, daysAgo) ->
# Totally duplicated in My Matches tab for now until we figure out what we're doing.
since = new Date() - 86400 * 1000 * daysAgo
scoreHistory = (s for s in session.scoreHistory ? [] when s[0] >= since)
return '' unless scoreHistory.length > 1
times = (s[0] for s in scoreHistory)
times = ((100 * (t - times[0]) / (times[times.length - 1] - times[0])).toFixed(1) for t in times)
scores = (s[1] for s in scoreHistory)
lowest = _.min scores
highest = _.max scores
scores = (Math.round(100 * (s - lowest) / (highest - lowest)) for s in scores)
currentScore = Math.round scoreHistory[scoreHistory.length - 1][1] * 100
chartData = times.join(',') + '|' + scores.join(',')
"https://chart.googleapis.com/chart?chs=600x75&cht=lxy&chtt=Score%3A+#{currentScore}&chts=222222,12,r&chf=a,s,000000FF&chls=2&chd=t:#{chartData}"
handleMailchimpWebHook = (req, res) -> handleMailchimpWebHook = (req, res) ->
post = req.body post = req.body