From 013ace65f6e050b7d72fddb758d808ef2622c4a7 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 9 Jul 2014 16:24:14 -0700 Subject: [PATCH 01/21] Basic email structure plus distributed locking system --- app/schemas/models/mail_sent.coffee | 16 +++++++++++++++ app/schemas/models/mail_task.coffee | 21 +++++++++++++++++++ server/commons/LockManager.coffee | 24 ++++++++++++++++++++++ server/commons/mapping.coffee | 2 ++ server/mail/sent/MailSent.coffee | 11 ++++++++++ server/mail/sent/mail_sent_handler.coffee | 12 +++++++++++ server/mail/tasks/MailTask.coffee | 7 +++++++ server/mail/tasks/mail_task_handler.coffee | 12 +++++++++++ server/routes/mail.coffee | 20 +++++++++++++++++- server_config.coffee | 4 ++++ 10 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 app/schemas/models/mail_sent.coffee create mode 100644 app/schemas/models/mail_task.coffee create mode 100644 server/commons/LockManager.coffee create mode 100644 server/mail/sent/MailSent.coffee create mode 100644 server/mail/sent/mail_sent_handler.coffee create mode 100644 server/mail/tasks/MailTask.coffee create mode 100644 server/mail/tasks/mail_task_handler.coffee diff --git a/app/schemas/models/mail_sent.coffee b/app/schemas/models/mail_sent.coffee new file mode 100644 index 000000000..4236ad94b --- /dev/null +++ b/app/schemas/models/mail_sent.coffee @@ -0,0 +1,16 @@ +c = require './../schemas' +#This will represent transactional emails which have been sent + +MailSentSchema = c.object { + title: 'Sent mail' + description: 'Emails which have been sent through the system' +} +_.extend MailSentSchema.properties, + mailTask: c.objectId {} + user: c.objectId links: [{rel: 'extra', href: '/db/user/{($)}'}] + sent: c.date title: 'Sent', readOnly: true + +c.extendBasicProperties MailSentSchema, 'mail.sent' + +module.exports = MailSentSchema + \ No newline at end of file diff --git a/app/schemas/models/mail_task.coffee b/app/schemas/models/mail_task.coffee new file mode 100644 index 000000000..ac450e0ee --- /dev/null +++ b/app/schemas/models/mail_task.coffee @@ -0,0 +1,21 @@ +c = require './../schemas' +#This will represent transactional emails which have been sent + +MailTaskSchema = c.object { + title: 'Mail task' + description: 'Mail tasks to call at certain intervals' +} +_.extend MailTaskSchema.properties, + url: + title: 'URL' + description: 'The associated URL of the mailtask to call' + type: 'string' + frequency: + title: 'Frequency' + description: 'The number of seconds the servers should check whether or not to send the email' + type: 'integer' + +c.extendBasicProperties MailTaskSchema, 'mail.task' + +module.exports = MailTaskSchema + \ No newline at end of file diff --git a/server/commons/LockManager.coffee b/server/commons/LockManager.coffee new file mode 100644 index 000000000..83dd43efc --- /dev/null +++ b/server/commons/LockManager.coffee @@ -0,0 +1,24 @@ +config = require '../../server_config' +redis = require 'redis' +class LockManager + constructor: -> + unless config.isProduction or config.redis.host isnt "localhost" + throw "You shouldn't be instantiating distributed locks unless in production." + @redisClient = redis.createClient config.redis.port, config.redis.host + @lockValues = {} + @unlockScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end" + + setLock: (lockName, timeoutMs, cb) => + randomNumber = Math.floor(Math.random() * 1000000000) + @redisClient.set [lockName,randomNumber, "NX", "PX", timeoutMs], (err, res) -> + if err? then return cb err, null + @lockValues[lockName] = randomNumber + cb null, res + + releaseLock: (lockName, cb) => + @redisClient.eval [@unlockScript, 1, lockName, @lockValues[lockName]], (err, res) -> + if err? then return cb err, null + #1 represents success, 0 failure + cb null, Boolean(Number(res)) + +module.exports = new RedisLock() \ No newline at end of file diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index 802b33790..faa0ef551 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -9,6 +9,8 @@ module.exports.handlers = 'thang_type': 'levels/thangs/thang_type_handler' 'user': 'users/user_handler' 'user_remark': 'users/remarks/user_remark_handler' + 'mail_task': 'mail/tasks/mail_task_handler' + 'mail_sent': 'mail/sent/mail_sent_handler' 'achievement': 'achievements/achievement_handler' 'earned_achievement': 'achievements/earned_achievement_handler' diff --git a/server/mail/sent/MailSent.coffee b/server/mail/sent/MailSent.coffee new file mode 100644 index 000000000..7f59dd5b0 --- /dev/null +++ b/server/mail/sent/MailSent.coffee @@ -0,0 +1,11 @@ +mongoose = require 'mongoose' +plugins = require '../../plugins/plugins' +jsonschema = require '../../../app/schemas/models/mail_sent' + +MailSent = new mongoose.Schema({ + sent: + type: Date + 'default': Date.now +}, {strict: false}) + +module.exports = MailSent = mongoose.model('mailSent', MailSent) diff --git a/server/mail/sent/mail_sent_handler.coffee b/server/mail/sent/mail_sent_handler.coffee new file mode 100644 index 000000000..35e920c2b --- /dev/null +++ b/server/mail/sent/mail_sent_handler.coffee @@ -0,0 +1,12 @@ +MailSent = require './MailSent' +Handler = require '../../commons/Handler' + +class MailSentHandler extends Handler + modelClass: MailSent + editableProperties: ['mailTask','user','sent'] + jsonSchema: require '../../../app/schemas/models/mail_sent' + + hasAccess: (req) -> + req.user?.isAdmin() + +module.exports = new MailSentHandler() diff --git a/server/mail/tasks/MailTask.coffee b/server/mail/tasks/MailTask.coffee new file mode 100644 index 000000000..10a536787 --- /dev/null +++ b/server/mail/tasks/MailTask.coffee @@ -0,0 +1,7 @@ +mongoose = require 'mongoose' +plugins = require '../../plugins/plugins' +jsonschema = require '../../../app/schemas/models/mail_task' + +MailTaskSchema = new mongoose.Schema({}, {strict: false}) + +module.exports = MailTask = mongoose.model('mail.task', MailTaskSchema) diff --git a/server/mail/tasks/mail_task_handler.coffee b/server/mail/tasks/mail_task_handler.coffee new file mode 100644 index 000000000..12b6be53a --- /dev/null +++ b/server/mail/tasks/mail_task_handler.coffee @@ -0,0 +1,12 @@ +MailTask = require './MailTask' +Handler = require '../../commons/Handler' + +class MailTaskHandler extends Handler + modelClass: MailTask + editableProperties: ['url','frequency'] + jsonSchema: require '../../../app/schemas/models/mail_task' + + hasAccess: (req) -> + req.user?.isAdmin() + +module.exports = new MailTaskHandler() diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 4fdcef128..ae8b62e86 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -1,4 +1,5 @@ mail = require '../commons/mail' +MailTask = require '../mail/tasks/MailTask' User = require '../users/User' errors = require '../commons/errors' config = require '../../server_config' @@ -6,11 +7,16 @@ LevelSession = require '../levels/sessions/LevelSession' Level = require '../levels/Level' log = require 'winston' sendwithus = require '../sendwithus' - +if config.isProduction || true + redis = require 'redis' + redisClient = redis.createClient(config.redis.port,config.redis.host) module.exports.setup = (app) -> app.all config.mail.mailchimpWebhook, handleMailchimpWebHook app.get '/mail/cron/ladder-update', handleLadderUpdate + app.post '/mail/task', createMailTask + + #setInterval(handleScheduledMail, 5000) DEBUGGING = false @@ -28,6 +34,18 @@ isRequestFromDesignatedCronHandler = (req, res) -> return false return true +createMailTask = (req, res) -> + #unless req.user?.isAdmin() then return errors.forbidden(res) + unless req.body.url and req.body.frequency then return errors.badInput(res) + console.log "Creating mail task with url #{req.body.url} and frequency #{req.body.frequency}" + newMailTask = new MailTask {} + newMailTask.set("url",req.body.url) + newMailTask.set("frequency",req.body.frequency) + newMailTask.save (err) -> + if err? then return errors.serverError(res, err) + res.send("Created mail task!") + res.end() + handleLadderUpdate = (req, res) -> log.info('Going to see about sending ladder update emails.') requestIsFromDesignatedCronHandler = isRequestFromDesignatedCronHandler req, res diff --git a/server_config.coffee b/server_config.coffee index 6018bf84f..13fc4e7d1 100644 --- a/server_config.coffee +++ b/server_config.coffee @@ -12,6 +12,10 @@ config.mongo = host: process.env.COCO_MONGO_HOST or 'localhost' db: process.env.COCO_MONGO_DATABASE_NAME or 'coco' mongoose_replica_string: process.env.COCO_MONGO_MONGOOSE_REPLICA_STRING or '' + +config.redis = + port: process.env.COCO_REDIS_PORT or 6379 + host: process.env.COCO_REDIS_HOST or 'localhost' if config.unittest config.port += 1 From b74bdfbf51e000af18c5537835a49c898b5030b7 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 12:12:11 -0700 Subject: [PATCH 02/21] Added metadata to mail sent schema --- app/schemas/models/mail_sent.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/schemas/models/mail_sent.coffee b/app/schemas/models/mail_sent.coffee index 4236ad94b..4cd54e241 100644 --- a/app/schemas/models/mail_sent.coffee +++ b/app/schemas/models/mail_sent.coffee @@ -9,6 +9,7 @@ _.extend MailSentSchema.properties, mailTask: c.objectId {} user: c.objectId links: [{rel: 'extra', href: '/db/user/{($)}'}] sent: c.date title: 'Sent', readOnly: true + metadata: c.object {}, {} c.extendBasicProperties MailSentSchema, 'mail.sent' From 3ef5d88e2ac4f07500b0a5cd1e9616c01512d8bf Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 12:12:25 -0700 Subject: [PATCH 03/21] Updated sendwithus to fix memory leak --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cba6e383e..21231684c 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "express-useragent": "~0.0.9", "gridfs-stream": "0.4.x", "stream-buffers": "0.2.x", - "sendwithus": "2.0.x", + "sendwithus": "2.1.x", "aws-sdk": "~2.0.0", "bayesian-battle": "0.0.x", "redis": "", From 449f1e1afcb0e1a7f6ac4b3386b78822d9c1adf6 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 12:12:44 -0700 Subject: [PATCH 04/21] Updated locking/release procedure --- server/commons/LockManager.coffee | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/server/commons/LockManager.coffee b/server/commons/LockManager.coffee index 83dd43efc..56604381b 100644 --- a/server/commons/LockManager.coffee +++ b/server/commons/LockManager.coffee @@ -1,5 +1,6 @@ config = require '../../server_config' redis = require 'redis' + class LockManager constructor: -> unless config.isProduction or config.redis.host isnt "localhost" @@ -10,15 +11,20 @@ class LockManager setLock: (lockName, timeoutMs, cb) => randomNumber = Math.floor(Math.random() * 1000000000) - @redisClient.set [lockName,randomNumber, "NX", "PX", timeoutMs], (err, res) -> + @redisClient.set [lockName,randomNumber, "NX", "PX", timeoutMs], (err, res) => if err? then return cb err, null - @lockValues[lockName] = randomNumber - cb null, res + if res is "OK" + @lockValues[lockName] = randomNumber + return cb null, "Lock set!" + unless res + return cb "Lock already set!", null releaseLock: (lockName, cb) => @redisClient.eval [@unlockScript, 1, lockName, @lockValues[lockName]], (err, res) -> if err? then return cb err, null - #1 represents success, 0 failure - cb null, Boolean(Number(res)) - -module.exports = new RedisLock() \ No newline at end of file + if res + cb null, "The lock was released!" + else + cb "The lock was not released.", null + +module.exports = new LockManager() From 3a405e22f3ee2efe31f7a4f0b22e7c7bbcdc9de7 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 12:12:54 -0700 Subject: [PATCH 05/21] Changed name of mail sent collection --- server/mail/sent/MailSent.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/mail/sent/MailSent.coffee b/server/mail/sent/MailSent.coffee index 7f59dd5b0..f8479033a 100644 --- a/server/mail/sent/MailSent.coffee +++ b/server/mail/sent/MailSent.coffee @@ -8,4 +8,4 @@ MailSent = new mongoose.Schema({ 'default': Date.now }, {strict: false}) -module.exports = MailSent = mongoose.model('mailSent', MailSent) +module.exports = MailSent = mongoose.model('mail.sent', MailSent) From e218c2297db205a02cd701d97b37ff738ab347aa Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 12:13:21 -0700 Subject: [PATCH 06/21] Some emails working --- server/routes/mail.coffee | 201 +++++++++++++++++++++++++++++++++++++- 1 file changed, 196 insertions(+), 5 deletions(-) diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index ae8b62e86..784f4c099 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -1,23 +1,213 @@ mail = require '../commons/mail' MailTask = require '../mail/tasks/MailTask' +MailSent = require '../mail/sent/MailSent' User = require '../users/User' +async = require 'async' errors = require '../commons/errors' config = require '../../server_config' LevelSession = require '../levels/sessions/LevelSession' Level = require '../levels/Level' log = require 'winston' sendwithus = require '../sendwithus' -if config.isProduction || true - redis = require 'redis' - redisClient = redis.createClient(config.redis.port,config.redis.host) +if config.isProduction or config.redis.host isnt "localhost" + lockManager = require '../commons/LockManager' + module.exports.setup = (app) -> app.all config.mail.mailchimpWebhook, handleMailchimpWebHook app.get '/mail/cron/ladder-update', handleLadderUpdate app.post '/mail/task', createMailTask - - #setInterval(handleScheduledMail, 5000) + if lockManager + setupScheduledEmails() +setupScheduledEmails = -> + testForLockManager() + mailTaskMap = + "test_mail_task": candidateUpdateProfileTask + + MailTask.find({}).lean().exec (err, mailTasks) -> + if err? then throw "Failed to schedule mailTasks! #{err}" + for mailTask in mailTasks + setInterval mailTaskMap[mailTask.url], mailTask.frequency*2 + +testForLockManager = -> unless lockManager then throw "The system isn't configured to do distributed locking!" + +### Candidate Update Reminder Task ### +emailTimeRange = (timeRange, finalCallback) -> + waterfallContext = + "timeRange": timeRange + "mailTaskName": @mailTaskName + async.waterfall [ + findAllCandidatesWithinTimeRange.bind(waterfallContext) + (unfilteredCandidates, cb) -> #now filter the candidates to see if they are eligible + async.reject unfilteredCandidates, candidateFilter.bind(waterfallContext), (filtered) -> cb null, filtered + (filteredCandidates, cb) -> #Now send emails to the eligible candidates and record. + async.each filteredCandidates, sendReminderEmailToCandidate.bind(waterfallContext), cb + ], finalCallback + +findAllCandidatesWithinTimeRange = (cb) -> + findParameters = + "jobProfile.updated": + $gt: @timeRange.start + $lte: @timeRange.end + selection = "_id email jobProfile.name jobProfile.updated" + User.find(findParameters).select(selection).lean().exec cb + +candidateFilter = (candidate, sentEmailFilterCallback) -> + findParameters = + "user": candidate._id + "mailTask": @mailTaskName + "metadata.timeRangeName": @timeRange.name + "metadata.updated": candidate.jobProfile.updated + MailSent.find(findParameters).lean().exec (err, sentMail) -> + if err? then return errors.serverError("Error fetching sent mail in email task") + sentEmailFilterCallback Boolean(sentMail.length) + +findEmployersSignedUpAfterDate = (dateObject, cb) -> + countParameters = + $or: [{"dateCreated": {$gte: dateObject}},{"signedEmployerAgreement":{$gte: dateObject}}] + employerAt: {$exists: true} + permissions: "employer" + User.count countParameters, cb + +sendReminderEmailToCandidate = (candidate, sendEmailCallback) -> + findEmployersSignedUpAfterDate new Date(candidate.jobProfile.updated), (err, employersAfterCount) => + context = + email_id: "tem_CtTLsKQufxrxoPMn7upKiL" + recipient: + address: candidate.email + name: candidate.jobProfile.name + email_data: + profile_updated: candidate.jobProfile.updated #format nicely + new_company: employersAfterCount + log.info "Sending #{@timeRange.name} update reminder to #{context.recipient.name}(#{context.recipient.address})" + newSentMail = + mailTask: @mailTaskName + user: candidate._id + metadata: + timeRangeName: @timeRange.name + updated: candidate.jobProfile.updated + MailSent.create newSentMail, (err) -> + if err? then return sendEmailCallback err + sendwithus.api.send context, (err, result) -> + log.error "Error sending ladder update email: #{err} with result #{result}" if err + sendEmailCallback err + +generateWeekOffset = (originalDate, numberOfWeeks) -> + return (new Date(originalDate.getTime() - numberOfWeeks * 7 * 24 * 60 * 60 * 1000)).toISOString() + +candidateUpdateProfileTask = -> + mailTaskName = "candidateUpdateProfileTask" + lockDurationMs = 6000 + currentDate = new Date() + timeRanges = [] + for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [8, 52, 'eight weeks']] + timeRanges.push + start: generateWeekOffset currentDate, weekPair[0] + end: generateWeekOffset currentDate, weekPair[1] + name: weekPair[2] + lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> + if err? then return log.error "Error getting a task lock!" + async.each timeRanges, emailTimeRange.bind(mailTaskName: mailTaskName), (err) -> + if err then return log.error JSON.stringify err else log.info "Sent candidate update reminders!" + lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err + +### End Candidate Update Reminder Task ### +### Internal Candidate Update Reminder Email ### + +emailInternalCandidateUpdateReminder = (cb) -> + currentTime = new Date() + beginningOfUTCDay = new Date() + beginningOfUTCDay.setUTCHours(0,0,0,0) + asyncContext = + "beginningOfUTCDay": beginningOfUTCDay + "currentTime": currentTime + "mailTaskName": @mailTaskName + + async.waterfall [ + findCandidatesWhoUpdatedJobProfileToday.bind(asyncContext) + (unfilteredCandidates, cb) -> + async.reject unfilteredCandidates, candidatesUpdatedTodayFilter.bind(asyncContext), cb.bind(null,null) + (filteredCandidates, cb) -> + async.each filteredCandidates, sendInternalCandidateUpdateReminder.bind(asyncContext), cb + ], cb + +findCandidatesWhoUpdatedJobProfileToday = (cb) -> + findParameters = + "jobProfile.updated": + $lte: @currentTime.toISOString() + gt: @beginningOfUTCDay.toISOString() + User.find(findParameters).select("_id jobProfile.name jobProfile.updated").lean().exec cb + +candidatesUpdatedTodayFilter = (candidate, cb) -> + findParameters = + "user": candidate._id + "mailTask": @mailTaskName + "metadata.beginningOfUTCDay": @beginningOfUTCDay + MailSent.find(findParameters).lean().exec (err, sentMail) -> + if err? then return errors.serverError("Error fetching sent mail in #{@mailTaskName}") + cb Boolean(sentMail.length) + +sendInternalCandidateUpdateReminder = (candidate, cb) -> + context = + email_id: "tem_Ac7nhgKqatTHBCgDgjF5pE" + recipient: + address: "team@codecombat.com" #Change to whatever email address is necessary + name: "The CodeCombat Team" + log.info "Sending candidate updated reminder for #{candidate.jobProfile.name}" + + newSentMail = + mailTask: @mailTaskName + user: candidate._id + metadata: + beginningOfUTCDay: @beginningOfUTCDay + + MailSent.create newSentMail, (err) -> + if err? then return cb err + sendwithus.api.send context, (err, result) -> + log.error "Error sending ladder update email: #{err} with result #{result}" if err + cb err + +internalCandidateUpdateTask = -> + mailTaskName = "internalCandidateUpdateTask" + lockDurationMs = 6000 + lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> + if err? then return log.error "Error getting a task lock!" + emailInternalCandidateUpdateReminder.apply {"mailTaskName":mailTaskName}, (err) -> + if err? then return log.error "There was an error sending the internal candidate update reminder." + lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err +### End Internal Candidate Update Reminder Email ### + +### Employer New Candidates Available Email ### +employerNewCandidatesAvailableTask = -> + # tem_CCcHKr95Nvu5bT7c7iHCtm + #initialize featuredDate to job profile updated + mailTaskName = "employerNewCandidatesAvailableTask" + lockDurationMs = 6000 + lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> + +### End Employer New Candidates Available Email ### + +### New Recruit Leaderboard Email ### +newRecruitLeaderboardEmailTask = -> + # tem_kMQFCKX3v4DNAQDsMAsPJC + #maxRank and maxRankTime should be recorded if isSimulating is false + mailTaskName = "newRecruitLeaderboardEmailTask" + lockDurationMs = 6000 + lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> + +### End New Recruit Leaderboard Email ### + +### Employer Matching Candidate Notification Email ### +employerMatchingCandidateNotificationTask = -> + # tem_mYsepTfWQ265noKfZJcbBH + #save email filters in their own collection + mailTaskName = "employerMatchingCandidateNotificationTask" + lockDurationMs = 6000 + lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> + +### End Employer Matching Candidate Notification Email ### +### Ladder Update Email ### DEBUGGING = false LADDER_PREGAME_INTERVAL = 2 * 3600 * 1000 # Send emails two hours before players last submitted. @@ -169,6 +359,7 @@ getScoreHistoryGraphURL = (session, daysAgo) -> 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}&chxt=y&chxr=0,#{minScore},#{maxScore}" +### End Ladder Update Email ### handleMailchimpWebHook = (req, res) -> post = req.body From 67f2eeb584eda3232d74432b5639cf0512c8a820 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 13:14:09 -0700 Subject: [PATCH 07/21] Progress on employer new candidates available email --- server/routes/mail.coffee | 64 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 784f4c099..dd09a5d02 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -23,7 +23,7 @@ module.exports.setup = (app) -> setupScheduledEmails = -> testForLockManager() mailTaskMap = - "test_mail_task": candidateUpdateProfileTask + "test_mail_task": employerNewCandidatesAvailableTask MailTask.find({}).lean().exec (err, mailTasks) -> if err? then throw "Failed to schedule mailTasks! #{err}" @@ -50,6 +50,7 @@ findAllCandidatesWithinTimeRange = (cb) -> "jobProfile.updated": $gt: @timeRange.start $lte: @timeRange.end + "jobProfileApproved": true selection = "_id email jobProfile.name jobProfile.updated" User.find(findParameters).select(selection).lean().exec cb @@ -109,7 +110,7 @@ candidateUpdateProfileTask = -> lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> if err? then return log.error "Error getting a task lock!" async.each timeRanges, emailTimeRange.bind(mailTaskName: mailTaskName), (err) -> - if err then return log.error JSON.stringify err else log.info "Sent candidate update reminders!" + if err then log.error JSON.stringify err else log.info "Sent candidate update reminders!" lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err ### End Candidate Update Reminder Task ### @@ -125,18 +126,19 @@ emailInternalCandidateUpdateReminder = (cb) -> "mailTaskName": @mailTaskName async.waterfall [ - findCandidatesWhoUpdatedJobProfileToday.bind(asyncContext) + findNonApprovedCandidatesWhoUpdatedJobProfileToday.bind(asyncContext) (unfilteredCandidates, cb) -> async.reject unfilteredCandidates, candidatesUpdatedTodayFilter.bind(asyncContext), cb.bind(null,null) (filteredCandidates, cb) -> async.each filteredCandidates, sendInternalCandidateUpdateReminder.bind(asyncContext), cb ], cb -findCandidatesWhoUpdatedJobProfileToday = (cb) -> +findNonApprovedCandidatesWhoUpdatedJobProfileToday = (cb) -> findParameters = "jobProfile.updated": $lte: @currentTime.toISOString() gt: @beginningOfUTCDay.toISOString() + "jobProfileApproved": false User.find(findParameters).select("_id jobProfile.name jobProfile.updated").lean().exec cb candidatesUpdatedTodayFilter = (candidate, cb) -> @@ -174,17 +176,69 @@ internalCandidateUpdateTask = -> lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> if err? then return log.error "Error getting a task lock!" emailInternalCandidateUpdateReminder.apply {"mailTaskName":mailTaskName}, (err) -> - if err? then return log.error "There was an error sending the internal candidate update reminder." + if err? then log.error "There was an error sending the internal candidate update reminder." lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err ### End Internal Candidate Update Reminder Email ### ### Employer New Candidates Available Email ### + +emailEmployerNewCandidatesAvailableEmail = (cb) -> + currentTime = new Date() + asyncContext = + "currentTime": currentTime + "mailTaskName": @mailTaskName + + async.waterfall [ + findAllEmployers + makeEmployerNamesEasilyAccessible + (allEmployers, cb) -> + console.log "Found #{allEmployers.length} employers to email about new candidates available" + async.reject allEmployers, employersEmailedDigestMoreThanWeekAgoFilter.bind(asyncContext), cb.bind(null,null) + (employersToEmail, cb) -> + async.each employersToEmail, sendEmployerNewCandidatesAvailableEmail.bind(asyncContext), cb + ], cb + +findAllEmployers = (cb) -> + findParameters = + "employerAt": + $exists: true + permissions: "employer" + User.find(findParameters).select("_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName").lean().exec cb + +makeEmployerNamesEasilyAccessible = (allEmployers, cb) -> + #Make names easily accessible + for employer, index in allEmployers + if employer.signedEmployerAgreement?.data?.firstName + employer.name = employer.signedEmployerAgreement.data.firstName + " " + employer.signedEmployerAgreement.data.lastName + delete employer.signedEmployerAgreement + allEmployers[index] = employer + cb null, allEmployers + +employersEmailedDigestMoreThanWeekAgoFilter = (employer, cb) -> + findParameters = + "user": employer._id + "mailTask": @mailTaskName + "sent": + $lte: new Date(@currentTime.getTime() - 7 * 24 * 60 * 60 * 1000) + MailSent.find(findParameters).lean().exec (err, sentMail) -> + if err? then return errors.serverError("Error fetching sent mail in #{@mailTaskName}") + cb Boolean(sentMail.length) + +sendEmployerNewCandidatesAvailableEmail = (employer, cb) -> + cb null + + employerNewCandidatesAvailableTask = -> # tem_CCcHKr95Nvu5bT7c7iHCtm #initialize featuredDate to job profile updated mailTaskName = "employerNewCandidatesAvailableTask" lockDurationMs = 6000 lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> + if err? then return log.error "There was an error getting a task lock!" + emailEmployerNewCandidatesAvailableEmail.apply {"mailTaskName":mailTaskName}, (err) -> + if err? then return log.error "There was an error performing the #{mailTaskName} email task." + lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err + ### End Employer New Candidates Available Email ### From 3c257a488df971e0ad1e3b6d7167984e838a0be7 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 13:54:05 -0700 Subject: [PATCH 08/21] Employer candidates available email completed --- server/routes/mail.coffee | 43 ++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index dd09a5d02..ee1728f1f 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -203,7 +203,8 @@ findAllEmployers = (cb) -> "employerAt": $exists: true permissions: "employer" - User.find(findParameters).select("_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName").lean().exec cb + selection = "_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName activity dateCreated" + User.find(findParameters).select(selection).lean().exec cb makeEmployerNamesEasilyAccessible = (allEmployers, cb) -> #Make names easily accessible @@ -219,17 +220,49 @@ employersEmailedDigestMoreThanWeekAgoFilter = (employer, cb) -> "user": employer._id "mailTask": @mailTaskName "sent": - $lte: new Date(@currentTime.getTime() - 7 * 24 * 60 * 60 * 1000) + $gt: new Date(@currentTime.getTime() - 7 * 24 * 60 * 60 * 1000) MailSent.find(findParameters).lean().exec (err, sentMail) -> if err? then return errors.serverError("Error fetching sent mail in #{@mailTaskName}") cb Boolean(sentMail.length) sendEmployerNewCandidatesAvailableEmail = (employer, cb) -> - cb null - + lastLoginDate = employer.activity?.login?.last ? employer.dateCreated + countParameters = + "jobProfileApproved": true + "jobProfile": + $exists: true + $or: [ + jobProfileApprovedDate: + $gt: lastLoginDate.toISOString() + , + jobProfileApprovedDate: + $exists: false + "jobProfile.updated": + $gt: lastLoginDate.toISOString() + ] + User.count countParameters, (err, numberOfCandidatesSinceLogin) => + if err? then return cb err + context = + email_id: "tem_CCcHKr95Nvu5bT7c7iHCtm" + recipient: + address: employer.email + name: employer.name + email_data: + new_candidates: numberOfCandidatesSinceLogin + employer_company_name: employer.employerAt + company_name: "CodeCombat" + + log.info "Sending available candidates update reminder to #{context.recipient.name}(#{context.recipient.address})" + newSentMail = + mailTask: @mailTaskName + user: employer._id + MailSent.create newSentMail, (err) -> + if err? then return cb err + sendwithus.api.send context, (err, result) -> + log.error "Error sending ladder update email: #{err} with result #{result}" if err + cb err employerNewCandidatesAvailableTask = -> - # tem_CCcHKr95Nvu5bT7c7iHCtm #initialize featuredDate to job profile updated mailTaskName = "employerNewCandidatesAvailableTask" lockDurationMs = 6000 From 5c0c25ebd1235c9a225862496b40a031c966d21d Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 13:56:23 -0700 Subject: [PATCH 09/21] Async must not break upon sendwithus failure --- server/routes/mail.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index ee1728f1f..8983a7224 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -91,8 +91,8 @@ sendReminderEmailToCandidate = (candidate, sendEmailCallback) -> MailSent.create newSentMail, (err) -> if err? then return sendEmailCallback err sendwithus.api.send context, (err, result) -> - log.error "Error sending ladder update email: #{err} with result #{result}" if err - sendEmailCallback err + log.error "Error sending candidate update reminder email: #{err} with result #{result}" if err + sendEmailCallback null generateWeekOffset = (originalDate, numberOfWeeks) -> return (new Date(originalDate.getTime() - numberOfWeeks * 7 * 24 * 60 * 60 * 1000)).toISOString() @@ -167,8 +167,8 @@ sendInternalCandidateUpdateReminder = (candidate, cb) -> MailSent.create newSentMail, (err) -> if err? then return cb err sendwithus.api.send context, (err, result) -> - log.error "Error sending ladder update email: #{err} with result #{result}" if err - cb err + log.error "Error sending interal candidate update email: #{err} with result #{result}" if err + cb null internalCandidateUpdateTask = -> mailTaskName = "internalCandidateUpdateTask" @@ -259,8 +259,8 @@ sendEmployerNewCandidatesAvailableEmail = (employer, cb) -> MailSent.create newSentMail, (err) -> if err? then return cb err sendwithus.api.send context, (err, result) -> - log.error "Error sending ladder update email: #{err} with result #{result}" if err - cb err + log.error "Error sending employer candidates available email: #{err} with result #{result}" if err + cb null employerNewCandidatesAvailableTask = -> #initialize featuredDate to job profile updated From 59a35c844fd0e04de3a9c75d9ea8dd53d7adc69a Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 14:29:30 -0700 Subject: [PATCH 10/21] Made mail system resilient to redis failures --- server/commons/LockManager.coffee | 12 ++++++++++++ server/routes/mail.coffee | 8 +++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/server/commons/LockManager.coffee b/server/commons/LockManager.coffee index 56604381b..bdd847bf7 100644 --- a/server/commons/LockManager.coffee +++ b/server/commons/LockManager.coffee @@ -1,15 +1,26 @@ config = require '../../server_config' redis = require 'redis' +log = require 'winston' class LockManager constructor: -> unless config.isProduction or config.redis.host isnt "localhost" throw "You shouldn't be instantiating distributed locks unless in production." @redisClient = redis.createClient config.redis.port, config.redis.host + @redisClient.on "ready", => + log.info "Redis ready!" + @redisNotAvailable = false + @redisClient.on "error", (err) => + @redisNotAvailable = true + log.error "Redis connection error! Err: #{err}" + @redisClient.on "end", => + @redisNotAvailable = true + log.error "Redis connection ended!" @lockValues = {} @unlockScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end" setLock: (lockName, timeoutMs, cb) => + if @redisNotAvailable is true then return cb "Redis not available!" randomNumber = Math.floor(Math.random() * 1000000000) @redisClient.set [lockName,randomNumber, "NX", "PX", timeoutMs], (err, res) => if err? then return cb err, null @@ -20,6 +31,7 @@ class LockManager return cb "Lock already set!", null releaseLock: (lockName, cb) => + if @redisNotAvailable is true then return cb "Redis not available!" @redisClient.eval [@unlockScript, 1, lockName, @lockValues[lockName]], (err, res) -> if err? then return cb err, null if res diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 8983a7224..0e34e06db 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -274,25 +274,27 @@ employerNewCandidatesAvailableTask = -> ### End Employer New Candidates Available Email ### - + ### New Recruit Leaderboard Email ### +### newRecruitLeaderboardEmailTask = -> # tem_kMQFCKX3v4DNAQDsMAsPJC #maxRank and maxRankTime should be recorded if isSimulating is false mailTaskName = "newRecruitLeaderboardEmailTask" lockDurationMs = 6000 lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> - +### ### End New Recruit Leaderboard Email ### ### Employer Matching Candidate Notification Email ### +### employerMatchingCandidateNotificationTask = -> # tem_mYsepTfWQ265noKfZJcbBH #save email filters in their own collection mailTaskName = "employerMatchingCandidateNotificationTask" lockDurationMs = 6000 lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> - +### ### End Employer Matching Candidate Notification Email ### ### Ladder Update Email ### From 24bcfd9f26b336a3ee98302e3b0ef12345f8fe63 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 15:50:31 -0700 Subject: [PATCH 11/21] Reviewed code, almost ready for release --- server/routes/mail.coffee | 196 +++++++++++++++++++++----------------- 1 file changed, 107 insertions(+), 89 deletions(-) diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 0e34e06db..6939d8080 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -9,9 +9,20 @@ LevelSession = require '../levels/sessions/LevelSession' Level = require '../levels/Level' log = require 'winston' sendwithus = require '../sendwithus' -if config.isProduction or config.redis.host isnt "localhost" +if config.isProduction or config.redis.host isnt "localhost" #TODO: Ask Nick and Scott to change their environment variables and change the deploy ones lockManager = require '../commons/LockManager' - +#TODO: Ask Nick about email unsubscriptions +createMailTask = (req, res) -> #TODO: Ask Nick whether he thinks it is a good idea or not to hardcode the mail tasks + unless req.user?.isAdmin() then return errors.forbidden(res) + unless req.body.url and req.body.frequency then return errors.badInput(res) + console.log "Creating mail task with url #{req.body.url} and frequency #{req.body.frequency}" + newMailTask = new MailTask {} + newMailTask.set("url",req.body.url) + newMailTask.set("frequency",req.body.frequency) + newMailTask.save (err) -> + if err? then return errors.serverError(res, err) + res.send("Created mail task!") + res.end() module.exports.setup = (app) -> app.all config.mail.mailchimpWebhook, handleMailchimpWebHook @@ -22,28 +33,52 @@ module.exports.setup = (app) -> setupScheduledEmails = -> testForLockManager() - mailTaskMap = - "test_mail_task": employerNewCandidatesAvailableTask + mailTaskMap = #TODO: Edit this to include additional emails + "test_mail_task": candidateUpdateProfileTask - MailTask.find({}).lean().exec (err, mailTasks) -> + MailTask.find({}).lean().exec (err, mailTasks) -> #TODO: Ask Nick whether or not to remove this if err? then throw "Failed to schedule mailTasks! #{err}" for mailTask in mailTasks - setInterval mailTaskMap[mailTask.url], mailTask.frequency*2 + setInterval mailTaskMap[mailTask.url], mailTask.frequency*2 #TODO: Have some random offset to prevent lock contention testForLockManager = -> unless lockManager then throw "The system isn't configured to do distributed locking!" ### Candidate Update Reminder Task ### -emailTimeRange = (timeRange, finalCallback) -> + +candidateUpdateProfileTask = -> + mailTaskName = "candidateUpdateProfileTask" + lockDurationMs = 20000 #TODO: Change these to something appropriate for the mail frequency (ideally longer than the task but shorter than frequency) + currentDate = new Date() + timeRanges = [] + for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [8, 52, 'eight weeks']] + timeRanges.push + start: generateWeekOffset currentDate, weekPair[0] + end: generateWeekOffset currentDate, weekPair[1] + name: weekPair[2] + lockManager.setLock mailTaskName, lockDurationMs, (err) -> + if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}!" + async.each timeRanges, emailTimeRange.bind({mailTaskName: mailTaskName}), (err) -> + if err + log.error "There was an error sending the candidate profile update reminder emails: #{err}" + else + log.info "Completed mail task #{mailTaskName}" + lockManager.releaseLock mailTaskName, (err) -> + if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}" + +generateWeekOffset = (originalDate, numberOfWeeks) -> + return (new Date(originalDate.getTime() - numberOfWeeks * 7 * 24 * 60 * 60 * 1000)).toISOString() + +emailTimeRange = (timeRange, emailTimeRangeCallback) -> waterfallContext = "timeRange": timeRange "mailTaskName": @mailTaskName async.waterfall [ findAllCandidatesWithinTimeRange.bind(waterfallContext) - (unfilteredCandidates, cb) -> #now filter the candidates to see if they are eligible - async.reject unfilteredCandidates, candidateFilter.bind(waterfallContext), (filtered) -> cb null, filtered - (filteredCandidates, cb) -> #Now send emails to the eligible candidates and record. + (unfilteredCandidates, cb) -> + async.reject unfilteredCandidates, candidateFilter.bind(waterfallContext), cb.bind(null, null) + (filteredCandidates, cb) -> async.each filteredCandidates, sendReminderEmailToCandidate.bind(waterfallContext), cb - ], finalCallback + ], emailTimeRangeCallback findAllCandidatesWithinTimeRange = (cb) -> findParameters = @@ -61,8 +96,11 @@ candidateFilter = (candidate, sentEmailFilterCallback) -> "metadata.timeRangeName": @timeRange.name "metadata.updated": candidate.jobProfile.updated MailSent.find(findParameters).lean().exec (err, sentMail) -> - if err? then return errors.serverError("Error fetching sent mail in email task") - sentEmailFilterCallback Boolean(sentMail.length) + if err? + log.error "Error finding mail sent for task #{@mailTaskName} and user #{candidate._id}!" + sentEmailFilterCallback true + else + sentEmailFilterCallback Boolean(sentMail.length) findEmployersSignedUpAfterDate = (dateObject, cb) -> countParameters = @@ -73,14 +111,18 @@ findEmployersSignedUpAfterDate = (dateObject, cb) -> sendReminderEmailToCandidate = (candidate, sendEmailCallback) -> findEmployersSignedUpAfterDate new Date(candidate.jobProfile.updated), (err, employersAfterCount) => + if err? + log.error "There was an error finding employers who signed up after #{candidate.jobProfile.updated}: #{err}" + return sendEmailCallback err context = email_id: "tem_CtTLsKQufxrxoPMn7upKiL" recipient: address: candidate.email name: candidate.jobProfile.name email_data: - profile_updated: candidate.jobProfile.updated #format nicely new_company: employersAfterCount + company_name: "CodeCombat" + user_profile: "http://codecombat.com/account/profile/#{candidate._id}" log.info "Sending #{@timeRange.name} update reminder to #{context.recipient.name}(#{context.recipient.address})" newSentMail = mailTask: @mailTaskName @@ -93,30 +135,22 @@ sendReminderEmailToCandidate = (candidate, sendEmailCallback) -> sendwithus.api.send context, (err, result) -> log.error "Error sending candidate update reminder email: #{err} with result #{result}" if err sendEmailCallback null - -generateWeekOffset = (originalDate, numberOfWeeks) -> - return (new Date(originalDate.getTime() - numberOfWeeks * 7 * 24 * 60 * 60 * 1000)).toISOString() - -candidateUpdateProfileTask = -> - mailTaskName = "candidateUpdateProfileTask" - lockDurationMs = 6000 - currentDate = new Date() - timeRanges = [] - for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [8, 52, 'eight weeks']] - timeRanges.push - start: generateWeekOffset currentDate, weekPair[0] - end: generateWeekOffset currentDate, weekPair[1] - name: weekPair[2] - lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> - if err? then return log.error "Error getting a task lock!" - async.each timeRanges, emailTimeRange.bind(mailTaskName: mailTaskName), (err) -> - if err then log.error JSON.stringify err else log.info "Sent candidate update reminders!" - lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err - ### End Candidate Update Reminder Task ### ### Internal Candidate Update Reminder Email ### +internalCandidateUpdateTask = -> + mailTaskName = "internalCandidateUpdateTask" + lockDurationMs = 6000 #TODO: Change lock duration + lockManager.setLock mailTaskName, lockDurationMs, (err) -> + if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}!" + emailInternalCandidateUpdateReminder.apply {"mailTaskName":mailTaskName}, (err) -> + if err + log.error "There was an error sending the internal candidate update reminder.: #{err}" + else + log.info "Sent internal candidate update reminder email!" + lockManager.releaseLock mailTaskName, (err) -> + if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}" -emailInternalCandidateUpdateReminder = (cb) -> +emailInternalCandidateUpdateReminder = (internalCandidateUpdateReminderCallback) -> currentTime = new Date() beginningOfUTCDay = new Date() beginningOfUTCDay.setUTCHours(0,0,0,0) @@ -124,40 +158,43 @@ emailInternalCandidateUpdateReminder = (cb) -> "beginningOfUTCDay": beginningOfUTCDay "currentTime": currentTime "mailTaskName": @mailTaskName - async.waterfall [ findNonApprovedCandidatesWhoUpdatedJobProfileToday.bind(asyncContext) (unfilteredCandidates, cb) -> async.reject unfilteredCandidates, candidatesUpdatedTodayFilter.bind(asyncContext), cb.bind(null,null) (filteredCandidates, cb) -> async.each filteredCandidates, sendInternalCandidateUpdateReminder.bind(asyncContext), cb - ], cb + ], internalCandidateUpdateReminderCallback findNonApprovedCandidatesWhoUpdatedJobProfileToday = (cb) -> findParameters = "jobProfile.updated": $lte: @currentTime.toISOString() gt: @beginningOfUTCDay.toISOString() - "jobProfileApproved": false + "jobProfileApproved": false User.find(findParameters).select("_id jobProfile.name jobProfile.updated").lean().exec cb candidatesUpdatedTodayFilter = (candidate, cb) -> findParameters = "user": candidate._id "mailTask": @mailTaskName - "metadata.beginningOfUTCDay": @beginningOfUTCDay + "metadata.beginningOfUTCDay": @beginningOfUTCDay MailSent.find(findParameters).lean().exec (err, sentMail) -> - if err? then return errors.serverError("Error fetching sent mail in #{@mailTaskName}") - cb Boolean(sentMail.length) + if err? + log.error "Error finding mail sent for task #{@mailTaskName} and user #{candidate._id}!" + cb true + else + cb Boolean(sentMail.length) sendInternalCandidateUpdateReminder = (candidate, cb) -> context = email_id: "tem_Ac7nhgKqatTHBCgDgjF5pE" - recipient: + recipient: address: "team@codecombat.com" #Change to whatever email address is necessary name: "The CodeCombat Team" + email_data: + new_candidate_profile: "https://codecombat.com/account/profile/#{candidate._id}" log.info "Sending candidate updated reminder for #{candidate.jobProfile.name}" - newSentMail = mailTask: @mailTaskName user: candidate._id @@ -169,20 +206,23 @@ sendInternalCandidateUpdateReminder = (candidate, cb) -> sendwithus.api.send context, (err, result) -> log.error "Error sending interal candidate update email: #{err} with result #{result}" if err cb null - -internalCandidateUpdateTask = -> - mailTaskName = "internalCandidateUpdateTask" - lockDurationMs = 6000 - lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> - if err? then return log.error "Error getting a task lock!" - emailInternalCandidateUpdateReminder.apply {"mailTaskName":mailTaskName}, (err) -> - if err? then log.error "There was an error sending the internal candidate update reminder." - lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err -### End Internal Candidate Update Reminder Email ### - -### Employer New Candidates Available Email ### -emailEmployerNewCandidatesAvailableEmail = (cb) -> +### End Internal Candidate Update Reminder Email ### +### Employer New Candidates Available Email ### +employerNewCandidatesAvailableTask = -> + mailTaskName = "employerNewCandidatesAvailableTask" + lockDurationMs = 6000 #TODO: Update this lock duration + lockManager.setLock mailTaskName, lockDurationMs, (err) -> + if err? then return log.error "There was an error getting a task lock!" + emailEmployerNewCandidatesAvailable.apply {"mailTaskName":mailTaskName}, (err) -> + if err + log.error "There was an error completing the new candidates available task: #{err}" + else + log.info "Completed the employer new candidates available task!" + lockManager.releaseLock mailTaskName, (err) -> + if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}" + +emailEmployerNewCandidatesAvailable = (emailEmployerNewCandidatesAvailableCallback) -> currentTime = new Date() asyncContext = "currentTime": currentTime @@ -192,22 +232,20 @@ emailEmployerNewCandidatesAvailableEmail = (cb) -> findAllEmployers makeEmployerNamesEasilyAccessible (allEmployers, cb) -> - console.log "Found #{allEmployers.length} employers to email about new candidates available" async.reject allEmployers, employersEmailedDigestMoreThanWeekAgoFilter.bind(asyncContext), cb.bind(null,null) (employersToEmail, cb) -> async.each employersToEmail, sendEmployerNewCandidatesAvailableEmail.bind(asyncContext), cb - ], cb + ], emailEmployerNewCandidatesAvailableCallback findAllEmployers = (cb) -> findParameters = "employerAt": - $exists: true + $exists: true permissions: "employer" selection = "_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName activity dateCreated" User.find(findParameters).select(selection).lean().exec cb makeEmployerNamesEasilyAccessible = (allEmployers, cb) -> - #Make names easily accessible for employer, index in allEmployers if employer.signedEmployerAgreement?.data?.firstName employer.name = employer.signedEmployerAgreement.data.firstName + " " + employer.signedEmployerAgreement.data.lastName @@ -222,15 +260,16 @@ employersEmailedDigestMoreThanWeekAgoFilter = (employer, cb) -> "sent": $gt: new Date(@currentTime.getTime() - 7 * 24 * 60 * 60 * 1000) MailSent.find(findParameters).lean().exec (err, sentMail) -> - if err? then return errors.serverError("Error fetching sent mail in #{@mailTaskName}") - cb Boolean(sentMail.length) + if err? + log.error "Error finding mail sent for task #{@mailTaskName} and employer #employer._id}!" + cb true + else + cb Boolean(sentMail.length) sendEmployerNewCandidatesAvailableEmail = (employer, cb) -> - lastLoginDate = employer.activity?.login?.last ? employer.dateCreated + lastLoginDate = employer.activity?.login?.last ? employer.dateCreated countParameters = "jobProfileApproved": true - "jobProfile": - $exists: true $or: [ jobProfileApprovedDate: $gt: lastLoginDate.toISOString() @@ -246,12 +285,12 @@ sendEmployerNewCandidatesAvailableEmail = (employer, cb) -> email_id: "tem_CCcHKr95Nvu5bT7c7iHCtm" recipient: address: employer.email - name: employer.name email_data: new_candidates: numberOfCandidatesSinceLogin employer_company_name: employer.employerAt company_name: "CodeCombat" - + if employer.name + context.recipient.name = employer.name log.info "Sending available candidates update reminder to #{context.recipient.name}(#{context.recipient.address})" newSentMail = mailTask: @mailTaskName @@ -262,17 +301,6 @@ sendEmployerNewCandidatesAvailableEmail = (employer, cb) -> log.error "Error sending employer candidates available email: #{err} with result #{result}" if err cb null -employerNewCandidatesAvailableTask = -> - #initialize featuredDate to job profile updated - mailTaskName = "employerNewCandidatesAvailableTask" - lockDurationMs = 6000 - lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) -> - if err? then return log.error "There was an error getting a task lock!" - emailEmployerNewCandidatesAvailableEmail.apply {"mailTaskName":mailTaskName}, (err) -> - if err? then return log.error "There was an error performing the #{mailTaskName} email task." - lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err - - ### End Employer New Candidates Available Email ### ### New Recruit Leaderboard Email ### @@ -313,17 +341,7 @@ isRequestFromDesignatedCronHandler = (req, res) -> return false return true -createMailTask = (req, res) -> - #unless req.user?.isAdmin() then return errors.forbidden(res) - unless req.body.url and req.body.frequency then return errors.badInput(res) - console.log "Creating mail task with url #{req.body.url} and frequency #{req.body.frequency}" - newMailTask = new MailTask {} - newMailTask.set("url",req.body.url) - newMailTask.set("frequency",req.body.frequency) - newMailTask.save (err) -> - if err? then return errors.serverError(res, err) - res.send("Created mail task!") - res.end() + handleLadderUpdate = (req, res) -> log.info('Going to see about sending ladder update emails.') From 933fb6866cdeff86955639e0d874c9c27c4de67f Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Wed, 16 Jul 2014 16:30:57 -0700 Subject: [PATCH 12/21] Uncommented ellipse test cases. --- test/app/lib/world/ellipse.spec.coffee | 189 ++++++++++---------- test/app/lib/world/line_segment.spec.coffee | 14 +- 2 files changed, 102 insertions(+), 101 deletions(-) diff --git a/test/app/lib/world/ellipse.spec.coffee b/test/app/lib/world/ellipse.spec.coffee index bb71fdad5..8066898be 100644 --- a/test/app/lib/world/ellipse.spec.coffee +++ b/test/app/lib/world/ellipse.spec.coffee @@ -3,97 +3,98 @@ describe 'Ellipse', -> Rectangle = require 'lib/world/rectangle' Vector = require 'lib/world/vector' - #it 'contains its own center', -> - # ellipse = new Ellipse 0, 0, 10, 10 - # expect(ellipse.containsPoint(new Vector 0, 0)).toBe true - # - #it 'contains a point when rotated', -> - # ellipse = new Ellipse 0, -20, 40, 40, 3 * Math.PI / 4 - # p = new Vector 0, 2 - # expect(ellipse.containsPoint(p, true)).toBe true - # - #it 'contains more points properly', -> - # # ellipse with y major axis, off-origin center, and 45 degree rotation - # ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4 - # expect(ellipse.contains new Vector(1, 2)).toBe true - # expect(ellipse.contains new Vector(-1, 3)).toBe true - # expect(ellipse.contains new Vector(0, 4)).toBe true - # expect(ellipse.contains new Vector(1, 4)).toBe true - # expect(ellipse.contains new Vector(3, 0)).toBe true - # expect(ellipse.contains new Vector(1, 0)).toBe true - # expect(ellipse.contains new Vector(0, 1)).toBe true - # expect(ellipse.contains new Vector(-1, 2)).toBe true - # expect(ellipse.contains new Vector(2, 2)).toBe true - # expect(ellipse.contains new Vector(0, 0)).toBe false - # expect(ellipse.contains new Vector(0, 5)).toBe false - # expect(ellipse.contains new Vector(3, 4)).toBe false - # expect(ellipse.contains new Vector(4, 0)).toBe false - # expect(ellipse.contains new Vector(2, -1)).toBe false - # expect(ellipse.contains new Vector(0, -3)).toBe false - # expect(ellipse.contains new Vector(-2, -2)).toBe false - # expect(ellipse.contains new Vector(-2, 0)).toBe false - # expect(ellipse.contains new Vector(-2, 4)).toBe false - # - #it 'correctly calculates distance to a faraway point', -> - # ellipse = new Ellipse 100, 50, 20, 40 - # p = new Vector 200, 300 - # d = 10 * Math.sqrt(610) - # expect(ellipse.distanceToPoint(p)).toBeCloseTo d - # ellipse.rotation = Math.PI / 2 - # d = 80 * Math.sqrt(10) - # expect(ellipse.distanceToPoint(p)).toBeCloseTo d - # - #it 'does not modify itself or target Vector when calculating distance', -> - # ellipse = new Ellipse -100, -200, 1, 100 - # ellipse2 = ellipse.copy() - # p = new Vector -100.25, -101 - # p2 = p.copy() - # ellipse.distanceToPoint(p) - # expect(p.x).toEqual p2.x - # expect(p.y).toEqual p2.y - # expect(ellipse.x).toEqual ellipse2.x - # expect(ellipse.y).toEqual ellipse2.y - # expect(ellipse.width).toEqual ellipse2.width - # expect(ellipse.height).toEqual ellipse2.height - # expect(ellipse.rotation).toEqual ellipse2.rotation - # - #it 'correctly calculates distance to contained point', -> - # ellipse = new Ellipse -100, -200, 1, 100 - # ellipse2 = ellipse.copy() - # p = new Vector -100.25, -160 - # p2 = p.copy() - # expect(ellipse.distanceToPoint(p)).toBe 0 - # ellipse.rotation = 0.00000001 * Math.PI - # expect(ellipse.distanceToPoint(p)).toBe 0 - # - #it 'AABB works when not rotated', -> - # ellipse = new Ellipse 10, 20, 30, 40 - # rect = new Rectangle 10, 20, 30, 40 - # aabb1 = ellipse.axisAlignedBoundingBox() - # aabb2 = ellipse.axisAlignedBoundingBox() - # for prop in ['x', 'y', 'width', 'height'] - # expect(aabb1[prop]).toBe aabb2[prop] - # - #it 'AABB works when rotated', -> - # ellipse = new Ellipse 10, 20, 30, 40, Math.PI / 3 - # rect = new Rectangle 10, 20, 30, 40, Math.PI / 3 - # aabb1 = ellipse.axisAlignedBoundingBox() - # aabb2 = ellipse.axisAlignedBoundingBox() - # for prop in ['x', 'y', 'width', 'height'] - # expect(aabb1[prop]).toBe aabb2[prop] - # - #it 'calculates ellipse intersections properly', -> - # # ellipse with y major axis, off-origin center, and 45 degree rotation - # ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4 - # expect(ellipse.intersectsShape new Rectangle(0, 0, 2, 2, 0)).toBe true - # expect(ellipse.intersectsShape new Rectangle(0, -1, 2, 3, 0)).toBe true - # expect(ellipse.intersectsShape new Rectangle(-1, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe true - # expect(ellipse.intersectsShape new Rectangle(-1, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true - # expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true - # expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe false - # expect(ellipse.intersectsShape new Rectangle(-2, -2, 2, 2, 0)).toBe false - # expect(ellipse.intersectsShape new Rectangle(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, 0)).toBe false - # expect(ellipse.intersectsShape new Rectangle(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, Math.PI / 4)).toBe false - # expect(ellipse.intersectsShape new Rectangle(-2, 0, 2, 2, 0)).toBe false - # expect(ellipse.intersectsShape new Rectangle(0, -2, 2, 2, 0)).toBe false - # expect(ellipse.intersectsShape new Rectangle(1, 2, 1, 1, 0)).toBe true + it 'contains its own center', -> + ellipse = new Ellipse 0, 0, 10, 10 + expect(ellipse.containsPoint(new Vector 0, 0)).toBe true + + it 'contains a point when rotated', -> + ellipse = new Ellipse 0, -20, 40, 40, 3 * Math.PI / 4 + expect(ellipse.containsPoint new Vector(0, 0)).toBe true + expect(ellipse.containsPoint new Vector(0, 2)).toBe false + + it 'contains more points properly', -> + # ellipse with y major axis, off-origin center, and 45 degree rotation + ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4 + expect(ellipse.containsPoint new Vector(1, 2)).toBe true + expect(ellipse.containsPoint new Vector(-1, 3)).toBe true + expect(ellipse.containsPoint new Vector(0, 4)).toBe true + expect(ellipse.containsPoint new Vector(1, 4)).toBe true + expect(ellipse.containsPoint new Vector(3, 0)).toBe true + expect(ellipse.containsPoint new Vector(1, 0)).toBe true + expect(ellipse.containsPoint new Vector(0, 1)).toBe true + expect(ellipse.containsPoint new Vector(-1, 2)).toBe true + expect(ellipse.containsPoint new Vector(2, 2)).toBe true + expect(ellipse.containsPoint new Vector(0, 0)).toBe false + expect(ellipse.containsPoint new Vector(0, 5)).toBe false + expect(ellipse.containsPoint new Vector(3, 4)).toBe false + expect(ellipse.containsPoint new Vector(4, 0)).toBe false + expect(ellipse.containsPoint new Vector(2, -1)).toBe false + expect(ellipse.containsPoint new Vector(0, -3)).toBe false + expect(ellipse.containsPoint new Vector(-2, -2)).toBe false + expect(ellipse.containsPoint new Vector(-2, 0)).toBe false + expect(ellipse.containsPoint new Vector(-2, 4)).toBe false + + xit 'correctly calculates distance to a faraway point', -> + # TODO: this is the correct distance if the ellipse were a rectangle, but need to update for actual ellipse expected distances. + ellipse = new Ellipse 100, 50, 20, 40 + p = new Vector 200, 300 + d = 10 * Math.sqrt(610) + expect(ellipse.distanceToPoint(p)).toBeCloseTo d + ellipse.rotation = Math.PI / 2 + d = 80 * Math.sqrt(10) + expect(ellipse.distanceToPoint(p)).toBeCloseTo d + + it 'does not modify itself or target Vector when calculating distance', -> + ellipse = new Ellipse -100, -200, 1, 100 + ellipse2 = ellipse.copy() + p = new Vector -100.25, -101 + p2 = p.copy() + ellipse.distanceToPoint(p) + expect(p.x).toEqual p2.x + expect(p.y).toEqual p2.y + expect(ellipse.x).toEqual ellipse2.x + expect(ellipse.y).toEqual ellipse2.y + expect(ellipse.width).toEqual ellipse2.width + expect(ellipse.height).toEqual ellipse2.height + expect(ellipse.rotation).toEqual ellipse2.rotation + + it 'correctly calculates distance to contained point', -> + ellipse = new Ellipse -100, -200, 1, 100 + ellipse2 = ellipse.copy() + p = new Vector -100.25, -160 + p2 = p.copy() + expect(ellipse.distanceToPoint(p)).toBe 0 + ellipse.rotation = 0.00000001 * Math.PI + expect(ellipse.distanceToPoint(p)).toBe 0 + + it 'AABB works when not rotated', -> + ellipse = new Ellipse 10, 20, 30, 40 + rect = new Rectangle 10, 20, 30, 40 + aabb1 = ellipse.axisAlignedBoundingBox() + aabb2 = ellipse.axisAlignedBoundingBox() + for prop in ['x', 'y', 'width', 'height'] + expect(aabb1[prop]).toBe aabb2[prop] + + it 'AABB works when rotated', -> + ellipse = new Ellipse 10, 20, 30, 40, Math.PI / 3 + rect = new Rectangle 10, 20, 30, 40, Math.PI / 3 + aabb1 = ellipse.axisAlignedBoundingBox() + aabb2 = ellipse.axisAlignedBoundingBox() + for prop in ['x', 'y', 'width', 'height'] + expect(aabb1[prop]).toBe aabb2[prop] + + it 'calculates ellipse intersections properly', -> + # ellipse with y major axis, off-origin center, and 45 degree rotation + ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4 + expect(ellipse.intersectsShape new Rectangle(0, 0, 2, 2, 0)).toBe true + expect(ellipse.intersectsShape new Rectangle(0, -1, 2, 3, 0)).toBe true + expect(ellipse.intersectsShape new Rectangle(-1, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe true + expect(ellipse.intersectsShape new Rectangle(-1, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true + expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true + expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe false + expect(ellipse.intersectsShape new Rectangle(-2, -2, 2, 2, 0)).toBe false + expect(ellipse.intersectsShape new Rectangle(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, 0)).toBe false + expect(ellipse.intersectsShape new Rectangle(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, Math.PI / 4)).toBe false + expect(ellipse.intersectsShape new Rectangle(-2, 0, 2, 2, 0)).toBe false + expect(ellipse.intersectsShape new Rectangle(0, -2, 2, 2, 0)).toBe false + expect(ellipse.intersectsShape new Rectangle(1, 2, 1, 1, 0)).toBe true diff --git a/test/app/lib/world/line_segment.spec.coffee b/test/app/lib/world/line_segment.spec.coffee index d3b6ca939..4fef852c8 100644 --- a/test/app/lib/world/line_segment.spec.coffee +++ b/test/app/lib/world/line_segment.spec.coffee @@ -28,13 +28,13 @@ describe 'LineSegment', -> it 'can tell when a point is on a line or segment', -> lineSegment = new LineSegment v00, v11 expect(lineSegment.pointOnLine v22, false).toBe true - #expect(lineSegment.pointOnLine v22, true).toBe false - #expect(lineSegment.pointOnLine v00, false).toBe true - #expect(lineSegment.pointOnLine v00, true).toBe true - #expect(lineSegment.pointOnLine v11, true).toBe true - #expect(lineSegment.pointOnLine v11, false).toBe true - #expect(lineSegment.pointOnLine v34, false).toBe false - #expect(lineSegment.pointOnLine v34, true).toBe false + expect(lineSegment.pointOnLine v22, true).toBe false + expect(lineSegment.pointOnLine v00, false).toBe true + expect(lineSegment.pointOnLine v00, true).toBe true + expect(lineSegment.pointOnLine v11, true).toBe true + expect(lineSegment.pointOnLine v11, false).toBe true + expect(lineSegment.pointOnLine v34, false).toBe false + expect(lineSegment.pointOnLine v34, true).toBe false it 'correctly calculates distance to points', -> lineSegment = new LineSegment v00, v11 From ae23679a26babc948d7b961ae4a7755e96ad5747 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 16:37:06 -0700 Subject: [PATCH 13/21] Hard code mail tasks and add unsubscribe --- app/schemas/models/mail_task.coffee | 21 ---------- server/mail/tasks/MailTask.coffee | 7 ---- server/mail/tasks/mail_task_handler.coffee | 12 ------ server/routes/auth.coffee | 5 ++- server/routes/mail.coffee | 49 +++++++++++----------- 5 files changed, 28 insertions(+), 66 deletions(-) delete mode 100644 app/schemas/models/mail_task.coffee delete mode 100644 server/mail/tasks/MailTask.coffee delete mode 100644 server/mail/tasks/mail_task_handler.coffee diff --git a/app/schemas/models/mail_task.coffee b/app/schemas/models/mail_task.coffee deleted file mode 100644 index ac450e0ee..000000000 --- a/app/schemas/models/mail_task.coffee +++ /dev/null @@ -1,21 +0,0 @@ -c = require './../schemas' -#This will represent transactional emails which have been sent - -MailTaskSchema = c.object { - title: 'Mail task' - description: 'Mail tasks to call at certain intervals' -} -_.extend MailTaskSchema.properties, - url: - title: 'URL' - description: 'The associated URL of the mailtask to call' - type: 'string' - frequency: - title: 'Frequency' - description: 'The number of seconds the servers should check whether or not to send the email' - type: 'integer' - -c.extendBasicProperties MailTaskSchema, 'mail.task' - -module.exports = MailTaskSchema - \ No newline at end of file diff --git a/server/mail/tasks/MailTask.coffee b/server/mail/tasks/MailTask.coffee deleted file mode 100644 index 10a536787..000000000 --- a/server/mail/tasks/MailTask.coffee +++ /dev/null @@ -1,7 +0,0 @@ -mongoose = require 'mongoose' -plugins = require '../../plugins/plugins' -jsonschema = require '../../../app/schemas/models/mail_task' - -MailTaskSchema = new mongoose.Schema({}, {strict: false}) - -module.exports = MailTask = mongoose.model('mail.task', MailTaskSchema) diff --git a/server/mail/tasks/mail_task_handler.coffee b/server/mail/tasks/mail_task_handler.coffee deleted file mode 100644 index 12b6be53a..000000000 --- a/server/mail/tasks/mail_task_handler.coffee +++ /dev/null @@ -1,12 +0,0 @@ -MailTask = require './MailTask' -Handler = require '../../commons/Handler' - -class MailTaskHandler extends Handler - modelClass: MailTask - editableProperties: ['url','frequency'] - jsonSchema: require '../../../app/schemas/models/mail_task' - - hasAccess: (req) -> - req.user?.isAdmin() - -module.exports = new MailTaskHandler() diff --git a/server/routes/auth.coffee b/server/routes/auth.coffee index 5c3298f44..36c836741 100644 --- a/server/routes/auth.coffee +++ b/server/routes/auth.coffee @@ -143,7 +143,10 @@ module.exports.setup = (app) -> emails.recruitNotes ?= {} emails.recruitNotes.enabled = false msg = "Unsubscribed #{req.query.email} from recruiting emails." - + else if req.query.employerNotes + emails.employerNotes ?= {} + emails.employerNotes.enabled = false + msg = "Unsubscribed #{req.query.email} from employer emails." else msg = "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!" emailSettings.enabled = false for emailSettings in _.values(emails) diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 6939d8080..3a41e5316 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -1,5 +1,4 @@ mail = require '../commons/mail' -MailTask = require '../mail/tasks/MailTask' MailSent = require '../mail/sent/MailSent' User = require '../users/User' async = require 'async' @@ -12,42 +11,36 @@ sendwithus = require '../sendwithus' if config.isProduction or config.redis.host isnt "localhost" #TODO: Ask Nick and Scott to change their environment variables and change the deploy ones lockManager = require '../commons/LockManager' #TODO: Ask Nick about email unsubscriptions -createMailTask = (req, res) -> #TODO: Ask Nick whether he thinks it is a good idea or not to hardcode the mail tasks - unless req.user?.isAdmin() then return errors.forbidden(res) - unless req.body.url and req.body.frequency then return errors.badInput(res) - console.log "Creating mail task with url #{req.body.url} and frequency #{req.body.frequency}" - newMailTask = new MailTask {} - newMailTask.set("url",req.body.url) - newMailTask.set("frequency",req.body.frequency) - newMailTask.save (err) -> - if err? then return errors.serverError(res, err) - res.send("Created mail task!") - res.end() module.exports.setup = (app) -> app.all config.mail.mailchimpWebhook, handleMailchimpWebHook app.get '/mail/cron/ladder-update', handleLadderUpdate - app.post '/mail/task', createMailTask if lockManager setupScheduledEmails() setupScheduledEmails = -> testForLockManager() - mailTaskMap = #TODO: Edit this to include additional emails - "test_mail_task": candidateUpdateProfileTask + mailTasks = [ + taskFunction: candidateUpdateProfileTask + frequencyMs: 30 * 60 * 1000 #30 minutes + , + taskFunction: internalCandidateUpdateTask + frequencyMs: 10 * 60 * 1000 #10 minutes + , + taskFunction: employerNewCandidatesAvailableTask + frequencyMs: 30 * 60 * 1000 #30 minutes + ] + + for mailTask in mailTasks + setInterval mailTask.taskFunction, mailTask.frequencyMs - MailTask.find({}).lean().exec (err, mailTasks) -> #TODO: Ask Nick whether or not to remove this - if err? then throw "Failed to schedule mailTasks! #{err}" - for mailTask in mailTasks - setInterval mailTaskMap[mailTask.url], mailTask.frequency*2 #TODO: Have some random offset to prevent lock contention - testForLockManager = -> unless lockManager then throw "The system isn't configured to do distributed locking!" ### Candidate Update Reminder Task ### candidateUpdateProfileTask = -> mailTaskName = "candidateUpdateProfileTask" - lockDurationMs = 20000 #TODO: Change these to something appropriate for the mail frequency (ideally longer than the task but shorter than frequency) + lockDurationMs = 2 * 60 * 1000 currentDate = new Date() timeRanges = [] for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [8, 52, 'eight weeks']] @@ -86,10 +79,13 @@ findAllCandidatesWithinTimeRange = (cb) -> $gt: @timeRange.start $lte: @timeRange.end "jobProfileApproved": true - selection = "_id email jobProfile.name jobProfile.updated" + selection = "_id email jobProfile.name jobProfile.updated emails" #make sure to check for anyNotes too. User.find(findParameters).select(selection).lean().exec cb candidateFilter = (candidate, sentEmailFilterCallback) -> + if candidate.emails?.anyNotes?.enabled is false or candidate.emails?.recruitNotes?.enabled is false + log.info "Candidate #{candidate.jobProfile.name} opted out of emails, not sending to them." + return sentEmailFilterCallback true findParameters = "user": candidate._id "mailTask": @mailTaskName @@ -139,7 +135,7 @@ sendReminderEmailToCandidate = (candidate, sendEmailCallback) -> ### Internal Candidate Update Reminder Email ### internalCandidateUpdateTask = -> mailTaskName = "internalCandidateUpdateTask" - lockDurationMs = 6000 #TODO: Change lock duration + lockDurationMs = 2 * 60 * 1000 lockManager.setLock mailTaskName, lockDurationMs, (err) -> if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}!" emailInternalCandidateUpdateReminder.apply {"mailTaskName":mailTaskName}, (err) -> @@ -211,7 +207,7 @@ sendInternalCandidateUpdateReminder = (candidate, cb) -> ### Employer New Candidates Available Email ### employerNewCandidatesAvailableTask = -> mailTaskName = "employerNewCandidatesAvailableTask" - lockDurationMs = 6000 #TODO: Update this lock duration + lockDurationMs = 2 * 60 * 1000 lockManager.setLock mailTaskName, lockDurationMs, (err) -> if err? then return log.error "There was an error getting a task lock!" emailEmployerNewCandidatesAvailable.apply {"mailTaskName":mailTaskName}, (err) -> @@ -242,7 +238,7 @@ findAllEmployers = (cb) -> "employerAt": $exists: true permissions: "employer" - selection = "_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName activity dateCreated" + selection = "_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName activity dateCreated emails" User.find(findParameters).select(selection).lean().exec cb makeEmployerNamesEasilyAccessible = (allEmployers, cb) -> @@ -254,6 +250,9 @@ makeEmployerNamesEasilyAccessible = (allEmployers, cb) -> cb null, allEmployers employersEmailedDigestMoreThanWeekAgoFilter = (employer, cb) -> + if employer.emails?.employerNotes?.enabled is false + log.info "Employer #{employer.name}(#{employer.email}) opted out of emails, not sending to them." + return sentEmailFilterCallback true findParameters = "user": employer._id "mailTask": @mailTaskName From fc4568a100dcc922e10564b5fabf58715c4885b4 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 17:02:40 -0700 Subject: [PATCH 14/21] Fixed misc bugs --- server/routes/mail.coffee | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 3a41e5316..45f90ac2d 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -10,8 +10,7 @@ log = require 'winston' sendwithus = require '../sendwithus' if config.isProduction or config.redis.host isnt "localhost" #TODO: Ask Nick and Scott to change their environment variables and change the deploy ones lockManager = require '../commons/LockManager' -#TODO: Ask Nick about email unsubscriptions - + module.exports.setup = (app) -> app.all config.mail.mailchimpWebhook, handleMailchimpWebHook app.get '/mail/cron/ladder-update', handleLadderUpdate @@ -84,7 +83,6 @@ findAllCandidatesWithinTimeRange = (cb) -> candidateFilter = (candidate, sentEmailFilterCallback) -> if candidate.emails?.anyNotes?.enabled is false or candidate.emails?.recruitNotes?.enabled is false - log.info "Candidate #{candidate.jobProfile.name} opted out of emails, not sending to them." return sentEmailFilterCallback true findParameters = "user": candidate._id @@ -138,7 +136,7 @@ internalCandidateUpdateTask = -> lockDurationMs = 2 * 60 * 1000 lockManager.setLock mailTaskName, lockDurationMs, (err) -> if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}!" - emailInternalCandidateUpdateReminder.apply {"mailTaskName":mailTaskName}, (err) -> + emailInternalCandidateUpdateReminder.call {"mailTaskName":mailTaskName}, (err) -> if err log.error "There was an error sending the internal candidate update reminder.: #{err}" else @@ -166,7 +164,7 @@ findNonApprovedCandidatesWhoUpdatedJobProfileToday = (cb) -> findParameters = "jobProfile.updated": $lte: @currentTime.toISOString() - gt: @beginningOfUTCDay.toISOString() + $gt: @beginningOfUTCDay.toISOString() "jobProfileApproved": false User.find(findParameters).select("_id jobProfile.name jobProfile.updated").lean().exec cb @@ -209,8 +207,8 @@ employerNewCandidatesAvailableTask = -> mailTaskName = "employerNewCandidatesAvailableTask" lockDurationMs = 2 * 60 * 1000 lockManager.setLock mailTaskName, lockDurationMs, (err) -> - if err? then return log.error "There was an error getting a task lock!" - emailEmployerNewCandidatesAvailable.apply {"mailTaskName":mailTaskName}, (err) -> + if err? then return log.error "There was an error getting a task lock!: #{err}" + emailEmployerNewCandidatesAvailable.call {"mailTaskName":mailTaskName}, (err) -> if err log.error "There was an error completing the new candidates available task: #{err}" else From 8da06f6c6b67d394dfcced1abffe27fc84dd352c Mon Sep 17 00:00:00 2001 From: Darredevil Date: Thu, 17 Jul 2014 03:18:06 +0300 Subject: [PATCH 15/21] language sensitive docs added --- app/templates/docs/components.jade | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/templates/docs/components.jade b/app/templates/docs/components.jade index ac92772f6..157bb7f39 100644 --- a/app/templates/docs/components.jade +++ b/app/templates/docs/components.jade @@ -26,5 +26,8 @@ block content li(id="#{component.get('name')}#{doc.name}") | #{doc.name} ul.specialList - li!=marked(doc.description) - \ No newline at end of file + if doc.description[language.substring(1,language.length-1)] + li!=marked(doc.description[language.substring(1,language.length-1)]) + else + li!=marked(doc.description) + From 83177c5f0026874337cedd2d94042b9dfd052f28 Mon Sep 17 00:00:00 2001 From: Darredevil Date: Thu, 17 Jul 2014 03:19:09 +0300 Subject: [PATCH 16/21] language sensitive docs added --- app/views/docs/components_view.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/views/docs/components_view.coffee b/app/views/docs/components_view.coffee index 0d4f9d5e2..181eacbbd 100644 --- a/app/views/docs/components_view.coffee +++ b/app/views/docs/components_view.coffee @@ -19,10 +19,20 @@ module.exports = class UnnamedView extends RootView onLoaded: -> console.log 'we have the components...', (c.get('name') for c in @componentDocs.models) console.log 'we have the attributes...', (c.attributes for c in @componentDocs.models) + if (me.get('aceConfig')?.language?) is false + console.log 'default language javascript' + else + console.log 'language is =', me.get('aceConfig').language + + #console.log 'test', @componentDocs.models[99].attributes.propertyDocumentation[1].description['python'] super() getRenderData: -> c = super() c.components = @componentDocs.models c.marked = marked + if (me.get('aceConfig')?.language?) is false + c.language = 'javascript' + else + c.language = JSON.stringify(me.get('aceConfig').language) c From d821e459d171d438b62ea750c01864258686aea8 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 19:46:06 -0700 Subject: [PATCH 17/21] Encode/decode emails as URIs This helps to handle special characters like plusses. --- server/routes/auth.coffee | 6 ++++-- server/routes/mail.coffee | 15 +++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/server/routes/auth.coffee b/server/routes/auth.coffee index 36c836741..dd9dffee7 100644 --- a/server/routes/auth.coffee +++ b/server/routes/auth.coffee @@ -117,7 +117,8 @@ module.exports.setup = (app) -> ) ) - app.get '/auth/unsubscribe', (req, res) -> + app.get '/auth/unsubscribe', (req, res) -> + req.query.email = decodeURIComponent(req.query.email) email = req.query.email unless req.query.email return errors.badInput res, 'No email provided to unsubscribe.' @@ -131,7 +132,7 @@ module.exports.setup = (app) -> 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}'" @@ -146,6 +147,7 @@ module.exports.setup = (app) -> else if req.query.employerNotes emails.employerNotes ?= {} emails.employerNotes.enabled = false + msg = "Unsubscribed #{req.query.email} from employer emails." else msg = "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!" diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 45f90ac2d..6490b4394 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -21,13 +21,13 @@ setupScheduledEmails = -> testForLockManager() mailTasks = [ taskFunction: candidateUpdateProfileTask - frequencyMs: 30 * 60 * 1000 #30 minutes + frequencyMs: 10 * 60 * 1000 #10 minutes , taskFunction: internalCandidateUpdateTask frequencyMs: 10 * 60 * 1000 #10 minutes , taskFunction: employerNewCandidatesAvailableTask - frequencyMs: 30 * 60 * 1000 #30 minutes + frequencyMs: 10 * 60 * 1000 #10 minutes ] for mailTask in mailTasks @@ -48,7 +48,7 @@ candidateUpdateProfileTask = -> end: generateWeekOffset currentDate, weekPair[1] name: weekPair[2] lockManager.setLock mailTaskName, lockDurationMs, (err) -> - if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}!" + if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}" async.each timeRanges, emailTimeRange.bind({mailTaskName: mailTaskName}), (err) -> if err log.error "There was an error sending the candidate profile update reminder emails: #{err}" @@ -108,6 +108,8 @@ sendReminderEmailToCandidate = (candidate, sendEmailCallback) -> if err? log.error "There was an error finding employers who signed up after #{candidate.jobProfile.updated}: #{err}" return sendEmailCallback err + if employersAfterCount < 2 + employersAfterCount = 2 context = email_id: "tem_CtTLsKQufxrxoPMn7upKiL" recipient: @@ -117,6 +119,7 @@ sendReminderEmailToCandidate = (candidate, sendEmailCallback) -> new_company: employersAfterCount company_name: "CodeCombat" user_profile: "http://codecombat.com/account/profile/#{candidate._id}" + recipient_address: encodeURIComponent(candidate.email) log.info "Sending #{@timeRange.name} update reminder to #{context.recipient.name}(#{context.recipient.address})" newSentMail = mailTask: @mailTaskName @@ -135,7 +138,7 @@ internalCandidateUpdateTask = -> mailTaskName = "internalCandidateUpdateTask" lockDurationMs = 2 * 60 * 1000 lockManager.setLock mailTaskName, lockDurationMs, (err) -> - if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}!" + if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}" emailInternalCandidateUpdateReminder.call {"mailTaskName":mailTaskName}, (err) -> if err log.error "There was an error sending the internal candidate update reminder.: #{err}" @@ -207,7 +210,7 @@ employerNewCandidatesAvailableTask = -> mailTaskName = "employerNewCandidatesAvailableTask" lockDurationMs = 2 * 60 * 1000 lockManager.setLock mailTaskName, lockDurationMs, (err) -> - if err? then return log.error "There was an error getting a task lock!: #{err}" + if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}" emailEmployerNewCandidatesAvailable.call {"mailTaskName":mailTaskName}, (err) -> if err log.error "There was an error completing the new candidates available task: #{err}" @@ -249,7 +252,6 @@ makeEmployerNamesEasilyAccessible = (allEmployers, cb) -> employersEmailedDigestMoreThanWeekAgoFilter = (employer, cb) -> if employer.emails?.employerNotes?.enabled is false - log.info "Employer #{employer.name}(#{employer.email}) opted out of emails, not sending to them." return sentEmailFilterCallback true findParameters = "user": employer._id @@ -286,6 +288,7 @@ sendEmployerNewCandidatesAvailableEmail = (employer, cb) -> new_candidates: numberOfCandidatesSinceLogin employer_company_name: employer.employerAt company_name: "CodeCombat" + recipient_address: encodeURIComponent(employer.email) if employer.name context.recipient.name = employer.name log.info "Sending available candidates update reminder to #{context.recipient.name}(#{context.recipient.address})" From c717e609eaace0e9e46ef06f05ba9f427cfba476 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 19:46:30 -0700 Subject: [PATCH 18/21] Initialize value of this.redisNotAvailable --- server/commons/LockManager.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/server/commons/LockManager.coffee b/server/commons/LockManager.coffee index bdd847bf7..bd0a89171 100644 --- a/server/commons/LockManager.coffee +++ b/server/commons/LockManager.coffee @@ -6,6 +6,7 @@ class LockManager constructor: -> unless config.isProduction or config.redis.host isnt "localhost" throw "You shouldn't be instantiating distributed locks unless in production." + @redisNotAvailable = true @redisClient = redis.createClient config.redis.port, config.redis.host @redisClient.on "ready", => log.info "Redis ready!" From 2a865a0ca06ddf453d9d02200f291a47d8def9dc Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Wed, 16 Jul 2014 19:49:48 -0700 Subject: [PATCH 19/21] Remove testing conditions --- server/commons/LockManager.coffee | 2 +- server/routes/mail.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/commons/LockManager.coffee b/server/commons/LockManager.coffee index bd0a89171..36936b236 100644 --- a/server/commons/LockManager.coffee +++ b/server/commons/LockManager.coffee @@ -4,7 +4,7 @@ log = require 'winston' class LockManager constructor: -> - unless config.isProduction or config.redis.host isnt "localhost" + unless config.isProduction throw "You shouldn't be instantiating distributed locks unless in production." @redisNotAvailable = true @redisClient = redis.createClient config.redis.port, config.redis.host diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 6490b4394..4098402b0 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -8,7 +8,7 @@ LevelSession = require '../levels/sessions/LevelSession' Level = require '../levels/Level' log = require 'winston' sendwithus = require '../sendwithus' -if config.isProduction or config.redis.host isnt "localhost" #TODO: Ask Nick and Scott to change their environment variables and change the deploy ones +if config.isProduction lockManager = require '../commons/LockManager' module.exports.setup = (app) -> From 337de506dd9786dc7b0d65a29b4d98e4fcc9ca9d Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Thu, 17 Jul 2014 07:15:14 -0700 Subject: [PATCH 20/21] Removed mail task from server mapping --- server/commons/mapping.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index faa0ef551..69f8abfc0 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -9,7 +9,6 @@ module.exports.handlers = 'thang_type': 'levels/thangs/thang_type_handler' 'user': 'users/user_handler' 'user_remark': 'users/remarks/user_remark_handler' - 'mail_task': 'mail/tasks/mail_task_handler' 'mail_sent': 'mail/sent/mail_sent_handler' 'achievement': 'achievements/achievement_handler' 'earned_achievement': 'achievements/earned_achievement_handler' From 58a80f2512e9d32bd83659985faf16d3f1191e8f Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Thu, 17 Jul 2014 07:17:25 -0700 Subject: [PATCH 21/21] Removed old comment --- server/routes/mail.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 4098402b0..0f648178e 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -187,7 +187,7 @@ sendInternalCandidateUpdateReminder = (candidate, cb) -> context = email_id: "tem_Ac7nhgKqatTHBCgDgjF5pE" recipient: - address: "team@codecombat.com" #Change to whatever email address is necessary + address: "team@codecombat.com" name: "The CodeCombat Team" email_data: new_candidate_profile: "https://codecombat.com/account/profile/#{candidate._id}"