Merge pull request from codecombat/master

Master into production (includes email system launch)
This commit is contained in:
Michael Schmatz 2014-07-17 08:57:04 -07:00
commit 2df9c96c45
13 changed files with 532 additions and 109 deletions

View 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

View file

@ -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)

View file

@ -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

View file

@ -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": "",

View 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()

View file

@ -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'

View 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)

View 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()

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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