Merge pull request #918 from codecombat/master

Merge master into production
This commit is contained in:
Michael Schmatz 2014-04-25 08:49:20 -07:00
commit 2435846451
12 changed files with 268 additions and 10 deletions

View file

@ -1,5 +1,6 @@
FacebookHandler = require 'lib/FacebookHandler'
GPlusHandler = require 'lib/GPlusHandler'
LinkedInHandler = require 'lib/LinkedInHandler'
locale = require 'locale/locale'
{me} = require 'lib/auth'
Tracker = require 'lib/Tracker'
@ -35,7 +36,7 @@ Application = initialize: ->
@facebookHandler = new FacebookHandler()
@gplusHandler = new GPlusHandler()
$(document).bind 'keydown', preventBackspace
@linkedinHandler = new LinkedInHandler()
preload(COMMON_FILES)
$.i18n.init {
lng: me?.lang() ? 'en'

View file

@ -42,7 +42,18 @@
<script>require('initialize');</script>
<!-- begin LinkedIn code -->
<script>
window.linkedInAsyncInit = function() {
Backbone.Mediator.publish('linkedin-loaded');
};
</script>
<script type="text/javascript" src="http://platform.linkedin.com/in.js">
api_key: 75v8mv4ictvmx6
onLoad: linkedInAsyncInit
authorize: true
</script>
<!-- end LinkedIn code -->
<!-- begin segment.io code -->
<script type="text/javascript">
var analytics=analytics||[];(function(){var e=["identify","track","trackLink","trackForm","trackClick","trackSubmit","page","pageview","ab","alias","ready","group"],t=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var n=0;n<e.length;n++)analytics[e[n]]=t(e[n])})(),analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n)};
@ -70,6 +81,7 @@
/* custom configuration goes here (www.olark.com/documentation) */
olark.identify('1451-787-10-5544');/*]]>*/</script>
<!-- end olark code -->
</head>
<body class="nano clearfix">

View file

@ -0,0 +1,28 @@
CocoClass = require 'lib/CocoClass'
{me} = require 'lib/auth'
{backboneFailure} = require 'lib/errors'
storage = require 'lib/storage'
module.exports = LinkedInHandler = class LinkedInHandler extends CocoClass
constructor: ->
super()
subscriptions:
'linkedin-loaded':'onLinkedInLoaded'
onLinkedInLoaded: (e) =>
IN.Event.on IN, "auth", @onLinkedInAuth
onLinkedInAuth: (e) => console.log "Authorized with LinkedIn"
constructEmployerAgreementObject: (cb) =>
IN.API.Profile("me")
.fields(["positions","public-profile-url","id","first-name","last-name","email-address"])
.error(cb)
.result (profiles) =>
cb null, profiles.values[0]
destroy: ->
super()

View file

@ -19,6 +19,14 @@ module.exports.createUser = (userObject, failure=backboneFailure, nextURL=null)
error: failure,
success: -> if nextURL then window.location.href = nextURL else window.location.reload()
})
module.exports.createUserWithoutReload = (userObject, failure=backboneFailure) ->
user = new User(userObject)
user.save({}, {
error: failure
success: ->
Backbone.Mediator.publish("created-user-without-reload")
})
module.exports.loginUser = (userObject, failure=genericFailure) ->
jqxhr = $.post('/auth/login',

View file

@ -53,7 +53,7 @@ UserSchema = c.object {},
#Internationalization stuff
preferredLanguage: {type: 'string', default: 'en', 'enum': c.getLanguageCodeArray()}
signedCLA: c.date({title: 'Date Signed the CLA'})
wizard: c.object {},
colorConfig: c.object {additionalProperties: c.colorConfig()}
@ -109,6 +109,11 @@ UserSchema = c.object {},
jobProfileApproved: {title: 'Job Profile Approved', type: 'boolean', description: 'Whether your profile has been approved by CodeCombat.'}
jobProfileNotes: {type: 'string', maxLength: 1000, title: 'Our Notes', description: "CodeCombat's notes on the candidate.", format: 'markdown', default: ''}
employerAt: c.shortString {description: "If given employer permissions to view job candidates, for which employer?"}
signedEmployerAgreement: c.object {},
linkedinID: c.shortString {title:"LinkedInID", description: "The user's LinkedIn ID when they signed the contract."}
date: c.date {title: "Date signed employer agreement"}
data: c.object
c.extendBasicProperties UserSchema, 'user'

View file

@ -133,6 +133,14 @@ a[data-toggle="modal"]
background-color: transparent
margin: 0 14px
border-bottom-color: #ccc
.modal-footer.linkedin
text-align: center
.signin-text
font-size: 15px
padding-bottom: 10px
.login-link
cursor: pointer
// Bigger versions of some Bootstrap icons
// TODO: make the non-white versions of these if we ever need them

View file

@ -1,4 +1,6 @@
#employers-view
#see-candidates
cursor: pointer
.tablesorter
//img
// display: none

View file

@ -12,9 +12,11 @@ block content
span(data-i18n="employers.candidates_count_many") many
|
span(data-i18n="employers.candidates_count_suffix") highly skilled and vetted developers looking for work.
h3
a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/employer_signup", data-i18n="employers.contact_george") Contact George to see our candidates
if !isEmployer
h3
a#see-candidates(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/employer_signup") Click here to see candidates
if candidates.length
table.table.table-condensed.table-hover.table-responsive.tablesorter
thead

View file

@ -1,9 +1,68 @@
extends /templates/modal/modal_base
block modal-header-content
h3(data-i18n="employer_signup.title") Hire CodeCombat Players
if userIsAnonymous || !userIsAuthorized
h3(data-i18n="employer_signup.title") Sign up to hire CodeCombat players!
else
h3 CodeCombat Placement Agreement
block modal-body-content
h4(data-i18n="employer_signup.sub_heading") Let us find your next brilliant developers.
if userIsAnonymous
if userIsAuthorized
| You appear to be authorized on CodeCombat with LinkedIn.
else
h4(data-i18n="employer_signup.sub_heading") Let us find your next brilliant developers.
p Create an account to get started!
.form
.form-group
label.control-label(for="signup-email", data-i18n="general.email") Email
input#signup-email.form-control.input-large(name="email",type="email")
.form-group
label.control-label(for="signup-password", data-i18n="general.password") Password
input#signup-password.input-large.form-control(name="password", type="password")
else if !userIsAuthorized
.modal-footer.linkedin
p Please sign into your LinkedIn account to verify your identity.
script(type="in/Login" id="linkedInAuthButton" data-onAuth="contractCallback")
p(data-i18n="employer_signup.pitch_body") When you hire one of our players, you will pay CodeCombat 18% of her first-year salary, payable within 30 days of when she starts working. We will fully refund our placement fee if she leaves or is fired within 90 days. Cool? Email george@codecombat.com to get set up with employer permissions to see our candidates.
else
| Please agree to our terms before accessing our candidates.
br
br
b Who we are:
| CodeCombat is a programming game that both teaches and vets programmers. If you accept this agreement, we will let you hire the most talented developers on our platform.
br
br
b Placement fee:
| If you hire our any of our players, you agree to pay us 15% of the candidate's first year annualized starting base salary. The fee is due on the first day that the candidate is employed and is 100% refundable for up to 90 day if the candidate doesn't remain employed at the company for any reason.
br
br
b Interns are free:
| We will not bill you for interns and part time hires (remote or onsite) hired through this site, provided they do not become full time hires within 1 year of their start date. If they do become full time hires within 1 year of their start date, we will invoice you 15% of their first year's annualized starting base salary on their first day of full time employment. For these hires, the 90 day guarantee does not apply.
br
br
| By clicking Agree, you are agreeing to CodeCombat's Placement Agreement on behalf of your company. You also consent to CodeCombat storing basic LinkedIn profile data for verification purposes, including your name, email, public profile URL, and work history.
block modal-footer
if userIsAnonymous
if !userIsAuthorized
.modal-footer.linkedin
b.signin-text Sign in with LinkedIn to complete the registration process.
script(type="in/Login" id="linkedInAuthButton" data-onAuth="contractCallback")
br
br
| Already have a CodeCombat account?
a.login-link(data-toggle="coco-modal", data-target="modal/login") Log in to continue!
else
.modal-footer.linkedin
a.login-link(data-toggle="coco-modal", data-target="modal/login") Please log in to continue.
else if !userIsAnonymous && !userIsAuthorized
.modal-footer.linkedin
| We will record your name and work history for verification purposes.
else if userIsAuthorized && !userHasSignedContract
.modal-footer.linkedin
button.btn.btn-primary(id="contract-agreement-button") I agree
else
.modal-footer.linkedin
| Thanks #{firstName}! You've already agreed to the contract.

View file

@ -2,6 +2,7 @@ View = require 'views/kinds/RootView'
template = require 'templates/employers'
app = require 'application'
User = require 'models/User'
{me} = require 'lib/auth'
CocoCollection = require 'collections/CocoCollection'
EmployerSignupView = require 'views/modal/employer_signup_modal'
@ -27,6 +28,9 @@ module.exports = class EmployersView extends View
getRenderData: ->
c = super()
c.candidates = @candidates.models
userPermissions = me.get('permissions') || []
c.isEmployer = _.contains userPermissions, "employer"
c.moment = moment
c

View file

@ -1,7 +1,111 @@
View = require 'views/kinds/ModalView'
template = require 'templates/modal/employer_signup_modal'
forms = require('lib/forms')
User = require 'models/User'
auth = require('lib/auth')
me = auth.me
module.exports = class EmployerSignupView extends View
id: "employer-signup"
template: template
closeButton: true
subscriptions:
"server-error": "onServerError"
"created-user-without-reload": "linkedInAuth"
events:
"click #contract-agreement-button": "agreeToContract"
constructor: (options) ->
super(options)
@authorizedWithLinkedIn = IN?.User?.isAuthorized()
window.tracker?.trackEvent 'Started Employer Signup'
@reloadWhenClosed = false
window.contractCallback = =>
@authorizedWithLinkedIn = IN?.User?.isAuthorized()
@render()
onServerError: (e) ->
@disableModalInProgress(@$el)
afterInsert: ->
super()
linkedInButtonParentElement = document.getElementById("linkedInAuthButton")?.parentNode
if linkedInButtonParentElement
IN.parse()
if me.get('anonymous')
$(".IN-widget").get(0).addEventListener('click', @createAccount, true)
console.log "Parsed linkedin button element!"
console.log linkedInButtonParentElement
getRenderData: ->
context = super()
context.userIsAuthorized = @authorizedWithLinkedIn
context.userHasSignedContract = "employer" in me.get("permissions")
context.userIsAnonymous = context.me.get('anonymous')
context
agreeToContract: ->
application.linkedinHandler.constructEmployerAgreementObject (err, profileData) =>
if err? then return handleAgreementFailure err
$.ajax
url: "/db/user/#{me.id}/agreeToEmployerAgreement"
data: profileData
type: "POST"
success: @handleAgreementSuccess
error: @handleAgreementFailure
handleAgreementSuccess: (result) ->
window.tracker?.trackEvent 'Employer Agreed to Contract'
me.fetch()
window.location.reload()
handleAgreementFailure: (error) ->
alert "There was an error signing the contract. Please contact team@codecombat.com with this error: #{error.responseText}"
createAccount: (e) =>
window.tracker?.trackEvent 'Finished Employer Signup'
console.log "Tried to create account!"
e.stopPropagation()
forms.clearFormAlerts(@$el)
userObject = forms.formToObject @$el
delete userObject.subscribe
for key, val of me.attributes when key in ["preferredLanguage", "testGroupNumber", "dateCreated", "wizardColor1", "name", "music", "volume", "emails"]
userObject[key] ?= val
subscribe = true
#TODO: Enable all email subscriptions
userObject.emails ?= {}
userObject.emails.generalNews ?= {}
userObject.emails.generalNews.enabled = subscribe
res = tv4.validateMultiple userObject, User.schema
return forms.applyErrorsToForm(@$el, res.errors) unless res.valid
window.tracker?.trackEvent 'Finished Signup'
@enableModalInProgress(@$el)
auth.createUserWithoutReload userObject, null
linkedInAuth: (e) =>
console.log "Authorizing with linkedin"
@listenTo me,"sync", ->
IN.User.authorize(@recordUserDetails, @)
me.fetch()
@reloadWhenClosed = true
recordUserDetails: (e) =>
#TODO: refactor this out
@render()
destroy: ->
reloadWhenClosed = @reloadWhenClosed
super()
if reloadWhenClosed
window.location.reload()

View file

@ -14,7 +14,7 @@ LevelSessionHandler = require '../levels/sessions/level_session_handler'
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset']
privateProperties = [
'permissions', 'email', 'firstName', 'lastName', 'gender', 'facebookID',
'gplusID', 'music', 'volume', 'aceConfig', 'employerAt'
'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement'
]
candidateProperties = [
'jobProfile', 'jobProfileApproved', 'jobProfileNotes'
@ -182,6 +182,7 @@ UserHandler = class UserHandler extends Handler
getByRelationship: (req, res, args...) ->
return @agreeToCLA(req, res) if args[1] is 'agreeToCLA'
return @agreeToEmployerAgreement(req,res) if args[1] is 'agreeToEmployerAgreement'
return @avatar(req, res, args[0]) if args[1] is 'avatar'
return @getNamesByIDs(req, res) if args[1] is 'names'
return @nameToID(req, res, args[0]) if args[1] is 'nameToID'
@ -231,7 +232,31 @@ UserHandler = class UserHandler extends Handler
return @sendDatabaseError(res, err) if err
documents = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
@sendSuccess(res, documents)
agreeToEmployerAgreement: (req, res) ->
userIsAnonymous = req.user?.get('anonymous')
if userIsAnonymous then return errors.unauthorized(res, "You need to be logged in to agree to the employer agreeement.")
profileData = req.body
#TODO: refactor this bit to make it more elegant
if not profileData.id or not profileData.positions or not profileData.emailAddress or not profileData.firstName or not profileData.lastName
return errors.badInput(res, "You need to have a more complete profile to sign up for this service.")
@modelClass.findById(req.user.id).exec (err, user) =>
if user.get('employerAt') or user.get('signedEmployerAgreement') or "employer" in user.get('permissions')
return errors.conflict(res, "You already have signed the agreement!")
#TODO: Search for the current position
employerAt = _.filter(profileData.positions.values,"isCurrent")[0]?.company.name ? "Not available"
signedEmployerAgreement =
linkedinID: profileData.id
date: new Date()
data: profileData
updateObject =
"employerAt": employerAt
"signedEmployerAgreement": signedEmployerAgreement
$push: "permissions":'employer'
User.update {"_id": req.user.id}, updateObject, (err, result) =>
if err? then return errors.serverError(res, "There was an issue updating the user object to reflect employer status: #{err}")
res.send({"message": "The agreement was successful."})
res.end()
getCandidates: (req, res) ->
authorized = req.user.isAdmin() or ('employer' in req.user.get('permissions'))
since = (new Date((new Date()) - 2 * 30.4 * 86400 * 1000)).toISOString()