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"
about:
who_is_codecombat: "Cine este CodeCombat?" # I assume you meant (what)
who_is_codecombat: "Cine este CodeCombat?"
why_codecombat: "De ce CodeCombat?"
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."
@ -277,75 +277,75 @@ module.exports = nativeDescription: "limba română", englishDescription: "Roman
why_paragraph_3_center: "ci"
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_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_ending: "And hey, it's free. "
# why_ending_url: "Start wizarding now!"
# george_description: "CEO, business guy, web designer, game designer, and champion of beginning programmers everywhere."
# scott_description: "Programmer extraordinaire, software architect, kitchen wizard, and master of finances. Scott is the reasonable one."
# nick_description: "Programming wizard, eccentric motivation mage, and upside-down experimenter. Nick can do anything and chooses to build CodeCombat."
# jeremy_description: "Customer support mage, usability tester, and community organizer; you've probably already spoken with Jeremy."
# michael_description: "Programmer, sys-admin, and undergrad technical wunderkind, Michael is the person keeping our servers online."
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: "Nu uita, este totul gratis. "
why_ending_url: "Devino un vrăjitor acum!"
george_description: "CEO, business guy, web designer, game designer, și campion al programatorilor începători."
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 poate să facă orice si a ales să dezvolte CodeCombat."
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 este cel care ține serverele in picioare."
# legal:
# page_title: "Legal"
# opensource_intro: "CodeCombat is free to play and completely open source."
# opensource_description_prefix: "Check out "
# github_url: "our GitHub"
# opensource_description_center: "and help out if you like! CodeCombat is built on dozens of open source projects, and we love them. See "
# archmage_wiki_url: "our Archmage wiki"
# opensource_description_suffix: "for a list of the software that makes this game possible."
# practices_title: "Respectful Best Practices"
# practices_description: "These are our promises to you, the player, in slightly less legalese."
# privacy_title: "Privacy"
# 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."
# security_title: "Security"
# 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."
# email_title: "Email"
# email_description_prefix: "We will not inundate you with spam. Through"
# email_settings_url: "your email settings"
# email_description_suffix: "or through links in the emails we send, you can change your preferences and easily unsubscribe at any time."
# 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:"
# recruitment_title: "Recruitment"
# recruitment_description_prefix: "Here on CodeCombat, you're going to become a powerful wizardnot just in the game, but also in real life."
# url_hire_programmers: "No one can hire programmers fast enough"
# 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_italic: "a lot"
# recruitment_description_ending: "the site remains free and everybody's happy. That's the plan."
# copyrights_title: "Copyrights and Licenses"
# contributor_title: "Contributor License Agreement"
# contributor_description_prefix: "All contributions, both on the site and on our GitHub repository, are subject to our"
# cla_url: "CLA"
# contributor_description_suffix: "to which you should agree before contributing."
# 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"
# 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."
# art_title: "Art/Music - Creative Commons "
# art_description_prefix: "All common content is available under the"
# 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_music: "Music"
# art_sound: "Sound"
# art_artwork: "Artwork"
# art_sprites: "Sprites"
# art_other: "Any and all other non-code creative works that are made available when creating Levels."
# 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_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:"
# use_list_1: "If used in a movie or another game, include codecombat.com in the credits."
# 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."
# 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."
# rights_title: "Rights Reserved"
# rights_desc: "All rights are reserved for Levels themselves. This includes"
# rights_scripts: "Scripts"
# rights_unit: "Unit configuration"
# rights_description: "Description"
# rights_writings: "Writings"
# rights_media: "Media (sounds, music) and any other creative content made specifically for that Level and not made generally available when creating Levels."
# 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."
# nutshell_title: "In a Nutshell"
# 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."
# canonical: "The English version of this document is the definitive, canonical version. If there are any discrepencies between translations, the English document takes precedence."
legal:
page_title: "Aspecte Legale"
opensource_intro: "CodeCombat este free-to-play și complet open source."
opensource_description_prefix: "Vizitează "
github_url: "pagina noastră de GitHub"
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: "Archmage wiki"
opensource_description_suffix: "pentru o listă cu software-ul care face acest joc posibil."
# practices_title: "Respectful Best Practices" #not sure what you mean here? other word for /practices/?
practices_description: "Acestea sunt promisiunile noastre către tine, jucătorul, fără așa mulți termeni legali."
privacy_title: "Confidenţialitate şi termeni"
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: "Securitate"
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_description_prefix: "Noi nu vă vom inunda cu spam. Prin"
email_settings_url: "setările tale de email"
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_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: "Recrutare"
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: "Nimeni nu poate angaja programatori destul de rapid"
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: "mult"
recruitment_description_ending: "site-ul rămâne gratis și toată lumea este fericită. Acesta este planul."
copyrights_title: "Drepturi de autor și licențe"
contributor_title: "Acord de licență Contributor"
contributor_description_prefix: "Toți contribuitorii, atât pe site cât și pe GitHub-ul nostru, sunt supuși la"
cla_url: "ALC"
contributor_description_suffix: "la care trebuie să fi de accord înainte să poți contribui."
code_title: "Code - MIT"
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"
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ă/Muzică - Conținut Comun "
art_description_prefix: "Tot conținutul creativ/artistic este valabil sub"
cc_license_url: "Creative Commons Attribution 4.0 International License"
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: "Muzică"
art_sound: "Sunet"
art_artwork: "Artwork"
art_sprites: "Sprites" #can t be translated, either suggest alternative name or must be left like this
art_other: "Orice si toate celelalte creații non-cod care sunt disponibile când se crează nivele."
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: "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: "Dacă este folosit într-un film sau alt joc, includeți codecombat.com la credite."
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: "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: "Drepturi rezervate"
rights_desc: "Toate drepturile sunt rezervate pentru Nivele în sine. Asta include"
rights_scripts: "Script-uri"
rights_unit: "Configurații de unități"
rights_description: "Descriere"
rights_writings: "Scrieri"
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: "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: "Pe scurt"
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."
# contribute:
# page_title: "Contributing"

View file

@ -21,6 +21,11 @@ div#columns.row
span.ranked.hidden Submitted for Ranking
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
th Result
th Opponent

View file

@ -35,7 +35,7 @@ module.exports = class MyMatchesTabView extends CocoView
for session in @sessions.models
for match in session.get('matches') or []
opponent = match.opponents[0]
@nameMap[opponent.userID] = nameMap[opponent.userID]
@nameMap[opponent.userID] ?= nameMap[opponent.userID]
@finishRendering()
$.ajax('/db/user/-/names', {
@ -76,6 +76,17 @@ module.exports = class MyMatchesTabView extends CocoView
team.wins = _.filter(team.matches, {state: 'win'}).length
team.ties = _.filter(team.matches, {state: 'tie'}).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

View file

@ -65,7 +65,7 @@ module.exports = class LadderView extends RootView
return if @startsLoading
@insertSubView(@ladderTab = new LadderTabView({}, @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
if hash and not (hash in ['my-matches', 'simulate', 'ladder'])
@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]))
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):
mongo_executable = "mongod"
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.
query = {'original': mongoose.Types.ObjectId(id)}
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
return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, doc)
res.send(results)

View file

@ -2,6 +2,7 @@ authentication = require('passport')
LocalStrategy = require('passport-local').Strategy
User = require('../users/User')
UserHandler = require('../users/user_handler')
LevelSession = require '../levels/sessions/LevelSession'
config = require '../../server_config'
errors = require '../commons/errors'
mail = require '../commons/mail'
@ -21,16 +22,16 @@ module.exports.setup = (app) ->
if passwordReset and password.toLowerCase() is passwordReset
User.update {_id: user.get('_id')}, {passwordReset: ''}, {}, ->
return done(null, user)
hash = User.hashPassword(password)
unless user.get('passwordHash') is hash
return done(null, false, {message:'is wrong, wrong, wrong', property:'password'})
return done(null, false, {message:'is wrong, wrong, wrong', property:'password'})
return done(null, user)
)
))
app.post '/auth/spy', (req, res, next) ->
if req?.user?.isAdmin()
username = req.body.usernameLower
emailLower = req.body.emailLower
if emailLower
@ -39,19 +40,19 @@ module.exports.setup = (app) ->
query = {"nameLower":username}
else
return errors.badInput res, "You need to supply one of emailLower or username"
User.findOne query, (err, user) ->
if err? then return errors.serverError res, "There was an error finding the specified user"
unless user then return errors.badInput res, "The specified user couldn't be found"
req.logIn user, (err) ->
if err? then return errors.serverError res, "There was an error logging in with the specified"
res.send(UserHandler.formatEntity(req, user))
return res.end()
else
return errors.unauthorized res, "You must be an admin to enter espionage mode"
app.post('/auth/login', (req, res, next) ->
authentication.authenticate('local', (err, user, info) ->
return next(err) if err
@ -87,11 +88,11 @@ module.exports.setup = (app) ->
user.save((err) ->
if err
return @sendDatabaseError(res, err)
req.logIn(user, (err) ->
if err
return @sendDatabaseError(res, err)
if send
return @sendSuccess(res, user)
next() if next
@ -110,7 +111,7 @@ module.exports.setup = (app) ->
User.findOne({emailLower:req.body.email.toLowerCase()}).exec((err, user) ->
if not user
return errors.notFound(res, [{message:'not found.', property:'email'}])
user.set('passwordReset', Math.random().toString(36).slice(2,7).toUpperCase())
user.save (err) =>
return errors.serverError(res) if err
@ -127,12 +128,22 @@ module.exports.setup = (app) ->
return res.end()
)
)
app.get '/auth/unsubscribe', (req, res) ->
email = req.query.email
unless req.query.email
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) ->
if not user
return errors.notFound res, "No user found with email '#{req.query.email}'"
@ -152,4 +163,4 @@ createMailOptions = (receiver, password) ->
replyTo: config.mail.username
subject: "[CodeCombat] Password Reset"
text: "You can log into your account with: #{password}"
#
#

View file

@ -42,11 +42,11 @@ handleLadderUpdate = (req, res) ->
for daysAgo in emailDays
# Get every session that was submitted in a 5-minute window after the time.
startTime = getTimeFromDaysAgo daysAgo
#endTime = startTime + 5 * 60 * 1000
endTime = startTime + 1 * 60 * 60 * 1000 # Debugging: make sure there's something to send
endTime = startTime + 5 * 60 * 1000
#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)}}
# 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)
.select(selectString)
.lean()
@ -63,41 +63,49 @@ sendLadderUpdateEmail = (session, daysAgo) ->
if err
log.error "Couldn't find user for #{session.creator} from session #{session._id}"
return
if not user.email or not ('notification' in user.emailSubscriptions)
log.info "Not sending email to #{user.email} #{user.name} because they only want emails about #{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} - session unsubscribed: #{session.unsubscribed}"
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"
# 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) ->
# TODO: do something with the preferredLanguage?
context =
email_id: sendwithus.templates.ladder_update_email
recipient:
#address: user.email
address: 'nick@codecombat.com' # Debugging
address: user.email
#address: 'nick@codecombat.com' # Debugging
name: name
email_data:
name: name
days_ago: daysAgo
wins: session.numberOfWinsAndTies
losses: session.numberOfLosses
wins: victories.length
losses: defeats.length
total_score: Math.round(session.totalScore * 100)
team: session.team
team_name: session.team[0].toUpperCase() + session.team.substr(1)
level_name: session.levelName
session_id: session._id
ladder_url: "http://codecombat.com/play/ladder/#{session.levelID}#my-matches"
score_history_graph_url: getScoreHistoryGraphURL session, daysAgo
defeat: defeatContext
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."
sendwithus.api.send context, (err, result) ->
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) ->
"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
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) ->
post = req.body