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}")
|
li(id="#{component.get('name')}#{doc.name}")
|
||||||
| #{doc.name}
|
| #{doc.name}
|
||||||
ul.specialList
|
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: ->
|
onLoaded: ->
|
||||||
console.log 'we have the components...', (c.get('name') for c in @componentDocs.models)
|
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)
|
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()
|
super()
|
||||||
|
|
||||||
getRenderData: ->
|
getRenderData: ->
|
||||||
c = super()
|
c = super()
|
||||||
c.components = @componentDocs.models
|
c.components = @componentDocs.models
|
||||||
c.marked = marked
|
c.marked = marked
|
||||||
|
if (me.get('aceConfig')?.language?) is false
|
||||||
|
c.language = 'javascript'
|
||||||
|
else
|
||||||
|
c.language = JSON.stringify(me.get('aceConfig').language)
|
||||||
c
|
c
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
"express-useragent": "~0.0.9",
|
"express-useragent": "~0.0.9",
|
||||||
"gridfs-stream": "0.4.x",
|
"gridfs-stream": "0.4.x",
|
||||||
"stream-buffers": "0.2.x",
|
"stream-buffers": "0.2.x",
|
||||||
"sendwithus": "2.0.x",
|
"sendwithus": "2.1.x",
|
||||||
"aws-sdk": "~2.0.0",
|
"aws-sdk": "~2.0.0",
|
||||||
"bayesian-battle": "0.0.x",
|
"bayesian-battle": "0.0.x",
|
||||||
"redis": "",
|
"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'
|
'thang_type': 'levels/thangs/thang_type_handler'
|
||||||
'user': 'users/user_handler'
|
'user': 'users/user_handler'
|
||||||
'user_remark': 'users/remarks/user_remark_handler'
|
'user_remark': 'users/remarks/user_remark_handler'
|
||||||
|
'mail_sent': 'mail/sent/mail_sent_handler'
|
||||||
'achievement': 'achievements/achievement_handler'
|
'achievement': 'achievements/achievement_handler'
|
||||||
'earned_achievement': 'achievements/earned_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
|
email = req.query.email
|
||||||
unless req.query.email
|
unless req.query.email
|
||||||
return errors.badInput res, 'No email provided to unsubscribe.'
|
return errors.badInput res, 'No email provided to unsubscribe.'
|
||||||
|
@ -131,7 +132,7 @@ module.exports.setup = (app) ->
|
||||||
return errors.serverError res, 'Database failure.' if err
|
return errors.serverError res, 'Database failure.' if err
|
||||||
res.send "Unsubscribed #{req.query.email} from CodeCombat emails for #{session.levelName} #{session.team} ladder updates. Sorry to see you go! <p><a href='/play/ladder/#{session.levelID}#my-matches'>Ladder preferences</a></p>"
|
res.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()
|
res.end()
|
||||||
|
|
||||||
User.findOne({emailLower: req.query.email.toLowerCase()}).exec (err, user) ->
|
User.findOne({emailLower: req.query.email.toLowerCase()}).exec (err, user) ->
|
||||||
if not user
|
if not user
|
||||||
return errors.notFound res, "No user found with email '#{req.query.email}'"
|
return errors.notFound res, "No user found with email '#{req.query.email}'"
|
||||||
|
@ -143,7 +144,11 @@ module.exports.setup = (app) ->
|
||||||
emails.recruitNotes ?= {}
|
emails.recruitNotes ?= {}
|
||||||
emails.recruitNotes.enabled = false
|
emails.recruitNotes.enabled = false
|
||||||
msg = "Unsubscribed #{req.query.email} from recruiting emails."
|
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
|
else
|
||||||
msg = "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!"
|
msg = "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!"
|
||||||
emailSettings.enabled = false for emailSettings in _.values(emails)
|
emailSettings.enabled = false for emailSettings in _.values(emails)
|
||||||
|
|
|
@ -1,17 +1,330 @@
|
||||||
mail = require '../commons/mail'
|
mail = require '../commons/mail'
|
||||||
|
MailSent = require '../mail/sent/MailSent'
|
||||||
User = require '../users/User'
|
User = require '../users/User'
|
||||||
|
async = require 'async'
|
||||||
errors = require '../commons/errors'
|
errors = require '../commons/errors'
|
||||||
config = require '../../server_config'
|
config = require '../../server_config'
|
||||||
LevelSession = require '../levels/sessions/LevelSession'
|
LevelSession = require '../levels/sessions/LevelSession'
|
||||||
Level = require '../levels/Level'
|
Level = require '../levels/Level'
|
||||||
log = require 'winston'
|
log = require 'winston'
|
||||||
sendwithus = require '../sendwithus'
|
sendwithus = require '../sendwithus'
|
||||||
|
if config.isProduction
|
||||||
|
lockManager = require '../commons/LockManager'
|
||||||
|
|
||||||
module.exports.setup = (app) ->
|
module.exports.setup = (app) ->
|
||||||
app.all config.mail.mailchimpWebhook, handleMailchimpWebHook
|
app.all config.mail.mailchimpWebhook, handleMailchimpWebHook
|
||||||
app.get '/mail/cron/ladder-update', handleLadderUpdate
|
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
|
DEBUGGING = false
|
||||||
LADDER_PREGAME_INTERVAL = 2 * 3600 * 1000 # Send emails two hours before players last submitted.
|
LADDER_PREGAME_INTERVAL = 2 * 3600 * 1000 # Send emails two hours before players last submitted.
|
||||||
|
@ -28,6 +341,8 @@ isRequestFromDesignatedCronHandler = (req, res) ->
|
||||||
return false
|
return false
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
handleLadderUpdate = (req, res) ->
|
handleLadderUpdate = (req, res) ->
|
||||||
log.info('Going to see about sending ladder update emails.')
|
log.info('Going to see about sending ladder update emails.')
|
||||||
requestIsFromDesignatedCronHandler = isRequestFromDesignatedCronHandler req, res
|
requestIsFromDesignatedCronHandler = isRequestFromDesignatedCronHandler req, res
|
||||||
|
@ -151,6 +466,7 @@ getScoreHistoryGraphURL = (session, daysAgo) ->
|
||||||
chartData = times.join(',') + '|' + scores.join(',')
|
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}"
|
"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) ->
|
handleMailchimpWebHook = (req, res) ->
|
||||||
post = req.body
|
post = req.body
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,10 @@ config.mongo =
|
||||||
host: process.env.COCO_MONGO_HOST or 'localhost'
|
host: process.env.COCO_MONGO_HOST or 'localhost'
|
||||||
db: process.env.COCO_MONGO_DATABASE_NAME or 'coco'
|
db: process.env.COCO_MONGO_DATABASE_NAME or 'coco'
|
||||||
mongoose_replica_string: process.env.COCO_MONGO_MONGOOSE_REPLICA_STRING or ''
|
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
|
if config.unittest
|
||||||
config.port += 1
|
config.port += 1
|
||||||
|
|
|
@ -3,97 +3,98 @@ describe 'Ellipse', ->
|
||||||
Rectangle = require 'lib/world/rectangle'
|
Rectangle = require 'lib/world/rectangle'
|
||||||
Vector = require 'lib/world/vector'
|
Vector = require 'lib/world/vector'
|
||||||
|
|
||||||
#it 'contains its own center', ->
|
it 'contains its own center', ->
|
||||||
# ellipse = new Ellipse 0, 0, 10, 10
|
ellipse = new Ellipse 0, 0, 10, 10
|
||||||
# expect(ellipse.containsPoint(new Vector 0, 0)).toBe true
|
expect(ellipse.containsPoint(new Vector 0, 0)).toBe true
|
||||||
#
|
|
||||||
#it 'contains a point when rotated', ->
|
it 'contains a point when rotated', ->
|
||||||
# ellipse = new Ellipse 0, -20, 40, 40, 3 * Math.PI / 4
|
ellipse = new Ellipse 0, -20, 40, 40, 3 * Math.PI / 4
|
||||||
# p = new Vector 0, 2
|
expect(ellipse.containsPoint new Vector(0, 0)).toBe true
|
||||||
# expect(ellipse.containsPoint(p, true)).toBe true
|
expect(ellipse.containsPoint new Vector(0, 2)).toBe false
|
||||||
#
|
|
||||||
#it 'contains more points properly', ->
|
it 'contains more points properly', ->
|
||||||
# # ellipse with y major axis, off-origin center, and 45 degree rotation
|
# ellipse with y major axis, off-origin center, and 45 degree rotation
|
||||||
# ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4
|
ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4
|
||||||
# expect(ellipse.contains new Vector(1, 2)).toBe true
|
expect(ellipse.containsPoint new Vector(1, 2)).toBe true
|
||||||
# expect(ellipse.contains new Vector(-1, 3)).toBe true
|
expect(ellipse.containsPoint new Vector(-1, 3)).toBe true
|
||||||
# expect(ellipse.contains new Vector(0, 4)).toBe true
|
expect(ellipse.containsPoint new Vector(0, 4)).toBe true
|
||||||
# expect(ellipse.contains new Vector(1, 4)).toBe true
|
expect(ellipse.containsPoint new Vector(1, 4)).toBe true
|
||||||
# expect(ellipse.contains new Vector(3, 0)).toBe true
|
expect(ellipse.containsPoint new Vector(3, 0)).toBe true
|
||||||
# expect(ellipse.contains new Vector(1, 0)).toBe true
|
expect(ellipse.containsPoint new Vector(1, 0)).toBe true
|
||||||
# expect(ellipse.contains new Vector(0, 1)).toBe true
|
expect(ellipse.containsPoint new Vector(0, 1)).toBe true
|
||||||
# expect(ellipse.contains new Vector(-1, 2)).toBe true
|
expect(ellipse.containsPoint new Vector(-1, 2)).toBe true
|
||||||
# expect(ellipse.contains new Vector(2, 2)).toBe true
|
expect(ellipse.containsPoint new Vector(2, 2)).toBe true
|
||||||
# expect(ellipse.contains new Vector(0, 0)).toBe false
|
expect(ellipse.containsPoint new Vector(0, 0)).toBe false
|
||||||
# expect(ellipse.contains new Vector(0, 5)).toBe false
|
expect(ellipse.containsPoint new Vector(0, 5)).toBe false
|
||||||
# expect(ellipse.contains new Vector(3, 4)).toBe false
|
expect(ellipse.containsPoint new Vector(3, 4)).toBe false
|
||||||
# expect(ellipse.contains new Vector(4, 0)).toBe false
|
expect(ellipse.containsPoint new Vector(4, 0)).toBe false
|
||||||
# expect(ellipse.contains new Vector(2, -1)).toBe false
|
expect(ellipse.containsPoint new Vector(2, -1)).toBe false
|
||||||
# expect(ellipse.contains new Vector(0, -3)).toBe false
|
expect(ellipse.containsPoint new Vector(0, -3)).toBe false
|
||||||
# expect(ellipse.contains new Vector(-2, -2)).toBe false
|
expect(ellipse.containsPoint new Vector(-2, -2)).toBe false
|
||||||
# expect(ellipse.contains new Vector(-2, 0)).toBe false
|
expect(ellipse.containsPoint new Vector(-2, 0)).toBe false
|
||||||
# expect(ellipse.contains new Vector(-2, 4)).toBe false
|
expect(ellipse.containsPoint new Vector(-2, 4)).toBe false
|
||||||
#
|
|
||||||
#it 'correctly calculates distance to a faraway point', ->
|
xit 'correctly calculates distance to a faraway point', ->
|
||||||
# ellipse = new Ellipse 100, 50, 20, 40
|
# TODO: this is the correct distance if the ellipse were a rectangle, but need to update for actual ellipse expected distances.
|
||||||
# p = new Vector 200, 300
|
ellipse = new Ellipse 100, 50, 20, 40
|
||||||
# d = 10 * Math.sqrt(610)
|
p = new Vector 200, 300
|
||||||
# expect(ellipse.distanceToPoint(p)).toBeCloseTo d
|
d = 10 * Math.sqrt(610)
|
||||||
# ellipse.rotation = Math.PI / 2
|
expect(ellipse.distanceToPoint(p)).toBeCloseTo d
|
||||||
# d = 80 * Math.sqrt(10)
|
ellipse.rotation = Math.PI / 2
|
||||||
# expect(ellipse.distanceToPoint(p)).toBeCloseTo d
|
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
|
it 'does not modify itself or target Vector when calculating distance', ->
|
||||||
# ellipse2 = ellipse.copy()
|
ellipse = new Ellipse -100, -200, 1, 100
|
||||||
# p = new Vector -100.25, -101
|
ellipse2 = ellipse.copy()
|
||||||
# p2 = p.copy()
|
p = new Vector -100.25, -101
|
||||||
# ellipse.distanceToPoint(p)
|
p2 = p.copy()
|
||||||
# expect(p.x).toEqual p2.x
|
ellipse.distanceToPoint(p)
|
||||||
# expect(p.y).toEqual p2.y
|
expect(p.x).toEqual p2.x
|
||||||
# expect(ellipse.x).toEqual ellipse2.x
|
expect(p.y).toEqual p2.y
|
||||||
# expect(ellipse.y).toEqual ellipse2.y
|
expect(ellipse.x).toEqual ellipse2.x
|
||||||
# expect(ellipse.width).toEqual ellipse2.width
|
expect(ellipse.y).toEqual ellipse2.y
|
||||||
# expect(ellipse.height).toEqual ellipse2.height
|
expect(ellipse.width).toEqual ellipse2.width
|
||||||
# expect(ellipse.rotation).toEqual ellipse2.rotation
|
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
|
it 'correctly calculates distance to contained point', ->
|
||||||
# ellipse2 = ellipse.copy()
|
ellipse = new Ellipse -100, -200, 1, 100
|
||||||
# p = new Vector -100.25, -160
|
ellipse2 = ellipse.copy()
|
||||||
# p2 = p.copy()
|
p = new Vector -100.25, -160
|
||||||
# expect(ellipse.distanceToPoint(p)).toBe 0
|
p2 = p.copy()
|
||||||
# ellipse.rotation = 0.00000001 * Math.PI
|
expect(ellipse.distanceToPoint(p)).toBe 0
|
||||||
# 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
|
it 'AABB works when not rotated', ->
|
||||||
# rect = new Rectangle 10, 20, 30, 40
|
ellipse = new Ellipse 10, 20, 30, 40
|
||||||
# aabb1 = ellipse.axisAlignedBoundingBox()
|
rect = new Rectangle 10, 20, 30, 40
|
||||||
# aabb2 = ellipse.axisAlignedBoundingBox()
|
aabb1 = ellipse.axisAlignedBoundingBox()
|
||||||
# for prop in ['x', 'y', 'width', 'height']
|
aabb2 = ellipse.axisAlignedBoundingBox()
|
||||||
# expect(aabb1[prop]).toBe aabb2[prop]
|
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
|
it 'AABB works when rotated', ->
|
||||||
# rect = new Rectangle 10, 20, 30, 40, Math.PI / 3
|
ellipse = new Ellipse 10, 20, 30, 40, Math.PI / 3
|
||||||
# aabb1 = ellipse.axisAlignedBoundingBox()
|
rect = new Rectangle 10, 20, 30, 40, Math.PI / 3
|
||||||
# aabb2 = ellipse.axisAlignedBoundingBox()
|
aabb1 = ellipse.axisAlignedBoundingBox()
|
||||||
# for prop in ['x', 'y', 'width', 'height']
|
aabb2 = ellipse.axisAlignedBoundingBox()
|
||||||
# expect(aabb1[prop]).toBe aabb2[prop]
|
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
|
it 'calculates ellipse intersections properly', ->
|
||||||
# ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4
|
# ellipse with y major axis, off-origin center, and 45 degree rotation
|
||||||
# expect(ellipse.intersectsShape new Rectangle(0, 0, 2, 2, 0)).toBe true
|
ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4
|
||||||
# expect(ellipse.intersectsShape new Rectangle(0, -1, 2, 3, 0)).toBe true
|
expect(ellipse.intersectsShape new Rectangle(0, 0, 2, 2, 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(0, -1, 2, 3, 0)).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, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe true
|
||||||
# expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).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, Math.PI / 4)).toBe false
|
expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true
|
||||||
# expect(ellipse.intersectsShape new Rectangle(-2, -2, 2, 2, 0)).toBe false
|
expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).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(-2, -2, 2, 2, 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(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, 0)).toBe false
|
||||||
# expect(ellipse.intersectsShape new Rectangle(-2, 0, 2, 2, 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(0, -2, 2, 2, 0)).toBe false
|
expect(ellipse.intersectsShape new Rectangle(-2, 0, 2, 2, 0)).toBe false
|
||||||
# expect(ellipse.intersectsShape new Rectangle(1, 2, 1, 1, 0)).toBe true
|
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', ->
|
it 'can tell when a point is on a line or segment', ->
|
||||||
lineSegment = new LineSegment v00, v11
|
lineSegment = new LineSegment v00, v11
|
||||||
expect(lineSegment.pointOnLine v22, false).toBe true
|
expect(lineSegment.pointOnLine v22, false).toBe true
|
||||||
#expect(lineSegment.pointOnLine v22, true).toBe false
|
expect(lineSegment.pointOnLine v22, true).toBe false
|
||||||
#expect(lineSegment.pointOnLine v00, false).toBe true
|
expect(lineSegment.pointOnLine v00, false).toBe true
|
||||||
#expect(lineSegment.pointOnLine v00, true).toBe true
|
expect(lineSegment.pointOnLine v00, true).toBe true
|
||||||
#expect(lineSegment.pointOnLine v11, true).toBe true
|
expect(lineSegment.pointOnLine v11, true).toBe true
|
||||||
#expect(lineSegment.pointOnLine v11, false).toBe true
|
expect(lineSegment.pointOnLine v11, false).toBe true
|
||||||
#expect(lineSegment.pointOnLine v34, false).toBe false
|
expect(lineSegment.pointOnLine v34, false).toBe false
|
||||||
#expect(lineSegment.pointOnLine v34, true).toBe false
|
expect(lineSegment.pointOnLine v34, true).toBe false
|
||||||
|
|
||||||
it 'correctly calculates distance to points', ->
|
it 'correctly calculates distance to points', ->
|
||||||
lineSegment = new LineSegment v00, v11
|
lineSegment = new LineSegment v00, v11
|
||||||
|
|
Reference in a new issue