mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-29 18:45:48 -05:00
Merge pull request #1343 from codecombat/master
Master into production (includes email system launch)
This commit is contained in:
commit
2df9c96c45
13 changed files with 532 additions and 109 deletions
17
app/schemas/models/mail_sent.coffee
Normal file
17
app/schemas/models/mail_sent.coffee
Normal file
|
@ -0,0 +1,17 @@
|
|||
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
|
||||
metadata: c.object {}, {}
|
||||
|
||||
c.extendBasicProperties MailSentSchema, 'mail.sent'
|
||||
|
||||
module.exports = MailSentSchema
|
||||
|
|
@ -26,5 +26,8 @@ block content
|
|||
li(id="#{component.get('name')}#{doc.name}")
|
||||
| #{doc.name}
|
||||
ul.specialList
|
||||
li!=marked(doc.description)
|
||||
|
||||
if doc.description[language.substring(1,language.length-1)]
|
||||
li!=marked(doc.description[language.substring(1,language.length-1)])
|
||||
else
|
||||
li!=marked(doc.description)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": "",
|
||||
|
|
43
server/commons/LockManager.coffee
Normal file
43
server/commons/LockManager.coffee
Normal file
|
@ -0,0 +1,43 @@
|
|||
config = require '../../server_config'
|
||||
redis = require 'redis'
|
||||
log = require 'winston'
|
||||
|
||||
class LockManager
|
||||
constructor: ->
|
||||
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
|
||||
@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
|
||||
if res is "OK"
|
||||
@lockValues[lockName] = randomNumber
|
||||
return cb null, "Lock set!"
|
||||
unless res
|
||||
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
|
||||
cb null, "The lock was released!"
|
||||
else
|
||||
cb "The lock was not released.", null
|
||||
|
||||
module.exports = new LockManager()
|
|
@ -9,6 +9,7 @@ module.exports.handlers =
|
|||
'thang_type': 'levels/thangs/thang_type_handler'
|
||||
'user': 'users/user_handler'
|
||||
'user_remark': 'users/remarks/user_remark_handler'
|
||||
'mail_sent': 'mail/sent/mail_sent_handler'
|
||||
'achievement': 'achievements/achievement_handler'
|
||||
'earned_achievement': 'achievements/earned_achievement_handler'
|
||||
|
||||
|
|
11
server/mail/sent/MailSent.coffee
Normal file
11
server/mail/sent/MailSent.coffee
Normal file
|
@ -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('mail.sent', MailSent)
|
12
server/mail/sent/mail_sent_handler.coffee
Normal file
12
server/mail/sent/mail_sent_handler.coffee
Normal file
|
@ -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()
|
|
@ -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! <p><a href='/play/ladder/#{session.levelID}#my-matches'>Ladder preferences</a></p>"
|
||||
res.end()
|
||||
|
||||
|
||||
User.findOne({emailLower: req.query.email.toLowerCase()}).exec (err, user) ->
|
||||
if not user
|
||||
return errors.notFound res, "No user found with email '#{req.query.email}'"
|
||||
|
@ -143,7 +144,11 @@ 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)
|
||||
|
|
|
@ -1,17 +1,330 @@
|
|||
mail = require '../commons/mail'
|
||||
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
|
||||
lockManager = require '../commons/LockManager'
|
||||
|
||||
module.exports.setup = (app) ->
|
||||
app.all config.mail.mailchimpWebhook, handleMailchimpWebHook
|
||||
app.get '/mail/cron/ladder-update', handleLadderUpdate
|
||||
if lockManager
|
||||
setupScheduledEmails()
|
||||
|
||||
setupScheduledEmails = ->
|
||||
testForLockManager()
|
||||
mailTasks = [
|
||||
taskFunction: candidateUpdateProfileTask
|
||||
frequencyMs: 10 * 60 * 1000 #10 minutes
|
||||
,
|
||||
taskFunction: internalCandidateUpdateTask
|
||||
frequencyMs: 10 * 60 * 1000 #10 minutes
|
||||
,
|
||||
taskFunction: employerNewCandidatesAvailableTask
|
||||
frequencyMs: 10 * 60 * 1000 #10 minutes
|
||||
]
|
||||
|
||||
for mailTask in mailTasks
|
||||
setInterval mailTask.taskFunction, mailTask.frequencyMs
|
||||
|
||||
testForLockManager = -> unless lockManager then throw "The system isn't configured to do distributed locking!"
|
||||
|
||||
### Candidate Update Reminder Task ###
|
||||
|
||||
candidateUpdateProfileTask = ->
|
||||
mailTaskName = "candidateUpdateProfileTask"
|
||||
lockDurationMs = 2 * 60 * 1000
|
||||
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}: #{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}"
|
||||
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) ->
|
||||
async.reject unfilteredCandidates, candidateFilter.bind(waterfallContext), cb.bind(null, null)
|
||||
(filteredCandidates, cb) ->
|
||||
async.each filteredCandidates, sendReminderEmailToCandidate.bind(waterfallContext), cb
|
||||
], emailTimeRangeCallback
|
||||
|
||||
findAllCandidatesWithinTimeRange = (cb) ->
|
||||
findParameters =
|
||||
"jobProfile.updated":
|
||||
$gt: @timeRange.start
|
||||
$lte: @timeRange.end
|
||||
"jobProfileApproved": true
|
||||
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
|
||||
return sentEmailFilterCallback true
|
||||
findParameters =
|
||||
"user": candidate._id
|
||||
"mailTask": @mailTaskName
|
||||
"metadata.timeRangeName": @timeRange.name
|
||||
"metadata.updated": candidate.jobProfile.updated
|
||||
MailSent.find(findParameters).lean().exec (err, sentMail) ->
|
||||
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 =
|
||||
$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) =>
|
||||
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:
|
||||
address: candidate.email
|
||||
name: candidate.jobProfile.name
|
||||
email_data:
|
||||
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
|
||||
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 candidate update reminder email: #{err} with result #{result}" if err
|
||||
sendEmailCallback null
|
||||
### End Candidate Update Reminder Task ###
|
||||
### Internal Candidate Update Reminder Email ###
|
||||
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}: #{err}"
|
||||
emailInternalCandidateUpdateReminder.call {"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 = (internalCandidateUpdateReminderCallback) ->
|
||||
currentTime = new Date()
|
||||
beginningOfUTCDay = new Date()
|
||||
beginningOfUTCDay.setUTCHours(0,0,0,0)
|
||||
asyncContext =
|
||||
"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
|
||||
], internalCandidateUpdateReminderCallback
|
||||
|
||||
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) ->
|
||||
findParameters =
|
||||
"user": candidate._id
|
||||
"mailTask": @mailTaskName
|
||||
"metadata.beginningOfUTCDay": @beginningOfUTCDay
|
||||
MailSent.find(findParameters).lean().exec (err, sentMail) ->
|
||||
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:
|
||||
address: "team@codecombat.com"
|
||||
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
|
||||
metadata:
|
||||
beginningOfUTCDay: @beginningOfUTCDay
|
||||
|
||||
MailSent.create newSentMail, (err) ->
|
||||
if err? then return cb err
|
||||
sendwithus.api.send context, (err, result) ->
|
||||
log.error "Error sending interal candidate update email: #{err} with result #{result}" if err
|
||||
cb null
|
||||
|
||||
### End Internal Candidate Update Reminder Email ###
|
||||
### Employer New Candidates Available Email ###
|
||||
employerNewCandidatesAvailableTask = ->
|
||||
mailTaskName = "employerNewCandidatesAvailableTask"
|
||||
lockDurationMs = 2 * 60 * 1000
|
||||
lockManager.setLock mailTaskName, lockDurationMs, (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}"
|
||||
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
|
||||
"mailTaskName": @mailTaskName
|
||||
|
||||
async.waterfall [
|
||||
findAllEmployers
|
||||
makeEmployerNamesEasilyAccessible
|
||||
(allEmployers, cb) ->
|
||||
async.reject allEmployers, employersEmailedDigestMoreThanWeekAgoFilter.bind(asyncContext), cb.bind(null,null)
|
||||
(employersToEmail, cb) ->
|
||||
async.each employersToEmail, sendEmployerNewCandidatesAvailableEmail.bind(asyncContext), cb
|
||||
], emailEmployerNewCandidatesAvailableCallback
|
||||
|
||||
findAllEmployers = (cb) ->
|
||||
findParameters =
|
||||
"employerAt":
|
||||
$exists: true
|
||||
permissions: "employer"
|
||||
selection = "_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName activity dateCreated emails"
|
||||
User.find(findParameters).select(selection).lean().exec cb
|
||||
|
||||
makeEmployerNamesEasilyAccessible = (allEmployers, cb) ->
|
||||
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) ->
|
||||
if employer.emails?.employerNotes?.enabled is false
|
||||
return sentEmailFilterCallback true
|
||||
findParameters =
|
||||
"user": employer._id
|
||||
"mailTask": @mailTaskName
|
||||
"sent":
|
||||
$gt: new Date(@currentTime.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
MailSent.find(findParameters).lean().exec (err, sentMail) ->
|
||||
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
|
||||
countParameters =
|
||||
"jobProfileApproved": 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
|
||||
email_data:
|
||||
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})"
|
||||
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 employer candidates available email: #{err} with result #{result}" if err
|
||||
cb null
|
||||
|
||||
### 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.
|
||||
|
@ -28,6 +341,8 @@ isRequestFromDesignatedCronHandler = (req, res) ->
|
|||
return false
|
||||
return true
|
||||
|
||||
|
||||
|
||||
handleLadderUpdate = (req, res) ->
|
||||
log.info('Going to see about sending ladder update emails.')
|
||||
requestIsFromDesignatedCronHandler = isRequestFromDesignatedCronHandler req, res
|
||||
|
@ -151,6 +466,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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue