diff --git a/app/lib/LinkedInHandler.coffee b/app/lib/LinkedInHandler.coffee index 69f9cb473..a19090391 100644 --- a/app/lib/LinkedInHandler.coffee +++ b/app/lib/LinkedInHandler.coffee @@ -22,6 +22,13 @@ module.exports = LinkedInHandler = class LinkedInHandler extends CocoClass .error(cb) .result (profiles) => 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: -> super() diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index ed781891e..5a0209a39 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -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'} 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}, - {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?'} 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!'} diff --git a/app/styles/account/profile.sass b/app/styles/account/profile.sass index d03e7ff92..8774604a9 100644 --- a/app/styles/account/profile.sass +++ b/app/styles/account/profile.sass @@ -38,6 +38,9 @@ i margin-right: 5px + .linked-in-button + cursor: default + .sample-profile position: absolute right: 5px diff --git a/app/templates/account/profile.jade b/app/templates/account/profile.jade index 9577e4b1e..58c88944f 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -18,6 +18,13 @@ block content button.btn#toggle-editing i.icon-cog 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 button.btn.btn-success#toggle-job-profile-active i.icon-eye-open @@ -42,7 +49,7 @@ block content // button.btn // i.icon-user // span(data-i18n="account_settings.sample_profile") See a sample profile - + if profile && allowedToViewJobProfile div(class="job-profile-container" + (editing ? " editable-profile" : "")) .job-profile-row @@ -52,7 +59,7 @@ block content .editable-icon.glyphicon.glyphicon-pencil img.profile-photo(src=user.getPhotoURL(240, true)) .profile-caption= profile.jobTitle || 'Software Developer' - + #links-container.editable-section .editable-display(title="Click to add social and personal links") .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 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 - + if !editing && !myProfile button#contact-candidate.btn.btn-large.btn-inverse.flat-button 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'. button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save - + #short-description-container.editable-section .editable-display(title="Click to write your tagline") .editable-icon.glyphicon.glyphicon-pencil 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 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 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. button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save - + #skills-container.editable-section .editable-display.editable-thinner(title="Click to tag your programming skills") .editable-icon.glyphicon.glyphicon-pencil @@ -215,12 +222,12 @@ block content h3.edit-label Tag your programming skills each skill in ["python", "coffeescript", "node", "ios", "objective-c", "javascript", "app-engine", "mongodb", "web dev", "django", "backbone"] code.edit-example-tag= skill - span + span else each skill in profile.skills code= skill - span - + span + form.editable-form .editable-icon.glyphicon.glyphicon-remove 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. else if modified div.long-description.editable-thinner!= marked(profile.longDescription) - + form.editable-form .editable-icon.glyphicon.glyphicon-remove .form-group @@ -260,7 +267,7 @@ block content img.header-icon(src="/images/pages/account/profile/work.png", alt="") span(data-i18n="account_profile.work_experience") Work Experience | - #{profile.experience} - | + | span(data-i18n=profile.experience == 1 ? "units.year" : "units.years") each job in profile.work if job.role && job.employer @@ -307,7 +314,7 @@ block content .form-group 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) - p.help-block + p.help-block span(data-i18n="account_profile.work_duration_help") When did you hold this gig? | Ex.: "Feb 2013 - present". .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) button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save - + if user.get('jobProfileNotes') || me.isAdmin() div(class="our-notes-section" + (editing ? " deemphasized" : "")) h3.experience-header(data-i18n="account_profile.our_notes") Our Notes @@ -443,23 +450,23 @@ block content else if allowedToViewJobProfile .public-profile-container h2(data-i18n="common.loading") Loading... - + else if user .public-profile-container - h2 - span(data-i18n="account_profile.profile_for_prefix") Profile for + h2 + span(data-i18n="account_profile.profile_for_prefix") Profile for 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)) - + p To see a private user profile, you may need to log in. else .public-profile-container - h2 - span(data-i18n="account_profile.profile_for_prefix") Profile for + h2 + span(data-i18n="account_profile.profile_for_prefix") Profile for 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") diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index ccec22977..cd66cbe17 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -1,6 +1,7 @@ View = require 'views/kinds/RootView' template = require 'templates/account/profile' User = require 'models/User' +{me} = require 'lib/auth' JobProfileContactView = require 'views/modal/job_profile_contact_modal' JobProfileView = require 'views/account/job_profile_view' forms = require 'lib/forms' @@ -8,9 +9,12 @@ forms = require 'lib/forms' module.exports = class ProfileView extends View id: "profile-view" template: template + subscriptions: + 'linkedin-loaded': 'onLinkedInLoaded' events: 'click #toggle-editing': 'toggleEditing' + 'click #importLinkedIn': 'importLinkedIn' 'click #toggle-job-profile-active': 'toggleJobProfileActive' 'click #toggle-job-profile-approved': 'toggleJobProfileApproved' 'click #save-notes-button': 'onJobProfileNotesChanged' @@ -27,6 +31,13 @@ module.exports = class ProfileView extends View constructor: (options, @userID) -> @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 if User.isObjectID @userID @finishInit() @@ -51,9 +62,134 @@ module.exports = class ProfileView extends View else @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: -> context = super() context.userID = @userID + context.linkedInAuthorized = @authorizedWithLinkedIn context.jobProfileSchema = me.schema().properties.jobProfile if @user and not jobProfile = @user.get 'jobProfile' jobProfile = {} @@ -105,6 +241,8 @@ module.exports = class ProfileView extends View toggleEditing: -> @editing = not @editing @render() + _.delay @renderLinkedInButton, 1000 + @saveEdits() toggleJobProfileApproved: -> return unless me.isAdmin()