mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-25 00:28:31 -05:00
Merge pull request #918 from codecombat/master
Merge master into production
This commit is contained in:
commit
2435846451
12 changed files with 268 additions and 10 deletions
|
@ -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'
|
||||
|
|
|
@ -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)};
|
||||
|
@ -71,6 +82,7 @@
|
|||
olark.identify('1451-787-10-5544');/*]]>*/</script>
|
||||
<!-- end olark code -->
|
||||
|
||||
|
||||
</head>
|
||||
<body class="nano clearfix">
|
||||
<div id="fb-root"></div>
|
||||
|
|
28
app/lib/LinkedInHandler.coffee
Normal file
28
app/lib/LinkedInHandler.coffee
Normal 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()
|
|
@ -20,6 +20,14 @@ module.exports.createUser = (userObject, failure=backboneFailure, nextURL=null)
|
|||
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',
|
||||
{
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#employers-view
|
||||
#see-candidates
|
||||
cursor: pointer
|
||||
.tablesorter
|
||||
//img
|
||||
// display: none
|
||||
|
|
|
@ -12,8 +12,10 @@ 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.
|
||||
if !isEmployer
|
||||
|
||||
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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
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")
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue