linkedin2cv 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.buildpacks +1 -1
- data/.gitignore +3 -2
- data/Procfile +1 -3
- data/README.md +2 -2
- data/app/routes/api.rb +27 -3
- data/app/routes/web.rb +6 -5
- data/app/views/home.erb +2 -2
- data/app/views/index.haml +1 -3
- data/bin/linkedin2cv +9 -0
- data/lib/linkedin2cv/converter.rb +25 -21
- data/lib/linkedin2cv/renderer/latex_renderer.rb +41 -33
- data/lib/linkedin2cv/version.rb +1 -1
- data/linkedin2cv.gemspec +1 -1
- data/public/app/index.html +3 -3
- data/public/app/scripts/app.js +2 -2
- data/public/app/scripts/controllers/main.js +1 -1
- data/public/app/scripts/services/linkedin2cv.js +2 -2
- data/public/app/styles/main.scss +1 -1
- data/public/app/views/main.html +1 -1
- data/public/bower.json +1 -1
- data/public/package.json +1 -1
- data/public/test/spec/controllers/main.js +1 -1
- data/public/test/spec/services/happyapi.js +6 -6
- data/public/test/spec/services/happyservice.js +1 -1
- data/spec/converter_spec.rb +0 -32
- data/spec/latex_renderer_spec.rb +32 -0
- data/spec/mocks/config.yml +5 -1
- data/spec/mocks/matt.latex +1424 -0
- data/spec/mocks/profile.json +1 -1
- data/templates/cv.erb +15 -4
- data/texlive.packages +17 -0
- metadata +10 -6
- data/app/views/hello.erb +0 -36
- data/test.rb +0 -109
- data/test.sh +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c48a59e31c94af34237c92f71a6872892289589c
|
4
|
+
data.tar.gz: 1dccf11adb9d8481312779c47dd0ecba6a919d46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a286e40bdea56ddde934ead557fc7eff9e36ec3d6986a7642edb9a1e7d4a6a8408246a0388a731977ddf8d4c778171078567b3b528b0915b7e6edc5c33b0a6f1
|
7
|
+
data.tar.gz: 335e4ba1a22918848c0327021cb39babe78b6819c9d796243884b5cba8420207ecfbbfc67e070098994cf64bd193ab1c8b516300cb2cdf93f180fd6ded3c6623
|
data/.buildpacks
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
https://github.com/heroku/heroku-buildpack-ruby.git
|
2
|
-
https://github.com/mefellows/heroku-buildpack-
|
2
|
+
https://github.com/mefellows/heroku-buildpack-tex.git
|
data/.gitignore
CHANGED
data/Procfile
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
LinkedIn 2 CV
|
2
2
|
==============
|
3
3
|
|
4
|
-
Turn your LinkedIn Profile into a professional resume in many formats (PDF / HTML5 /
|
4
|
+
Turn your LinkedIn Profile into a professional resume in many formats (PDF / HTML5 / LaTeX / Asciidoc)
|
5
5
|
|
6
6
|
## Motivations
|
7
7
|
|
@@ -11,7 +11,7 @@ Oh, and don't bother using the LinkedIn 'Export to PDF' feature - it sucks.
|
|
11
11
|
|
12
12
|
## Getting Started
|
13
13
|
|
14
|
-
First, install [MacTex](http://www.tug.org/mactex/) so that you can produce LaTeX documents. Then, simply run the converter:
|
14
|
+
First, install a Tex distribution ([MacTex](http://www.tug.org/mactex/) or [TexLive](http://www.tug.org/texlive/)) so that you can produce LaTeX documents. Then, simply run the converter:
|
15
15
|
|
16
16
|
gem install linkedin2resume
|
17
17
|
linkedin2cv convert --format latex --options config.yml matt.fellows
|
data/app/routes/api.rb
CHANGED
@@ -6,6 +6,7 @@ require 'sinatra/param'
|
|
6
6
|
require "sinatra/json"
|
7
7
|
require "json"
|
8
8
|
require "sinatra-websocket"
|
9
|
+
require "linkedin2cv/converter"
|
9
10
|
|
10
11
|
#
|
11
12
|
# Public: API for the Application
|
@@ -14,8 +15,16 @@ module Linkedin2CV
|
|
14
15
|
module Routes
|
15
16
|
class API < Sinatra::Application
|
16
17
|
helpers Sinatra::Param
|
18
|
+
use Rack::Logger
|
17
19
|
|
18
20
|
configure do
|
21
|
+
set :views, 'app/views'
|
22
|
+
set :public_folder, 'public/dist'
|
23
|
+
set :api, ENV['LINKEDIN_API_KEY']
|
24
|
+
set :secret, ENV["LINKEDIN_API_SECRET"]
|
25
|
+
set :logging, true
|
26
|
+
set :server, 'thin'
|
27
|
+
set :port, 5000
|
19
28
|
set :json_encoder, :to_json
|
20
29
|
set :sockets, []
|
21
30
|
end
|
@@ -36,8 +45,23 @@ module Linkedin2CV
|
|
36
45
|
# Public: Main API entry point to run the Linkedin2CV service
|
37
46
|
#
|
38
47
|
#
|
39
|
-
get '/api/
|
48
|
+
get '/api/cv/:name' do
|
40
49
|
|
50
|
+
param :format, String, default: 'latex'
|
51
|
+
param :name, String, required: true
|
52
|
+
param :access_token, String, required: true
|
53
|
+
|
54
|
+
logger.debug("Request received to create a CV. Format: #{params[:format]}, Name: #{params[:name]}, Token: #{params[:access_token]}")
|
55
|
+
|
56
|
+
options = {}
|
57
|
+
options['output_file'] = "/tmp/#{params[:name]}"
|
58
|
+
options['format'] = params[:format]
|
59
|
+
|
60
|
+
converter = Linkedin2CV::Converter.new(params[:access_token])
|
61
|
+
converter.create_resume(options)
|
62
|
+
|
63
|
+
content_type "application/pdf"
|
64
|
+
File.read("#{options['output_file']}.pdf")
|
41
65
|
end
|
42
66
|
|
43
67
|
|
@@ -90,9 +114,9 @@ module Linkedin2CV
|
|
90
114
|
# Override this for custom behaviour.
|
91
115
|
#
|
92
116
|
def checkPong(msg)
|
93
|
-
|
117
|
+
logger.debug("Checking ping pong for a ping: " + msg['message'])
|
94
118
|
if (msg['message'] == 'ping')
|
95
|
-
|
119
|
+
logger.debug("Ping hit!")
|
96
120
|
response_obj = {'message' => 'pong'}
|
97
121
|
end
|
98
122
|
end
|
data/app/routes/web.rb
CHANGED
@@ -99,12 +99,13 @@ module Linkedin2CV
|
|
99
99
|
token = client.request_access_token(code, redirect_uri: "http://#{request.host}:#{request.port}/auth/callback")
|
100
100
|
session[:atoken] = token.token
|
101
101
|
|
102
|
-
#
|
103
|
-
file = File.new('.token', 'w')
|
104
|
-
file.write(token.token)
|
105
|
-
file.close
|
106
|
-
|
102
|
+
# CLI Only behaviour
|
107
103
|
if !ENV['CLI_ONLY'].nil?
|
104
|
+
# Store in env for command line!
|
105
|
+
file = File.new('.token', 'w')
|
106
|
+
file.write(token.token)
|
107
|
+
file.close
|
108
|
+
|
108
109
|
logger.info "Got access token, shutting down!: #{session[:atoken]}"
|
109
110
|
Thread.current.thread_variable_set('access_token', token.token)
|
110
111
|
Thread.kill(Thread.current)
|
data/app/views/home.erb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
<!doctype html><!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--><!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]--><!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]--><!--[if gt IE 8]><!--><html class="no-js"><!--<![endif]--><head><meta charset="utf-8"><title>Linkedin2CV Generator | Generate a Linkedin2CV for your website</title><meta name="description" content=""><meta name="viewport" content="width=device-width"><!-- Place favicon.ico and apple-touch-icon.png in the root directory --><link rel="stylesheet" href="styles/vendor.css"><link rel="stylesheet" href="styles/f24dfdad.main.css"><body ng-app="
|
2
|
-
<p class="browselinkedin2cv
|
1
|
+
<!doctype html><!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--><!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]--><!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]--><!--[if gt IE 8]><!--><html class="no-js"><!--<![endif]--><head><meta charset="utf-8"><title>Linkedin2CV Generator | Generate a Linkedin2CV for your website</title><meta name="description" content=""><meta name="viewport" content="width=device-width"><!-- Place favicon.ico and apple-touch-icon.png in the root directory --><link rel="stylesheet" href="styles/vendor.css"><link rel="stylesheet" href="styles/f24dfdad.main.css"><body ng-app="linkedin2cvGeneratorApp"><!--[if lt IE 7]>
|
2
|
+
<p class="browselinkedin2cv">You are using an <strong>outdated</strong> browser. Please <a href="http://browselinkedin2cv.com/">upgrade your browser</a> to improve your experience.</p>
|
3
3
|
<![endif]--><!-- Add your site or application content here --><div class="container" ng-view=""></div><!-- Google Analytics: change UA-XXXXX-X to be your site's ID --><script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
4
4
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
5
5
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
data/app/views/index.haml
CHANGED
@@ -2,10 +2,8 @@
|
|
2
2
|
%a{:href => "/auth/logout"} Logout
|
3
3
|
%p= profile.headline
|
4
4
|
%br
|
5
|
+
%a{:href => "/api/cv/#{session[:atoken]}?access_token=#{session[:atoken]}"} Download CV
|
5
6
|
%div= "Your token is #{session[:atoken]}"
|
6
7
|
|
7
8
|
-else
|
8
9
|
%a{:href => "/auth"} Login using LinkedIn
|
9
|
-
|
10
|
-
-if session[:authorize_url]
|
11
|
-
%p="yo, auth url #{session[:authorize_url]}"
|
data/bin/linkedin2cv
ADDED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'linkedin-oauth2'
|
2
|
+
require 'linkedin2cv/logging'
|
2
3
|
require 'asciidoctor'
|
3
4
|
require 'launchy'
|
4
5
|
require 'tempfile'
|
@@ -13,17 +14,20 @@ module Linkedin2CV
|
|
13
14
|
class Converter
|
14
15
|
include Logging
|
15
16
|
|
16
|
-
TOKEN_WAIT_TIME = 15
|
17
17
|
API_KEY = ENV['LINKEDIN_API_KEY']
|
18
18
|
API_SECRET = ENV["LINKEDIN_API_SECRET"]
|
19
19
|
|
20
|
-
def initialize
|
20
|
+
def initialize(token = nil)
|
21
21
|
@display_fields = ["company", "publication", "patent", "language", "skills", "certification", "education", "course", "volunteer", "recommendations"]
|
22
22
|
@profile_fields = ["projects:(start-date,end-date,description,id,name,url),main-address,phone-numbers,email-address,first-name,last-name,maiden-name,formatted-name,phonetic-first-name,phonetic-last-name,formatted-phonetic-name,headline,location:(name,country:(code)),industry,current-status,current-share,num-connections,num-connections-capped,summary,specialties,positions,picture-url,api-standard-profile-request:(url,headers),public-profile-url,last-modified-timestamp,proposal-comments,associations,interests,publications:(id,title,publisher:(name),authors:(id,name,person),date,url,summary),patents,languages:(id,language:(name),proficiency:(level,name)),skills:(id,skill:(name)),certifications:(id,name,authority:(name),number,start-date,end-date),educations:(id,school-name,field-of-study,start-date,end-date,degree,activities,notes),courses:(id,name,number),three-current-positions:(id,title,summary,start-date,end-date,is-current,company),three-past-positions:(id,title,summary,start-date,end-date,is-current,company),num-recommenders,recommendations-received:(id,recommendation-type,recommendation-text,recommender),mfeed-rss-url,following,job-bookmarks,suggestions,date-of-birth,member-url-resources:(url,name),related-profile-views,honors-awards"]
|
23
|
+
@token = token
|
23
24
|
|
24
25
|
# Confirm API keys setup
|
26
|
+
raise StandardError, "API_KEY not set" unless !API_KEY.nil?
|
27
|
+
raise StandardError, "API_SECRET not set" unless !API_SECRET.nil?
|
25
28
|
|
26
29
|
# Setup default options to be merged with supplied ones
|
30
|
+
|
27
31
|
end
|
28
32
|
|
29
33
|
# Public: Create a resume given a LinkedIn Profile
|
@@ -42,25 +46,33 @@ module Linkedin2CV
|
|
42
46
|
|
43
47
|
# Currently only supports one LaTeX template
|
44
48
|
renderer = Linkedin2CV::LatexRenderer.new
|
45
|
-
renderer.
|
49
|
+
renderer.render(profile, options)
|
46
50
|
else
|
47
51
|
puts "Sorry mate, I don't yet support the '#{options['format']}' format'"
|
48
52
|
end
|
49
53
|
end
|
50
54
|
|
55
|
+
# Public: Authenticate & Authorize the end user (via OAuth2) and fetch access token
|
56
|
+
#
|
57
|
+
#
|
51
58
|
def auth
|
52
|
-
|
53
|
-
|
54
|
-
|
59
|
+
if @token.nil?
|
60
|
+
# If in CLI Mode, spawn a thread, run the API and open a browser window
|
61
|
+
|
62
|
+
thread = nil
|
63
|
+
if !File.exists?('.token')
|
64
|
+
auth_thread = prompt_user_authorisation
|
55
65
|
|
56
|
-
|
57
|
-
|
66
|
+
puts "Waiting to retrieve LinkedIn OAuth token"
|
67
|
+
sleep 1 until auth_thread.thread_variable?('access_token')
|
68
|
+
end
|
69
|
+
|
70
|
+
# Get Token from environment
|
71
|
+
@token = fetch_token_from_tempfile
|
72
|
+
puts "I have a token for you: #{@token}"
|
58
73
|
end
|
59
74
|
|
60
|
-
|
61
|
-
token = fetch_token_from_tempfile
|
62
|
-
puts "I have a token for you: #{token}"
|
63
|
-
token
|
75
|
+
@token
|
64
76
|
end
|
65
77
|
|
66
78
|
# Public: Fetch Profile Data
|
@@ -73,15 +85,7 @@ module Linkedin2CV
|
|
73
85
|
|
74
86
|
# Get Client info based on fields provided
|
75
87
|
client = LinkedIn::Client.new(API_KEY, API_SECRET, token)
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
# Public: Fetches a template for rendering via Asciidoctor
|
81
|
-
#
|
82
|
-
#
|
83
|
-
def lookup_template
|
84
|
-
|
88
|
+
client.profile(:fields => @profile_fields)
|
85
89
|
end
|
86
90
|
|
87
91
|
# Public: Invoke the API and browser session for end user authentication.
|
@@ -1,52 +1,35 @@
|
|
1
1
|
module Linkedin2CV
|
2
2
|
class LatexRenderer
|
3
3
|
|
4
|
-
# Public:
|
4
|
+
# Public: Render a LaTeX document
|
5
5
|
#
|
6
6
|
#
|
7
|
-
def
|
8
|
-
|
9
|
-
def clean_latex(s)
|
10
|
-
# Clean &
|
11
|
-
s = s.gsub(/(?<!\\)\&(?!\\)/, '\\\&')
|
12
|
-
|
13
|
-
# Clean $
|
14
|
-
s = s.gsub(/(?<!\\)\$(?!\\)/, '\\\$')
|
15
|
-
|
16
|
-
# Clean %
|
17
|
-
s = s.gsub(/(?<!\\)%(?!\\)/, '\\\%')
|
18
|
-
|
19
|
-
# # Clean ~
|
20
|
-
s = s.gsub(/\~/, '\\\textasciitilde')
|
21
|
-
|
22
|
-
# # Clean >
|
23
|
-
s = s.gsub(/\>/, '\\\textgreater')
|
24
|
-
|
25
|
-
# # Clean <
|
26
|
-
s = s.gsub(/\</, '\\\textless')
|
27
|
-
|
28
|
-
s
|
29
|
-
end
|
30
|
-
|
31
|
-
require 'tilt/erb'
|
7
|
+
def render(profile, options)
|
8
|
+
output = render_latex(profile, options)
|
32
9
|
output_filename = "#{options['output_file']}.latex"
|
33
|
-
|
34
|
-
|
35
|
-
output = template.render(self, :profile => profile, :options => options)
|
10
|
+
render_pdf(output, output_filename)
|
11
|
+
end
|
36
12
|
|
13
|
+
# Public: Produce a Latex PDF
|
14
|
+
#
|
15
|
+
#
|
16
|
+
def render_pdf(output, output_filename)
|
37
17
|
output_file = File.new(output_filename, 'w')
|
38
18
|
output_file.write(output)
|
39
19
|
output_file.close
|
40
20
|
|
41
21
|
# Make sure this variable is escaped, clearly.....
|
42
|
-
|
22
|
+
system("pdflatex -output-directory=/tmp/ #{output_filename}")
|
43
23
|
end
|
44
24
|
|
45
|
-
#
|
25
|
+
# Convert a LinkedIn Profile into a LaTeX source document.
|
46
26
|
#
|
47
27
|
#
|
48
|
-
def
|
49
|
-
|
28
|
+
def render_latex(profile, options)
|
29
|
+
require 'tilt/erb'
|
30
|
+
template = Tilt.new('templates/cv.erb')
|
31
|
+
output = template.render(self, :profile => profile, :options => options)
|
32
|
+
output
|
50
33
|
end
|
51
34
|
end
|
52
35
|
end
|
@@ -62,3 +45,28 @@ class ERB::Compiler
|
|
62
45
|
end
|
63
46
|
|
64
47
|
end
|
48
|
+
|
49
|
+
# Public: Prepare a string for LaTeX rendering.
|
50
|
+
#
|
51
|
+
# Escapes special chars, replaces etc.
|
52
|
+
def clean_latex(s)
|
53
|
+
# Clean &
|
54
|
+
s = s.gsub(/(?<!\\)\&(?!\\)/, '\\\&')
|
55
|
+
|
56
|
+
# Clean $
|
57
|
+
s = s.gsub(/(?<!\\)\$(?!\\)/, '\\\$')
|
58
|
+
|
59
|
+
# Clean %
|
60
|
+
s = s.gsub(/(?<!\\)%(?!\\)/, '\\\%')
|
61
|
+
|
62
|
+
# # Clean ~
|
63
|
+
s = s.gsub(/\~/, '\\\textasciitilde')
|
64
|
+
|
65
|
+
# # Clean >
|
66
|
+
s = s.gsub(/\>/, '\\\textgreater')
|
67
|
+
|
68
|
+
# # Clean <
|
69
|
+
s = s.gsub(/\</, '\\\textless')
|
70
|
+
|
71
|
+
s
|
72
|
+
end
|
data/lib/linkedin2cv/version.rb
CHANGED
data/linkedin2cv.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["matt.fellows@onegeek.com.au"]
|
11
11
|
spec.summary = "Linkedin2CV"
|
12
12
|
spec.description = "Turn your LinkedIn Profile into a professional resume in many formats (PDF / HTML5 / LaTeX / Asciidoc)"
|
13
|
-
spec.homepage = "
|
13
|
+
spec.homepage = "http://linkedin2cv.herokuapp.com"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
data/public/app/index.html
CHANGED
@@ -17,9 +17,9 @@
|
|
17
17
|
<link rel="stylesheet" href="styles/main.css">
|
18
18
|
<!-- endbuild -->
|
19
19
|
</head>
|
20
|
-
<body ng-app="
|
20
|
+
<body ng-app="linkedin2cvApp">
|
21
21
|
<!--[if lt IE 7]>
|
22
|
-
<p class="browselinkedin2cv
|
22
|
+
<p class="browselinkedin2cv">You are using an <strong>outdated</strong> browser. Please <a href="http://browselinkedin2cv.com/">upgrade your browser</a> to improve your experience.</p>
|
23
23
|
<![endif]-->
|
24
24
|
|
25
25
|
<!-- Add your site or application content here -->
|
@@ -67,7 +67,7 @@
|
|
67
67
|
<!-- build:js({.tmp,app}) scripts/scripts.js -->
|
68
68
|
<script src="scripts/app.js"></script>
|
69
69
|
<script src="scripts/controllers/main.js"></script>
|
70
|
-
<script src="scripts/services/
|
70
|
+
<script src="scripts/services/linkedin2cvapi.js"></script>
|
71
71
|
<!-- endbuild -->
|
72
72
|
</body>
|
73
73
|
</html>
|
data/public/app/scripts/app.js
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
'use strict';
|
2
2
|
|
3
3
|
angular
|
4
|
-
.module('
|
4
|
+
.module('linkedin2cvApp', [
|
5
5
|
'ngCookies',
|
6
6
|
'ngResource',
|
7
7
|
'ngSanitize',
|
8
8
|
'ngRoute',
|
9
|
-
'
|
9
|
+
'linkedin2cvServices'
|
10
10
|
])
|
11
11
|
.config(function ($routeProvider) {
|
12
12
|
$routeProvider
|
@@ -1,8 +1,8 @@
|
|
1
1
|
'use strict';
|
2
2
|
|
3
|
-
var
|
3
|
+
var linkedin2cvServices = angular.module('linkedin2cvServices', ['ngResource']);
|
4
4
|
|
5
|
-
|
5
|
+
linkedin2cvServices.factory('Linkedin2CV', ['$q', '$rootScope', '$interval', function($q, $rootScope, $interval) {
|
6
6
|
// We return this object to anything injecting our service
|
7
7
|
var Service = {};
|
8
8
|
|
data/public/app/styles/main.scss
CHANGED
@@ -6,7 +6,7 @@ $icon-font-path: "/bower_components/bootstrap-sass-official/vendor/assets/fonts/
|
|
6
6
|
@import "../bower_components/bootstrap-sass-official/vendor/assets/stylesheets/bootstrap.scss";
|
7
7
|
// endbower
|
8
8
|
|
9
|
-
.browselinkedin2cv
|
9
|
+
.browselinkedin2cv {
|
10
10
|
margin: 0.2em 0;
|
11
11
|
background: #ccc;
|
12
12
|
color: #000;
|
data/public/app/views/main.html
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
<div class="header">
|
2
2
|
<ul class="nav nav-pills pull-right">
|
3
3
|
<li class="active"><a ng-href="#">Home</a></li>
|
4
|
-
<li><a ng-href="http://github.com/mefellows/linkedin2cv
|
4
|
+
<li><a ng-href="http://github.com/mefellows/linkedin2cv-generator">About</a></li>
|
5
5
|
</ul>
|
6
6
|
<h1 class="text-muted">Linkedin2CV App</h1>
|
7
7
|
</div>
|
data/public/bower.json
CHANGED