From 4072c1b96960d8cd42249f82c53de073eab8c5cd Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Sat, 7 Jun 2014 11:45:49 -0700 Subject: [PATCH 1/5] Basic functionality done, needs validation error handling As well as formatting --- app/lib/LinkedInHandler.coffee | 7 ++ app/schemas/models/user.coffee | 2 +- app/templates/account/profile.jade | 5 ++ app/views/account/profile_view.coffee | 95 ++++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/app/lib/LinkedInHandler.coffee b/app/lib/LinkedInHandler.coffee index 69f9cb473..db34fe4f0 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"]) + .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/templates/account/profile.jade b/app/templates/account/profile.jade index b0f6d84a1..fe31e5a87 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -3,6 +3,11 @@ extends /templates/base block content if allowedToEditJobProfile .profile-control-bar + if linkedInAuthorized + button.btn#importLinkedIn + span Import LinkedIn + else + script(type="in/Login" id="linkedInAuthButton" data-onAuth="contractCallback") if editing .progress.profile-completion-progress .progress-bar.progress-bar-success(style="width: #{100 * progress}%") diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index 3ec1c9384..be0de3b5c 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' @@ -28,6 +32,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 @uploadFilePath = "db/user/#{@userID}" @highlightedContainers = [] @@ -41,8 +52,90 @@ 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: => + application.linkedinHandler.getProfileData (err, profileData) => + console.log profileData + @processLinkedInProfileData profileData, -> + console.log "DONE" + + processLinkedInProfileData: (p, cb) -> + #handle formatted-name + currentJobProfile = me.get('jobProfile') + if p["formattedName"]? and p["formattedName"] isnt "private" + currentJobProfile.name = p["formattedName"] + if p["skills"]?["values"].length + skillNames = [] + for skill in p.skills.values + skillNames.push skill.skill.name + + console.log "Skills: #{skillNames}" + currentJobProfile.skills = skillNames + if p["headline"] + console.log "jobProfile.shortDescription: #{p["headline"]}" + currentJobProfile.shortDescription = p["headline"] + if p["summary"] + console.log "jobProfile.longDescription: #{p.summary}" + currentJobProfile.longDescription = p.summary + if p["positions"]?["values"]?.length + newWorks = [] + for position in p["positions"]["values"] + workObj = {} + workObj.description = position.summary?.slice(0,139) + 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 = "" + workObj.employer = position.company?.name ? "" + workObj.role = position.title ? "" + newWorks.push workObj + currentJobProfile.work = newWorks + + + if p["educations"]?["values"]?.length + newEducation = [] + 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 + + educationObject.school = education.schoolName ? "" + educationObject.description = "" + console.log "Educated at:#{education.schoolName}" + newEducation.push educationObject + currentJobProfile.education = newEducation + me.set('jobProfile',currentJobProfile) + @render() + getRenderData: -> context = super() + context.linkedInAuthorized = @authorizedWithLinkedIn context.jobProfileSchema = me.schema().properties.jobProfile unless jobProfile = @user.get 'jobProfile' jobProfile = {} From 7ec3910e36ab3544d3e19ab688848cfb36dfa291 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Sun, 8 Jun 2014 17:38:02 -0700 Subject: [PATCH 2/5] Make LinkedIn import more robust --- app/templates/account/profile.jade | 12 +++--- app/views/account/profile_view.coffee | 60 +++++++++++++++++++-------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/app/templates/account/profile.jade b/app/templates/account/profile.jade index fe31e5a87..9a8e21c45 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -3,11 +3,7 @@ extends /templates/base block content if allowedToEditJobProfile .profile-control-bar - if linkedInAuthorized - button.btn#importLinkedIn - span Import LinkedIn - else - script(type="in/Login" id="linkedInAuthButton" data-onAuth="contractCallback") + if editing .progress.profile-completion-progress .progress-bar.progress-bar-success(style="width: #{100 * progress}%") @@ -23,6 +19,12 @@ block content button.btn#toggle-editing i.icon-cog span(data-i18n="account_profile.edit_profile") Edit Profile + if linkedInAuthorized + button.btn.btn-success#importLinkedIn + i.icon-arrow-down + span Import LinkedIn + else + script(type="in/Login" id="linkedInAuthButton" data-onAuth="contractCallback") if profile.active button.btn.btn-success#toggle-job-profile-active i.icon-eye-open diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index be0de3b5c..ba4808781 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -69,35 +69,43 @@ module.exports = class ProfileView extends View 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, -> - console.log "DONE" - + jobProfileSchema: -> @user.schema().properties.jobProfile.properties + processLinkedInProfileData: (p, cb) -> #handle formatted-name - currentJobProfile = me.get('jobProfile') + currentJobProfile = @user.get('jobProfile') + jobProfileSchema = @user.schema().properties.jobProfile.properties + if p["formattedName"]? and p["formattedName"] isnt "private" - currentJobProfile.name = p["formattedName"] + 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 - - console.log "Skills: #{skillNames}" + skillNames.push skill.skill.name.slice(0,skillMaxLength) currentJobProfile.skills = skillNames if p["headline"] - console.log "jobProfile.shortDescription: #{p["headline"]}" - currentJobProfile.shortDescription = p["headline"] + shortDescriptionMaxLength = jobProfileSchema.shortDescription.maxLength + currentJobProfile.shortDescription = p["headline"].slice(0,shortDescriptionMaxLength) if p["summary"] - console.log "jobProfile.longDescription: #{p.summary}" - currentJobProfile.longDescription = 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 = {} - workObj.description = position.summary?.slice(0,139) - if position.startDate?.year + 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" @@ -105,18 +113,26 @@ module.exports = class ProfileView extends View 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.degree = educationObject.degree.slice(0,eduSchema.degree.maxLength) + 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" @@ -124,14 +140,21 @@ module.exports = class ProfileView extends View educationObject.degree = "Studying" else educationObject.duration += education.endDate.year - + else + educationObject.duration = "" + educationObject.duration = educationObject.duration.slice(0,eduSchema.duration.maxLength) educationObject.school = education.schoolName ? "" + educationObject.school = educationObject.school.slice(0,eduSchema.school.maxLength) educationObject.description = "" - console.log "Educated at:#{education.schoolName}" newEducation.push educationObject currentJobProfile.education = newEducation - me.set('jobProfile',currentJobProfile) - @render() + validationErrors = @user.validate() + if validationErrors + return alert("Please notify team@codecombat.com! There were validation errors from the import: #{JSON.stringify validationErrors}") + else + @user.set('jobProfile',currentJobProfile) + @user.save() + @render() getRenderData: -> context = super() @@ -186,6 +209,7 @@ module.exports = class ProfileView extends View toggleEditing: -> @editing = not @editing @render() + IN.parse() toggleJobProfileApproved: -> return unless me.isAdmin() From bb5d23e6bae0c6b0d2447dfa2e2a480123a036f7 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Sun, 8 Jun 2014 18:51:45 -0600 Subject: [PATCH 3/5] Wrapped LinkedIn button in a .btn. --- app/styles/account/profile.sass | 3 +++ app/templates/account/profile.jade | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) 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 9a8e21c45..c31ea42c2 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -24,7 +24,8 @@ block content i.icon-arrow-down span Import LinkedIn else - script(type="in/Login" id="linkedInAuthButton" data-onAuth="contractCallback") + button.btn.linked-in-button + script(type="in/Login" id="linkedInAuthButton" data-onAuth="contractCallback") if profile.active button.btn.btn-success#toggle-job-profile-active i.icon-eye-open From f29a41f108f8155631ea22b5aa6b18df1b7a1a18 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Sun, 8 Jun 2014 18:15:16 -0700 Subject: [PATCH 4/5] Add linkedIn link, field of study support to LinkedIn import --- app/lib/LinkedInHandler.coffee | 2 +- app/templates/account/profile.jade | 2 +- app/views/account/profile_view.coffee | 28 +++++++++++++++++++++++---- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/lib/LinkedInHandler.coffee b/app/lib/LinkedInHandler.coffee index db34fe4f0..a19090391 100644 --- a/app/lib/LinkedInHandler.coffee +++ b/app/lib/LinkedInHandler.coffee @@ -25,7 +25,7 @@ module.exports = LinkedInHandler = class LinkedInHandler extends CocoClass getProfileData: (cb) => IN.API.Profile("me") - .fields(["formatted-name","educations","skills","headline","summary","positions"]) + .fields(["formatted-name","educations","skills","headline","summary","positions","public-profile-url"]) .error(cb) .result (profiles) => cb null, profiles.values[0] diff --git a/app/templates/account/profile.jade b/app/templates/account/profile.jade index c31ea42c2..8f3813343 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -19,7 +19,7 @@ block content button.btn#toggle-editing i.icon-cog span(data-i18n="account_profile.edit_profile") Edit Profile - if linkedInAuthorized + if linkedInAuthorized && editing button.btn.btn-success#importLinkedIn i.icon-arrow-down span Import LinkedIn diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index ba4808781..2e797328f 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -79,6 +79,7 @@ module.exports = class ProfileView extends View processLinkedInProfileData: (p, cb) -> #handle formatted-name currentJobProfile = @user.get('jobProfile') + oldJobProfile = _.cloneDeep(currentJobProfile) jobProfileSchema = @user.schema().properties.jobProfile.properties if p["formattedName"]? and p["formattedName"] isnt "private" @@ -131,7 +132,7 @@ module.exports = class ProfileView extends View for education in p["educations"]["values"] educationObject = {} educationObject.degree = education.degree ? "Studied" - educationObject.degree = educationObject.degree.slice(0,eduSchema.degree.maxLength) + 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())) @@ -142,18 +143,37 @@ module.exports = class ProfileView extends View 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 - return alert("Please notify team@codecombat.com! There were validation errors from the import: #{JSON.stringify validationErrors}") + @user.set('jobProfile',oldJobProfile) + return alert("Please notify team@codecombat.com! There were validation errors from the LinkedIn import: #{JSON.stringify validationErrors}") else - @user.set('jobProfile',currentJobProfile) - @user.save() @render() getRenderData: -> From ad80fcf229f19cc2f5c45b03e343a14409ee9603 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Sun, 8 Jun 2014 18:21:56 -0700 Subject: [PATCH 5/5] Removed linkedIn callback, disabled autosave --- app/views/account/profile_view.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index 2e797328f..7d2f7c7d5 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -73,10 +73,10 @@ module.exports = class ProfileView extends View unless overwriteConfirm then return application.linkedinHandler.getProfileData (err, profileData) => console.log profileData - @processLinkedInProfileData profileData, -> + @processLinkedInProfileData profileData jobProfileSchema: -> @user.schema().properties.jobProfile.properties - processLinkedInProfileData: (p, cb) -> + processLinkedInProfileData: (p) -> #handle formatted-name currentJobProfile = @user.get('jobProfile') oldJobProfile = _.cloneDeep(currentJobProfile) @@ -230,6 +230,7 @@ module.exports = class ProfileView extends View @editing = not @editing @render() IN.parse() + @saveEdits() toggleJobProfileApproved: -> return unless me.isAdmin()