virgild-resumetools 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,231 @@
1
+ #--
2
+ # Copyright (c) 2009 Virgil Dimaguila
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person
5
+ # obtaining a copy of this software and associated documentation
6
+ # files (the "Software"), to deal in the Software without
7
+ # restriction, including without limitation the rights to use,
8
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the
10
+ # Software is furnished to do so, subject to the following
11
+ # conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ # OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ module ResumeTools
27
+
28
+ # = Resume
29
+ # Represents a single resume document
30
+ class Resume
31
+
32
+ # The title of the resume
33
+ attr_accessor :title
34
+
35
+ # The name (or label) for this resume
36
+ attr_accessor :name
37
+
38
+ # The full name of the subject of this resume
39
+ attr_accessor :full_name
40
+
41
+ # The subject's URL
42
+ attr_accessor :url
43
+
44
+ # The subject's e-mail address
45
+ attr_accessor :email
46
+
47
+ # The subject's telephone number
48
+ attr_accessor :telephone
49
+
50
+ # First line of the address
51
+ attr_accessor :address1
52
+
53
+ # Second line of the address
54
+ attr_accessor :address2
55
+
56
+ # The resume's sections
57
+ attr_reader :sections
58
+
59
+ # Creates a +Resume+ and passes it to the given block. Returns the created +Resume+.
60
+ def self.build(&block)
61
+ resume = self.new
62
+ yield resume
63
+ resume
64
+ end
65
+
66
+ # Creates a new +Resume+ with the given properties
67
+ def initialize(props={})
68
+ @title = props[:title] || ""
69
+ @name = props[:name] || "Unnamed Resume"
70
+ @full_name = props[:full_name] || ""
71
+ @url = props[:url] || ""
72
+ @email = props[:email] || ""
73
+ @telephone = props[:telephone] || ""
74
+ @address1 = props[:address1] || ""
75
+ @address2 = props[:address2] || ""
76
+ @sections = Array.new
77
+ end
78
+
79
+ # Add section
80
+ def add_section(section)
81
+ self.sections << section
82
+ self
83
+ end
84
+
85
+ # Create new section and add to sections
86
+ def create_section(props={}, &block)
87
+ section = Section.new(props)
88
+ yield section if block_given?
89
+ self.add_section(section)
90
+ self
91
+ end
92
+
93
+ #
94
+ def has_url?
95
+ !self.url.blank?
96
+ end
97
+
98
+ #
99
+ def has_email?
100
+ !self.email.blank?
101
+ end
102
+
103
+ #
104
+ def has_telephone?
105
+ !self.telephone.blank?
106
+ end
107
+
108
+ #
109
+ def has_address1?
110
+ !self.address1.blank?
111
+ end
112
+
113
+ #
114
+ def has_address2?
115
+ !self.address2.blank?
116
+ end
117
+
118
+ end
119
+
120
+
121
+ # = Section
122
+ # Represents a section in the resume
123
+ class Section
124
+ # Section title
125
+ attr_accessor :title
126
+
127
+ # Section paragraph
128
+ attr_accessor :para
129
+
130
+ # List of periods
131
+ attr_reader :periods
132
+
133
+ # List of items
134
+ attr_reader :items
135
+
136
+ #
137
+ def initialize(props={})
138
+ @title = props[:title] || ""
139
+ @para = props[:para] || ""
140
+ @items = Array.new
141
+ @periods = Array.new
142
+ end
143
+
144
+ # Creates a period and adds it to the list
145
+ def create_period(props={}, &block)
146
+ period = Period.new(props)
147
+ yield period if block_given?
148
+ self.add_period(period)
149
+ self
150
+ end
151
+
152
+ # Adds a period
153
+ def add_period(period)
154
+ self.periods << period
155
+ end
156
+
157
+ # Ads an item
158
+ def add_item(item)
159
+ self.items << item
160
+ end
161
+
162
+ # Creates an item and adds it to the list
163
+ def create_item(props={}, &block)
164
+ item = Item.new(props)
165
+ yield item if block_given?
166
+ self.add_item(item)
167
+ self
168
+ end
169
+ end
170
+
171
+
172
+ # = Period
173
+ # Represents a period in the section
174
+ class Period
175
+ # Period title
176
+ attr_accessor :title
177
+
178
+ # Period location
179
+ attr_accessor :location
180
+
181
+ # Period organization
182
+ attr_accessor :organization
183
+
184
+ # Period start date
185
+ attr_accessor :dtstart
186
+
187
+ # Period end date
188
+ attr_accessor :dtend
189
+
190
+ # List of items
191
+ attr_reader :items
192
+
193
+ #
194
+ def initialize(props={})
195
+ @title = props[:title] || ""
196
+ @location = props[:location] || ""
197
+ @organization = props[:organization] || ""
198
+ @dtstart = props[:dtstart] || nil
199
+ @dtend = props[:dtend] || nil
200
+ @items = Array.new
201
+ end
202
+
203
+ # Adds an item
204
+ def add_item(item)
205
+ self.items << item
206
+ end
207
+
208
+ # Creates an item and adds it to the list
209
+ def create_item(props={}, &block)
210
+ item = Item.new(props)
211
+ yield item if block_given?
212
+ self.add_item(item)
213
+ item
214
+ end
215
+ end
216
+
217
+
218
+ # = Item
219
+ # Represents an item in a period or section. Items are usually
220
+ # bulleted items that are listed in order
221
+ class Item
222
+ # The item text
223
+ attr_accessor :text
224
+
225
+ #
226
+ def initialize(props={})
227
+ @text = props[:text] || ""
228
+ end
229
+ end
230
+
231
+ end
@@ -0,0 +1,107 @@
1
+ #--
2
+ # Copyright (c) 2009 Virgil Dimaguila
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person
5
+ # obtaining a copy of this software and associated documentation
6
+ # files (the "Software"), to deal in the Software without
7
+ # restriction, including without limitation the rights to use,
8
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the
10
+ # Software is furnished to do so, subject to the following
11
+ # conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ # OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ module ResumeTools
27
+ module TextReader
28
+ class ParseException < Exception
29
+ end
30
+
31
+ module ClassMethods
32
+ require 'treetop'
33
+
34
+ Treetop.load File.join(File.dirname(__FILE__), '..', 'grammars', 'resume.treetop')
35
+
36
+ # Builds a new Resume instance from text
37
+ def from_text(text)
38
+ parser = ::ResumeParser.new
39
+ result = parser.parse(text)
40
+
41
+ if result.nil?
42
+ raise ParseException.new(parser.failure_reason)
43
+ end
44
+
45
+ resume = ResumeTools::Resume.new
46
+ current_section = nil
47
+ current_period = nil
48
+
49
+ result.elements.each do |element|
50
+ case element.data_type
51
+ when :item
52
+ item = ::ResumeTools::Item.new(:text => element.value)
53
+ if current_period
54
+ current_period.add_item(item)
55
+ elsif current_section
56
+ current_section.add_item(item)
57
+ end
58
+ when :period
59
+ current_period = ::ResumeTools::Period.new(:title => element.value)
60
+ current_section.add_period(current_period) if current_section
61
+ when :section
62
+ current_period = nil # Reset period
63
+ current_section = ::ResumeTools::Section.new(:title => element.value)
64
+ resume.add_section(current_section)
65
+ when :period_location
66
+ current_period.location = element.value if current_period
67
+ when :period_organization
68
+ current_period.organization = element.value if current_period
69
+ when :period_dates
70
+ if current_period
71
+ dates = element.value.split("to", 2).map { |d| d.strip }
72
+ if dates.length == 1
73
+ current_period.dtend = dates[0]
74
+ current_period.dtstart = nil
75
+ elsif dates.length == 2
76
+ current_period.dtstart = dates[0]
77
+ current_period.dtend = dates[1]
78
+ end
79
+ end
80
+ when :paragraph
81
+ current_section.para = element.value if current_section
82
+ when :contact_name
83
+ resume.full_name = element.value
84
+ when :contact_telephone
85
+ resume.telephone = element.value
86
+ when :contact_email
87
+ resume.email = element.value
88
+ when :contact_address
89
+ if resume.address1.blank?
90
+ resume.address1 = element.value
91
+ elsif resume.address2.blank?
92
+ resume.address2 = element.value
93
+ end
94
+ when :contact_url
95
+ resume.url = element.value
96
+ end
97
+ end
98
+
99
+ resume
100
+ end
101
+ end
102
+ end
103
+
104
+ class << Resume
105
+ include TextReader::ClassMethods
106
+ end
107
+ end
@@ -0,0 +1,35 @@
1
+ #--
2
+ # Resume Tools, Copyright (c) 2009 Virgil Dimaguila
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person
5
+ # obtaining a copy of this software and associated documentation
6
+ # files (the "Software"), to deal in the Software without
7
+ # restriction, including without limitation the rights to use,
8
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the
10
+ # Software is furnished to do so, subject to the following
11
+ # conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ # OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ unless defined? ResumeTools::VERSION
27
+ module ResumeTools
28
+ module VERSION
29
+ MAJOR = 0
30
+ MINOR = 2
31
+ TINY = 0
32
+ STRING = [MAJOR, MINOR, TINY].join('.')
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,34 @@
1
+ #--
2
+ # Copyright (c) 2009 Virgil Dimaguila
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person
5
+ # obtaining a copy of this software and associated documentation
6
+ # files (the "Software"), to deal in the Software without
7
+ # restriction, including without limitation the rights to use,
8
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the
10
+ # Software is furnished to do so, subject to the following
11
+ # conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ # OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ $:.unshift(File.dirname(__FILE__))
27
+
28
+ require "extlib"
29
+ require "uuidtools"
30
+
31
+ require "resumetools/version"
32
+ require "resume/resume"
33
+ require "resume/text_reader"
34
+ require "resume/pdf"
@@ -0,0 +1,93 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+ require "treetop"
3
+
4
+ Treetop.load File.join(File.dirname(__FILE__), "../lib/grammars/resume.treetop")
5
+
6
+ describe "Resume Grammar" do
7
+ before(:all) do
8
+ resume_file = "sample.resume"
9
+ @parser = ResumeParser.new
10
+ @resume_txt = File.read(File.join(File.dirname(__FILE__), "../examples/#{resume_file}"))
11
+ end
12
+
13
+ it "should have a parser" do
14
+ @parser.should be_instance_of(ResumeParser)
15
+ end
16
+
17
+ describe "after parsing sample file" do
18
+ def elements_with_type(type)
19
+ @elements.reject { |e| e.data_type != type }
20
+ end
21
+
22
+ before(:all) do
23
+ @result = @parser.parse(@resume_txt)
24
+ @elements = @result.elements
25
+ end
26
+
27
+ it "should parse our file" do
28
+ @result.should_not be_nil
29
+ end
30
+
31
+ it "should have 33 items" do
32
+ elements_with_type(:item).length.should == 33
33
+ end
34
+
35
+ it "should have 2 paragraphs" do
36
+ elements_with_type(:paragraph).length.should == 2
37
+ end
38
+
39
+ it "should have 7 sections" do
40
+ elements_with_type(:section).length.should == 7
41
+ end
42
+
43
+ it "should have contact name" do
44
+ elements_with_type(:contact_name).length.should == 1
45
+ elements_with_type(:contact_name)[0].value.should == "Thomas B. Seeker"
46
+ end
47
+
48
+ it "should have contact telephone" do
49
+ elements_with_type(:contact_telephone).length.should == 1
50
+ elements_with_type(:contact_telephone)[0].value.should == "(410) 555-1212"
51
+ end
52
+
53
+ it "should have contact addresses" do
54
+ elements_with_type(:contact_address).length.should == 2
55
+ elements_with_type(:contact_address)[0].value.should == "1234 Northern Star Circle"
56
+ elements_with_type(:contact_address)[1].value.should == "Baltimore, MD 12345"
57
+ end
58
+
59
+ it "should have contact e-mail" do
60
+ elements_with_type(:contact_email).length.should == 1
61
+ elements_with_type(:contact_email)[0].value.should == "seeker@nettcom.com"
62
+ end
63
+
64
+ it "should have contact URL" do
65
+ elements_with_type(:contact_url).length.should == 1
66
+ elements_with_type(:contact_url)[0].value.should == "http://nettcom.com/tseeker"
67
+ end
68
+
69
+ it "should have 5 periods" do
70
+ elements_with_type(:period).length.should == 5
71
+ end
72
+
73
+ it "should have 5 period organizations" do
74
+ elements_with_type(:period_organization).length.should == 5
75
+ elements_with_type(:period_organization).map { |o| o.value }.should == [
76
+ "Computer Engineering Corporation",
77
+ "Business Consultants, Inc.",
78
+ "US Army Infantry",
79
+ "Air Force Institute of Technology (AFIT)",
80
+ "University of California, Los Angeles (UCLA)"
81
+ ]
82
+ end
83
+
84
+ it "should have 3 period locations" do
85
+ elements_with_type(:period_location).length.should == 3
86
+ end
87
+
88
+ it "should have 3 period dates" do
89
+ elements_with_type(:period_dates).length.should == 3
90
+ end
91
+ end
92
+
93
+ end
@@ -0,0 +1,61 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe "Creating a resume from a text" do
4
+ before do
5
+ sample = File.join(File.dirname(__FILE__), '..', 'examples', 'sample.resume')
6
+ text = File.read(sample)
7
+ @resume = ResumeTools::Resume.from_text(text)
8
+ end
9
+
10
+ it "should create a resume" do
11
+ @resume.should be_instance_of(ResumeTools::Resume)
12
+ end
13
+
14
+ it "should have the correct contact info" do
15
+ @resume.full_name.should == "Thomas B. Seeker"
16
+ @resume.address1.should == "1234 Northern Star Circle"
17
+ @resume.address2.should == "Baltimore, MD 12345"
18
+ @resume.telephone.should == "(410) 555-1212"
19
+ end
20
+
21
+ it "should have 7 sections" do
22
+ @resume.should have(7).sections
23
+ end
24
+
25
+ it "should have sections in order" do
26
+ @resume.sections.map { |s| s.title }.should == [
27
+ "Career Goal",
28
+ "Qualifications Summary",
29
+ "Technical Skills",
30
+ "Professional Experience",
31
+ "Education",
32
+ "Specialized Training",
33
+ "Certification, Honors, and Professional Affiliations"
34
+ ]
35
+ end
36
+
37
+ it "should have a paragraph in the Career Goal section" do
38
+ @resume.sections[0].para.should_not be_blank
39
+ end
40
+
41
+ it "should have a paragraph in the Qualifications Summary section" do
42
+ @resume.sections[1].para.should_not be_blank
43
+ end
44
+
45
+ it "should have 4 items in the Technical Skills section" do
46
+ @resume.sections[2].should have(4).items
47
+ end
48
+
49
+ it "should have 3 periods in the Professional Experience section" do
50
+ @resume.sections[3].should have(3).periods
51
+ end
52
+
53
+ it "should have 4 items in the Systems Engineer period" do
54
+ @resume.sections[3].periods[0].should have(4).items
55
+ end
56
+
57
+ it "should have proper dates" do
58
+ @resume.sections[3].periods[0].dtstart.should == "1993"
59
+ @resume.sections[3].periods[0].dtend.should == "Present"
60
+ end
61
+ end
@@ -0,0 +1,37 @@
1
+ #--
2
+ # Copyright (c) 2009 Virgil Dimaguila
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person
5
+ # obtaining a copy of this software and associated documentation
6
+ # files (the "Software"), to deal in the Software without
7
+ # restriction, including without limitation the rights to use,
8
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the
10
+ # Software is furnished to do so, subject to the following
11
+ # conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ # OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ require File.join(File.dirname(__FILE__), "spec_helper")
27
+
28
+ describe "Rendering a Resume to PDF" do
29
+ before do
30
+ @resume = ResumeTools::Resume.new
31
+ end
32
+
33
+ it "should render to PDF" do
34
+ result = @resume.render_pdf
35
+ result.should_not be_nil
36
+ end
37
+ end