Merge branch 'feature/linkedin-profile-import'

Conflicts:
	app/templates/account/profile.jade
	app/views/account/profile_view.coffee
This commit is contained in:
Michael Schmatz 2014-06-08 18:37:33 -07:00
commit 693da57f13
5 changed files with 179 additions and 24 deletions

View file

@ -22,6 +22,13 @@ module.exports = LinkedInHandler = class LinkedInHandler extends CocoClass
.error(cb) .error(cb)
.result (profiles) => .result (profiles) =>
cb null, profiles.values[0] cb null, profiles.values[0]
getProfileData: (cb) =>
IN.API.Profile("me")
.fields(["formatted-name","educations","skills","headline","summary","positions","public-profile-url"])
.error(cb)
.result (profiles) =>
cb null, profiles.values[0]
destroy: -> destroy: ->
super() super()

View file

@ -78,7 +78,7 @@ UserSchema = c.object {},
city: c.shortString {title: 'City', description: 'City you want to work in (or live in now), like "San Francisco" or "Lubbock, TX".', default: 'Defaultsville, CA', format: 'city'} city: c.shortString {title: 'City', description: 'City you want to work in (or live in now), like "San Francisco" or "Lubbock, TX".', default: 'Defaultsville, CA', format: 'city'}
country: c.shortString {title: 'Country', description: 'Country you want to work in (or live in now), like "USA" or "France".', default: 'USA', format: 'country'} country: c.shortString {title: 'Country', description: 'Country you want to work in (or live in now), like "USA" or "France".', default: 'USA', format: 'country'}
skills: c.array {title: 'Skills', description: 'Tag relevant developer skills in order of proficiency.', default: ['javascript'], minItems: 1, maxItems: 30, uniqueItems: true}, skills: c.array {title: 'Skills', description: 'Tag relevant developer skills in order of proficiency.', default: ['javascript'], minItems: 1, maxItems: 30, uniqueItems: true},
{type: 'string', minLength: 1, maxLength: 20, description: 'Ex.: "objective-c", "mongodb", "rails", "android", "javascript"', format: 'skill'} {type: 'string', minLength: 1, maxLength: 50, description: 'Ex.: "objective-c", "mongodb", "rails", "android", "javascript"', format: 'skill'}
experience: {type: 'integer', title: 'Years of Experience', minimum: 0, description: 'How many years of professional experience (getting paid) developing software do you have?'} experience: {type: 'integer', title: 'Years of Experience', minimum: 0, description: 'How many years of professional experience (getting paid) developing software do you have?'}
shortDescription: {type: 'string', maxLength: 140, title: 'Short Description', description: 'Who are you, and what are you looking for? 140 characters max.', default: 'Programmer seeking to build great software.'} shortDescription: {type: 'string', maxLength: 140, title: 'Short Description', description: 'Who are you, and what are you looking for? 140 characters max.', default: 'Programmer seeking to build great software.'}
longDescription: {type: 'string', maxLength: 600, title: 'Description', description: 'Describe yourself to potential employers. Keep it short and to the point. We recommend outlining the position that would most interest you. Tasteful markdown okay; 600 characters max.', format: 'markdown', default: '* I write great code.\n* You need great code?\n* Great!'} longDescription: {type: 'string', maxLength: 600, title: 'Description', description: 'Describe yourself to potential employers. Keep it short and to the point. We recommend outlining the position that would most interest you. Tasteful markdown okay; 600 characters max.', format: 'markdown', default: '* I write great code.\n* You need great code?\n* Great!'}

View file

@ -38,6 +38,9 @@
i i
margin-right: 5px margin-right: 5px
.linked-in-button
cursor: default
.sample-profile .sample-profile
position: absolute position: absolute
right: 5px right: 5px

View file

@ -18,6 +18,13 @@ block content
button.btn#toggle-editing button.btn#toggle-editing
i.icon-cog i.icon-cog
span(data-i18n="account_profile.edit_profile") Edit Profile span(data-i18n="account_profile.edit_profile") Edit Profile
if linkedInAuthorized && editing
button.btn.btn-success#importLinkedIn
i.icon-arrow-down
span Import LinkedIn
else if editing
button.btn.linked-in-button
script(type="in/Login" id="linkedInAuthButton" data-onAuth="contractCallback")
if profile && profile.active if profile && profile.active
button.btn.btn-success#toggle-job-profile-active button.btn.btn-success#toggle-job-profile-active
i.icon-eye-open i.icon-eye-open
@ -42,7 +49,7 @@ block content
// button.btn // button.btn
// i.icon-user // i.icon-user
// span(data-i18n="account_settings.sample_profile") See a sample profile // span(data-i18n="account_settings.sample_profile") See a sample profile
if profile && allowedToViewJobProfile if profile && allowedToViewJobProfile
div(class="job-profile-container" + (editing ? " editable-profile" : "")) div(class="job-profile-container" + (editing ? " editable-profile" : ""))
.job-profile-row .job-profile-row
@ -52,7 +59,7 @@ block content
.editable-icon.glyphicon.glyphicon-pencil .editable-icon.glyphicon.glyphicon-pencil
img.profile-photo(src=user.getPhotoURL(240, true)) img.profile-photo(src=user.getPhotoURL(240, true))
.profile-caption= profile.jobTitle || 'Software Developer' .profile-caption= profile.jobTitle || 'Software Developer'
#links-container.editable-section #links-container.editable-section
.editable-display(title="Click to add social and personal links") .editable-display(title="Click to add social and personal links")
.editable-icon.glyphicon.glyphicon-pencil .editable-icon.glyphicon.glyphicon-pencil
@ -162,7 +169,7 @@ block content
option(value='Internship', selected=profile.lookingFor == "Internship", data-i18n="account_profile.basics_looking_for_internship") Internship option(value='Internship', selected=profile.lookingFor == "Internship", data-i18n="account_profile.basics_looking_for_internship") Internship
p.help-block(data-i18n="account_profile.basics_looking_for_help") What kind of developer position do you want? p.help-block(data-i18n="account_profile.basics_looking_for_help") What kind of developer position do you want?
button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save
if !editing && !myProfile if !editing && !myProfile
button#contact-candidate.btn.btn-large.btn-inverse.flat-button button#contact-candidate.btn.btn-large.btn-inverse.flat-button
span(data-i18n="account_profile.contact") Contact span(data-i18n="account_profile.contact") Contact
@ -188,14 +195,14 @@ block content
p.help-block(data-i18n="account_profile.name_help") Name you want employers to see, like 'Nick Winter'. p.help-block(data-i18n="account_profile.name_help") Name you want employers to see, like 'Nick Winter'.
button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save
#short-description-container.editable-section #short-description-container.editable-section
.editable-display(title="Click to write your tagline") .editable-display(title="Click to write your tagline")
.editable-icon.glyphicon.glyphicon-pencil .editable-icon.glyphicon.glyphicon-pencil
if editing && (!profile.shortDescription || profile.shortDescription == jobProfileSchema.properties.shortDescription.default) if editing && (!profile.shortDescription || profile.shortDescription == jobProfileSchema.properties.shortDescription.default)
h3.edit-label(data-i18n="account_profile.short_description_header") Write a short description of yourself h3.edit-label(data-i18n="account_profile.short_description_header") Write a short description of yourself
p.edit-example-text(data-i18n="account_profile.short_description_blurb") Add a tagline to help an employer quickly learn more about you. p.edit-example-text(data-i18n="account_profile.short_description_blurb") Add a tagline to help an employer quickly learn more about you.
else if profile.shortDescription else if profile.shortDescription
p.editable-thinner= profile.shortDescription p.editable-thinner= profile.shortDescription
@ -207,7 +214,7 @@ block content
p.help-block(data-i18n="account_profile.short_description_help") Who are you, and what are you looking for? 140 characters max. p.help-block(data-i18n="account_profile.short_description_help") Who are you, and what are you looking for? 140 characters max.
button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save
#skills-container.editable-section #skills-container.editable-section
.editable-display.editable-thinner(title="Click to tag your programming skills") .editable-display.editable-thinner(title="Click to tag your programming skills")
.editable-icon.glyphicon.glyphicon-pencil .editable-icon.glyphicon.glyphicon-pencil
@ -215,12 +222,12 @@ block content
h3.edit-label Tag your programming skills h3.edit-label Tag your programming skills
each skill in ["python", "coffeescript", "node", "ios", "objective-c", "javascript", "app-engine", "mongodb", "web dev", "django", "backbone"] each skill in ["python", "coffeescript", "node", "ios", "objective-c", "javascript", "app-engine", "mongodb", "web dev", "django", "backbone"]
code.edit-example-tag= skill code.edit-example-tag= skill
span span
else else
each skill in profile.skills each skill in profile.skills
code= skill code= skill
span span
form.editable-form form.editable-form
.editable-icon.glyphicon.glyphicon-remove .editable-icon.glyphicon.glyphicon-remove
h3(data-i18n="account_profile.skills_header") Skills h3(data-i18n="account_profile.skills_header") Skills
@ -243,7 +250,7 @@ block content
p.edit-example-text(data-i18n="account_profile.long_description_blurb") Tell employers how awesome you are and what role you want. p.edit-example-text(data-i18n="account_profile.long_description_blurb") Tell employers how awesome you are and what role you want.
else if modified else if modified
div.long-description.editable-thinner!= marked(profile.longDescription) div.long-description.editable-thinner!= marked(profile.longDescription)
form.editable-form form.editable-form
.editable-icon.glyphicon.glyphicon-remove .editable-icon.glyphicon.glyphicon-remove
.form-group .form-group
@ -260,7 +267,7 @@ block content
img.header-icon(src="/images/pages/account/profile/work.png", alt="") img.header-icon(src="/images/pages/account/profile/work.png", alt="")
span(data-i18n="account_profile.work_experience") Work Experience span(data-i18n="account_profile.work_experience") Work Experience
| - #{profile.experience} | - #{profile.experience}
| |
span(data-i18n=profile.experience == 1 ? "units.year" : "units.years") span(data-i18n=profile.experience == 1 ? "units.year" : "units.years")
each job in profile.work each job in profile.work
if job.role && job.employer if job.role && job.employer
@ -307,7 +314,7 @@ block content
.form-group .form-group
label.control-label(data-i18n="account_profile.work_duration") Duration label.control-label(data-i18n="account_profile.work_duration") Duration
input.form-control(type='text', maxlength='100', name="root[work][#{index}][duration]", value=job.duration) input.form-control(type='text', maxlength='100', name="root[work][#{index}][duration]", value=job.duration)
p.help-block p.help-block
span(data-i18n="account_profile.work_duration_help") When did you hold this gig? span(data-i18n="account_profile.work_duration_help") When did you hold this gig?
| Ex.: "Feb 2013 - present". | Ex.: "Feb 2013 - present".
.form-group .form-group
@ -370,7 +377,7 @@ block content
p.help-block(data-i18n="account_profile.education_description_help") Highlight anything about this educational experience. (140 chars; optional) p.help-block(data-i18n="account_profile.education_description_help") Highlight anything about this educational experience. (140 chars; optional)
button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save
if user.get('jobProfileNotes') || me.isAdmin() if user.get('jobProfileNotes') || me.isAdmin()
div(class="our-notes-section" + (editing ? " deemphasized" : "")) div(class="our-notes-section" + (editing ? " deemphasized" : ""))
h3.experience-header(data-i18n="account_profile.our_notes") Our Notes h3.experience-header(data-i18n="account_profile.our_notes") Our Notes
@ -443,23 +450,23 @@ block content
else if allowedToViewJobProfile else if allowedToViewJobProfile
.public-profile-container .public-profile-container
h2(data-i18n="common.loading") Loading... h2(data-i18n="common.loading") Loading...
else if user else if user
.public-profile-container .public-profile-container
h2 h2
span(data-i18n="account_profile.profile_for_prefix") Profile for span(data-i18n="account_profile.profile_for_prefix") Profile for
span= user.get('name') || "Anonymous Wizard" span= user.get('name') || "Anonymous Wizard"
span(data-i18n="account_profile.profile_for_suffix") span(data-i18n="account_profile.profile_for_suffix")
img.profile-photo(src=user.getPhotoURL(256)) img.profile-photo(src=user.getPhotoURL(256))
p To see a private user profile, you may need to log in. p To see a private user profile, you may need to log in.
else else
.public-profile-container .public-profile-container
h2 h2
span(data-i18n="account_profile.profile_for_prefix") Profile for span(data-i18n="account_profile.profile_for_prefix") Profile for
span= userID span= userID
span(data-i18n="account_profile.profile_for_suffix") span(data-i18n="account_profile.profile_for_suffix")
| |
span(data-i18n="loading_error.not_found") span(data-i18n="loading_error.not_found")

View file

@ -1,6 +1,7 @@
View = require 'views/kinds/RootView' View = require 'views/kinds/RootView'
template = require 'templates/account/profile' template = require 'templates/account/profile'
User = require 'models/User' User = require 'models/User'
{me} = require 'lib/auth'
JobProfileContactView = require 'views/modal/job_profile_contact_modal' JobProfileContactView = require 'views/modal/job_profile_contact_modal'
JobProfileView = require 'views/account/job_profile_view' JobProfileView = require 'views/account/job_profile_view'
forms = require 'lib/forms' forms = require 'lib/forms'
@ -8,9 +9,12 @@ forms = require 'lib/forms'
module.exports = class ProfileView extends View module.exports = class ProfileView extends View
id: "profile-view" id: "profile-view"
template: template template: template
subscriptions:
'linkedin-loaded': 'onLinkedInLoaded'
events: events:
'click #toggle-editing': 'toggleEditing' 'click #toggle-editing': 'toggleEditing'
'click #importLinkedIn': 'importLinkedIn'
'click #toggle-job-profile-active': 'toggleJobProfileActive' 'click #toggle-job-profile-active': 'toggleJobProfileActive'
'click #toggle-job-profile-approved': 'toggleJobProfileApproved' 'click #toggle-job-profile-approved': 'toggleJobProfileApproved'
'click #save-notes-button': 'onJobProfileNotesChanged' 'click #save-notes-button': 'onJobProfileNotesChanged'
@ -27,6 +31,13 @@ module.exports = class ProfileView extends View
constructor: (options, @userID) -> constructor: (options, @userID) ->
@userID ?= me.id @userID ?= me.id
@onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000
@authorizedWithLinkedIn = IN?.User?.isAuthorized()
@linkedInLoaded = Boolean(IN.parse)
@waitingForLinkedIn = false
window.contractCallback = =>
@authorizedWithLinkedIn = IN?.User?.isAuthorized()
@render()
super options super options
if User.isObjectID @userID if User.isObjectID @userID
@finishInit() @finishInit()
@ -51,9 +62,134 @@ module.exports = class ProfileView extends View
else else
@user = User.getByID(@userID) @user = User.getByID(@userID)
onLinkedInLoaded: =>
@linkedinLoaded = true
if @waitingForLinkedIn
@renderLinkedInButton()
renderLinkedInButton: =>
IN?.parse()
afterInsert: ->
super()
linkedInButtonParentElement = document.getElementById("linkedInAuthButton")
if linkedInButtonParentElement
if @linkedinLoaded
@renderLinkedInButton()
else
@waitingForLinkedIn = true
importLinkedIn: =>
overwriteConfirm = confirm("Importing LinkedIn data will overwrite your current work experience, skills, name, descriptions, and education. Continue?")
unless overwriteConfirm then return
application.linkedinHandler.getProfileData (err, profileData) =>
console.log profileData
@processLinkedInProfileData profileData
jobProfileSchema: -> @user.schema().properties.jobProfile.properties
processLinkedInProfileData: (p) ->
#handle formatted-name
currentJobProfile = @user.get('jobProfile')
oldJobProfile = _.cloneDeep(currentJobProfile)
jobProfileSchema = @user.schema().properties.jobProfile.properties
if p["formattedName"]? and p["formattedName"] isnt "private"
nameMaxLength = jobProfileSchema.name.maxLength
currentJobProfile.name = p["formattedName"].slice(0,nameMaxLength)
if p["skills"]?["values"].length
skillNames = []
skillMaxLength = jobProfileSchema.skills.items.maxLength
for skill in p.skills.values
skillNames.push skill.skill.name.slice(0,skillMaxLength)
currentJobProfile.skills = skillNames
if p["headline"]
shortDescriptionMaxLength = jobProfileSchema.shortDescription.maxLength
currentJobProfile.shortDescription = p["headline"].slice(0,shortDescriptionMaxLength)
if p["summary"]
longDescriptionMaxLength = jobProfileSchema.longDescription.maxLength
currentJobProfile.longDescription = p.summary.slice(0,longDescriptionMaxLength)
if p["positions"]?["values"]?.length
newWorks = []
workSchema = jobProfileSchema.work.items.properties
for position in p["positions"]["values"]
workObj = {}
descriptionMaxLength = workSchema.description.maxLength
workObj.description = position.summary?.slice(0,descriptionMaxLength)
workObj.description ?= ""
if position.startDate?.year?
workObj.duration = "#{position.startDate.year} - "
if (not position.endDate?.year) or (position.endDate?.year and position.endDate?.year > (new Date().getFullYear()))
workObj.duration += "present"
else
workObj.duration += position.endDate.year
else
workObj.duration = ""
durationMaxLength = workSchema.duration.maxLength
workObj.duration = workObj.duration.slice(0,durationMaxLength)
employerMaxLength = workSchema.employer.maxLength
workObj.employer = position.company?.name ? ""
workObj.employer = workObj.employer.slice(0,employerMaxLength)
workObj.role = position.title ? ""
roleMaxLength = workSchema.role.maxLength
workObj.role = workObj.role.slice(0,roleMaxLength)
newWorks.push workObj
currentJobProfile.work = newWorks
if p["educations"]?["values"]?.length
newEducation = []
eduSchema = jobProfileSchema.education.items.properties
for education in p["educations"]["values"]
educationObject = {}
educationObject.degree = education.degree ? "Studied"
if education.startDate?.year?
educationObject.duration = "#{education.startDate.year} - "
if (not education.endDate?.year) or (education.endDate?.year and education.endDate?.year > (new Date().getFullYear()))
educationObject.duration += "present"
if educationObject.degree is "Studied"
educationObject.degree = "Studying"
else
educationObject.duration += education.endDate.year
else
educationObject.duration = ""
if education.fieldOfStudy
if educationObject.degree is "Studied" or educationObject.degree is "Studying"
educationObject.degree += " #{education.fieldOfStudy}"
else
educationObject.degree += " in #{education.fieldOfStudy}"
educationObject.degree = educationObject.degree.slice(0,eduSchema.degree.maxLength)
educationObject.duration = educationObject.duration.slice(0,eduSchema.duration.maxLength)
educationObject.school = education.schoolName ? ""
educationObject.school = educationObject.school.slice(0,eduSchema.school.maxLength)
educationObject.description = ""
newEducation.push educationObject
currentJobProfile.education = newEducation
if p["publicProfileUrl"]
#search for linkedin link
links = currentJobProfile.links
alreadyHasLinkedIn = false
for link in links
if link.link.toLowerCase().indexOf("linkedin") > -1
alreadyHasLinkedIn = true
break
unless alreadyHasLinkedIn
newLink =
link: p["publicProfileUrl"]
name: "LinkedIn"
currentJobProfile.links.push newLink
@user.set('jobProfile',currentJobProfile)
validationErrors = @user.validate()
if validationErrors
@user.set('jobProfile',oldJobProfile)
return alert("Please notify team@codecombat.com! There were validation errors from the LinkedIn import: #{JSON.stringify validationErrors}")
else
@render()
getRenderData: -> getRenderData: ->
context = super() context = super()
context.userID = @userID context.userID = @userID
context.linkedInAuthorized = @authorizedWithLinkedIn
context.jobProfileSchema = me.schema().properties.jobProfile context.jobProfileSchema = me.schema().properties.jobProfile
if @user and not jobProfile = @user.get 'jobProfile' if @user and not jobProfile = @user.get 'jobProfile'
jobProfile = {} jobProfile = {}
@ -105,6 +241,8 @@ module.exports = class ProfileView extends View
toggleEditing: -> toggleEditing: ->
@editing = not @editing @editing = not @editing
@render() @render()
_.delay @renderLinkedInButton, 1000
@saveEdits()
toggleJobProfileApproved: -> toggleJobProfileApproved: ->
return unless me.isAdmin() return unless me.isAdmin()