diff --git a/app/locale/ro.coffee b/app/locale/ro.coffee index e9d7e3db6..5038a79d8 100644 --- a/app/locale/ro.coffee +++ b/app/locale/ro.coffee @@ -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 wizard–not 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" diff --git a/app/templates/play/ladder/my_matches_tab.jade b/app/templates/play/ladder/my_matches_tab.jade index c70192221..0eb83dddb 100644 --- a/app/templates/play/ladder/my_matches_tab.jade +++ b/app/templates/play/ladder/my_matches_tab.jade @@ -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 diff --git a/app/views/play/ladder/my_matches_tab.coffee b/app/views/play/ladder/my_matches_tab.coffee index a094aef82..660114111 100644 --- a/app/views/play/ladder/my_matches_tab.coffee +++ b/app/views/play/ladder/my_matches_tab.coffee @@ -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 diff --git a/app/views/play/ladder_view.coffee b/app/views/play/ladder_view.coffee index 11283af13..b7cf49599 100644 --- a/app/views/play/ladder_view.coffee +++ b/app/views/play/ladder_view.coffee @@ -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 diff --git a/bin/coco-mongodb b/bin/coco-mongodb index be4f285ed..4ff889493 100755 --- a/bin/coco-mongodb +++ b/bin/coco-mongodb @@ -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: diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index ada7acad3..4460b2d22 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -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) diff --git a/server/routes/auth.coffee b/server/routes/auth.coffee index 2e6dbf72d..c845b28c2 100644 --- a/server/routes/auth.coffee +++ b/server/routes/auth.coffee @@ -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!

Ladder preferences

" + 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}" -# \ No newline at end of file +# diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 4ef488b67..5ad2dfcd4 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -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