wagon 0.9.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.
- data/.document +5 -0
- data/.gitignore +23 -0
- data/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/bin/wagon +157 -0
- data/cache/.gitignore +1 -0
- data/extra/placeholder.jpg +0 -0
- data/lib/wagon.rb +11 -0
- data/lib/wagon/address.rb +35 -0
- data/lib/wagon/connection.rb +81 -0
- data/lib/wagon/directory.rb +72 -0
- data/lib/wagon/household.rb +63 -0
- data/lib/wagon/member.rb +11 -0
- data/lib/wagon/page.rb +31 -0
- data/lib/wagon/phone_number.rb +23 -0
- data/lib/wagon/photo_directory.rb +28 -0
- data/lib/wagon/ward.rb +29 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/wagon/directory_spec.rb +18 -0
- data/spec/wagon/photo_directory_spec.rb +13 -0
- data/spec/wagon/ward_spec.rb +18 -0
- data/spec/wagon_spec.rb +14 -0
- metadata +133 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Devin Christensen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "wagon"
|
8
|
+
gem.summary = %Q{Create a PDF from the lds.org ward Photo Directory.}
|
9
|
+
gem.description = %Q{Provided a valid lds.org username and password, Wagon will download all the information from the Photo Directory page and compile it into a convenient PDF.}
|
10
|
+
gem.email = "devin@threetrieslater.com"
|
11
|
+
gem.homepage = "http://github.com/threetrieslater/wagon"
|
12
|
+
gem.authors = ["Devin Christensen"]
|
13
|
+
gem.bindir = 'bin'
|
14
|
+
gem.add_dependency "nokogiri", ">= 1.4.0"
|
15
|
+
gem.add_dependency "highline", ">= 1.5.1"
|
16
|
+
gem.add_dependency "prawn", ">= 0.5.1"
|
17
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
18
|
+
gem.add_development_dependency "yard", ">= 0"
|
19
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
20
|
+
end
|
21
|
+
Jeweler::GemcutterTasks.new
|
22
|
+
rescue LoadError
|
23
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'spec/rake/spectask'
|
27
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
28
|
+
spec.libs << 'lib' << 'spec'
|
29
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
30
|
+
end
|
31
|
+
|
32
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
33
|
+
spec.libs << 'lib' << 'spec'
|
34
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
35
|
+
spec.rcov = true
|
36
|
+
end
|
37
|
+
|
38
|
+
task :spec => :check_dependencies
|
39
|
+
|
40
|
+
task :default => :spec
|
41
|
+
|
42
|
+
begin
|
43
|
+
require 'yard'
|
44
|
+
YARD::Rake::YardocTask.new
|
45
|
+
rescue LoadError
|
46
|
+
task :yardoc do
|
47
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
48
|
+
end
|
49
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.9.0
|
data/bin/wagon
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# == Synopsis
|
4
|
+
# Create a PDF from your wards photo directory on lds.org
|
5
|
+
#
|
6
|
+
# == Examples
|
7
|
+
# Output a file named "photo_directory.pdf" to the current directory.
|
8
|
+
# wagon
|
9
|
+
#
|
10
|
+
# Other examples:
|
11
|
+
# wagon --title="Ward Menu"
|
12
|
+
#
|
13
|
+
# == Usage
|
14
|
+
# wagon [options] [output_file]
|
15
|
+
#
|
16
|
+
# == Options
|
17
|
+
# -h, --help Displays help message
|
18
|
+
# -v, --version Display the version, then exit
|
19
|
+
# -V, --verbose Verbose output
|
20
|
+
# -t, --title Override the default title with your own
|
21
|
+
# -r, --rows Number of rows per page (default is 6)
|
22
|
+
# -c, --columns Number of columns per page (default is 7)
|
23
|
+
# -p, --padding Padding between households (default is 2)
|
24
|
+
# -f, --font-size Primary font size (default is 8)
|
25
|
+
# --page-numbers Include page numbers in the footer, e.g. (1 of 3)
|
26
|
+
# --date Include the current date in the footer
|
27
|
+
# --no-picture Do not include pictures
|
28
|
+
# --no-address Do not include street addresses
|
29
|
+
# --no-phone Do not include phone numbers
|
30
|
+
# --no-email Do not include email addresses
|
31
|
+
#
|
32
|
+
# == Copyright
|
33
|
+
# Copyright (c) 2009 Devin Christensen. Licensed under the MIT License:
|
34
|
+
# http://www.opensource.org/licenses/mit-license.php
|
35
|
+
|
36
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
37
|
+
|
38
|
+
require 'optparse'
|
39
|
+
require 'rdoc/usage'
|
40
|
+
require 'ostruct'
|
41
|
+
require 'wagon'
|
42
|
+
require 'highline/import'
|
43
|
+
|
44
|
+
class WagonApp
|
45
|
+
def initialize(arguments, stdin)
|
46
|
+
@arguments = arguments
|
47
|
+
@stdin = stdin
|
48
|
+
@options = OpenStruct.new()
|
49
|
+
|
50
|
+
# Set defaults
|
51
|
+
@options.verbose = false
|
52
|
+
@options.title = nil
|
53
|
+
@options.rows = 6
|
54
|
+
@options.columns = 7
|
55
|
+
@options.padding = 2
|
56
|
+
@options.font_size = 8
|
57
|
+
@options.page_numbers = false
|
58
|
+
@options.include_date = false
|
59
|
+
@options.picture = true
|
60
|
+
@options.address = true
|
61
|
+
@options.phone_number = true
|
62
|
+
@options.email = true
|
63
|
+
@options.output_file = "./photo_directory.pdf"
|
64
|
+
end
|
65
|
+
|
66
|
+
def run
|
67
|
+
if arguments_valid?
|
68
|
+
puts "Start at #{DateTime.now}\n\n" if @options.verbose
|
69
|
+
|
70
|
+
output_options if @options.verbose # [Optional]
|
71
|
+
|
72
|
+
process
|
73
|
+
|
74
|
+
puts "\nFinished in #{DateTime.now}" if @options.verbose
|
75
|
+
|
76
|
+
else
|
77
|
+
output_usage
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
def arguments_valid?
|
84
|
+
# Specify options
|
85
|
+
opts = OptionParser.new
|
86
|
+
opts.on('-v', '--version') { output_version ; exit 0 }
|
87
|
+
opts.on('-h', '--help') { output_help }
|
88
|
+
opts.on('-V', '--verbose') { @options.verbose = true }
|
89
|
+
opts.on('-t', '--title=TITLE') { |title| @options.title = title }
|
90
|
+
opts.on('-r', '--rows=ROWS') { |rows| @options.rows = rows }
|
91
|
+
opts.on('-c', '--columns=COLUMNS') { |columns| @options.columns = columns }
|
92
|
+
opts.on('-p', '--padding=PADDING') { |padding| @options.padding = padding }
|
93
|
+
opts.on('-f', '--font-size=SIZE') { |size| @options.font_size = size }
|
94
|
+
opts.on('--page-numbers') { @options.page_numbering = true }
|
95
|
+
opts.on('--date') { @options.include_date = true }
|
96
|
+
opts.on('--no-picture') { @options.picture = false }
|
97
|
+
opts.on('--no-address') { @options.address = false }
|
98
|
+
opts.on('--no-phone') { @options.phone_number = false }
|
99
|
+
opts.on('--no-email') { @options.email = false }
|
100
|
+
|
101
|
+
opts.parse!(@arguments) rescue return false
|
102
|
+
@options.output_file = @arguments.last unless @arguments.empty?
|
103
|
+
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
def output_options
|
108
|
+
puts "Options:\n"
|
109
|
+
|
110
|
+
@options.marshal_dump.each do |name, val|
|
111
|
+
puts " #{name} = #{val}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def output_help
|
116
|
+
output_version
|
117
|
+
RDoc::usage()
|
118
|
+
end
|
119
|
+
|
120
|
+
def output_usage
|
121
|
+
RDoc::usage('usage') # gets usage from comments above
|
122
|
+
end
|
123
|
+
|
124
|
+
def output_version
|
125
|
+
puts "#{File.basename(__FILE__)} version #{Wagon::VERSION}"
|
126
|
+
end
|
127
|
+
|
128
|
+
def process
|
129
|
+
username = ask("What is your lds.org username? ")
|
130
|
+
password = ask("What is your lds.org password? ") { |q| q.echo = "*" }
|
131
|
+
|
132
|
+
user = Wagon::connect(username, password)
|
133
|
+
|
134
|
+
puts "\nAlright, we're in!"
|
135
|
+
puts "I'm gonna go ahead and create that PDF for ya now."
|
136
|
+
puts "It might take a few minutes to gather all the info"
|
137
|
+
puts "so grab a crisp cool beverage, sit back and relax."
|
138
|
+
puts "I'll take it from here."
|
139
|
+
|
140
|
+
directory = user.ward.to_pdf( @options.marshal_dump )
|
141
|
+
directory.render_file(@options.output_file)
|
142
|
+
|
143
|
+
puts "\nFinished. Enjoy.\n"
|
144
|
+
|
145
|
+
rescue Wagon::AuthenticationFailure
|
146
|
+
puts "\nThe username and password combination you entered is invalid."
|
147
|
+
rescue
|
148
|
+
if @options.verbose
|
149
|
+
raise
|
150
|
+
else
|
151
|
+
puts "\nI encountered an unexpected problem, and I don't know what to do. :("
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
app = WagonApp.new(ARGV, STDIN)
|
157
|
+
app.run
|
data/cache/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.cache
|
Binary file
|
data/lib/wagon.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
module Wagon
|
2
|
+
BASE_PATH = File.join(File.dirname(__FILE__), '..')
|
3
|
+
VERSION = open(File.join(BASE_PATH, 'VERSION')).read()
|
4
|
+
|
5
|
+
def self.connect(username, password)
|
6
|
+
Connection.new(username, password)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'wagon/page'
|
11
|
+
require 'wagon/connection'
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Wagon
|
2
|
+
class Address
|
3
|
+
CITY_STATE_ZIP = %r/^(\D+), (\D+)?\s*(\d+(-\d+)?)?$/
|
4
|
+
|
5
|
+
attr_reader :street, :city, :state, :zip, :country
|
6
|
+
|
7
|
+
def self.extract_from_string(string)
|
8
|
+
parts = string.split("\n").collect(&:strip).delete_if(&:empty?)
|
9
|
+
street = city = state = zip = country = nil
|
10
|
+
|
11
|
+
parts.delete_if do |part|
|
12
|
+
next unless part =~ CITY_STATE_ZIP
|
13
|
+
city, state, zip = $1, ($2 || '').strip(), $3; true
|
14
|
+
end
|
15
|
+
|
16
|
+
self.new(parts.shift, city, state, zip, parts.shift)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(street, city, state, zip, country)
|
20
|
+
@street, @city, @state, @zip, @country = street, city, state, zip, country
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
[street, [[city, state].compact.join(", "), zip, country.to_s.empty? ? nil : "(#{country})"].compact.join(" ")].compact.join("\n")
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
street == other.street &&
|
29
|
+
city == other.city &&
|
30
|
+
state == other.state &&
|
31
|
+
zip == other.zip &&
|
32
|
+
country == other.country
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'uri'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'wagon/ward'
|
6
|
+
|
7
|
+
module Wagon
|
8
|
+
|
9
|
+
class AuthenticationFailure < StandardError; end
|
10
|
+
|
11
|
+
class Connection
|
12
|
+
HOST = 'secure.lds.org'
|
13
|
+
LOGIN_PATH = '/units/a/login/1,21568,779-1,00.html?URL='
|
14
|
+
|
15
|
+
def initialize(username, password)
|
16
|
+
response = post(LOGIN_PATH, 'username' => username, 'password' => password)
|
17
|
+
@cookies = response['set-cookie']
|
18
|
+
@home_path = URI.parse(response['location']).path
|
19
|
+
|
20
|
+
raise AuthenticationFailure.new("Invalid username and/or password") unless @cookies
|
21
|
+
end
|
22
|
+
|
23
|
+
def home_path
|
24
|
+
@home_path
|
25
|
+
end
|
26
|
+
|
27
|
+
def ward
|
28
|
+
@ward ||= Ward.new(self, home_path)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get(path)
|
32
|
+
Connection.perform_caching? ? get_with_caching(path) : get_without_caching(path)
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_without_caching(path)
|
36
|
+
_http.request(Net::HTTP::Get.new(path, {'Cookie' => @cookies || ''})).body
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_with_caching(path)
|
40
|
+
cache_path = File.join(Wagon::BASE_PATH, 'cache', Digest::SHA1.hexdigest(path) + ".cache")
|
41
|
+
return open(cache_path).read if File.exists?(cache_path)
|
42
|
+
open(cache_path, "w").write(data = get_without_caching(path))
|
43
|
+
data
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.perform_caching?
|
47
|
+
@@perform_caching ||= true
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.perform_caching(true_or_false)
|
51
|
+
@@perform_caching = true_or_false
|
52
|
+
end
|
53
|
+
|
54
|
+
def post(path, data)
|
55
|
+
request = Net::HTTP::Post.new(path, {'Cookie' => @cookies || ''})
|
56
|
+
request.set_form_data(data)
|
57
|
+
_http.request(request)
|
58
|
+
end
|
59
|
+
|
60
|
+
def _dump(depth)
|
61
|
+
Marshal.dump([@cookies, @home_path])
|
62
|
+
end
|
63
|
+
|
64
|
+
def self._load(string)
|
65
|
+
attributes = Marshal.restore(string)
|
66
|
+
connection = Connection.allocate()
|
67
|
+
connection.instance_variable_set(:@cookies, attributes.shift)
|
68
|
+
connection.instance_variable_set(:@home_path, attributes.shift)
|
69
|
+
connection
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def _http
|
74
|
+
return @http unless @http.nil?
|
75
|
+
@http = Net::HTTP.new(HOST, 443)
|
76
|
+
@http.use_ssl = true
|
77
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
78
|
+
@http
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'wagon/photo_directory'
|
2
|
+
require 'prawn'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module Wagon
|
6
|
+
class Directory < Page
|
7
|
+
def photo_directory_path
|
8
|
+
return @photo_directory_path unless @photo_directory_path.nil?
|
9
|
+
|
10
|
+
self.at('a.linknoline[href^="javascript:confirm_photo"]')['href'].match(/^javascript:confirm_photo\('(.*)'\);$/)
|
11
|
+
@photo_directory_path = $1
|
12
|
+
end
|
13
|
+
|
14
|
+
def photo_directory
|
15
|
+
@photo_directory ||= PhotoDirectory.new(connection, photo_directory_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def households
|
19
|
+
@households ||= photo_directory.households.sort
|
20
|
+
end
|
21
|
+
|
22
|
+
def members
|
23
|
+
households.collect(&:members).flatten()
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_pdf(options = {})
|
27
|
+
options = {:columns => 7, :rows => 6, :padding => 2, :font_size => 8, :address => true, :phone_number => true, :email => true}.merge(options)
|
28
|
+
|
29
|
+
Prawn::Document.new(:skip_page_creation => true, :left_margin => 10, :right_margin => 10, :top_margin => 20, :bottom_margin => 10) do |pdf|
|
30
|
+
columns = options[:columns].to_f
|
31
|
+
rows = options[:rows].to_f
|
32
|
+
padding = options[:padding].to_f
|
33
|
+
grid_width = pdf.bounds.width / columns
|
34
|
+
grid_height = pdf.bounds.height / rows
|
35
|
+
box_width = grid_width - (padding * 2)
|
36
|
+
box_height = grid_height - (padding * 2)
|
37
|
+
pages = (households.size.to_f / (columns * rows)).ceil()
|
38
|
+
info_lines = 1 + [:address, :phone_number, :email].inject(0) { |sum, item| sum += item ? 1 : 0 }
|
39
|
+
pdf.font_size = options[:font_size].to_i
|
40
|
+
|
41
|
+
(0...pages).each do |page|
|
42
|
+
pdf.start_new_page
|
43
|
+
(0...rows).each do |row|
|
44
|
+
y = pdf.bounds.top - row*grid_height
|
45
|
+
(0...columns).each do |column|
|
46
|
+
break if (index = page*rows*columns+row*columns+column) >= households.size
|
47
|
+
household = households[index]
|
48
|
+
x = pdf.bounds.left + column*grid_width
|
49
|
+
pdf.bounding_box([x, y], :width => grid_width, :height => grid_height) do
|
50
|
+
pdf.bounding_box([pdf.bounds.left + padding, pdf.bounds.top - padding], :width => box_width, :height => box_height) do
|
51
|
+
information = []
|
52
|
+
information.push(household.name)
|
53
|
+
information.push(*household.address.street) if options[:address]
|
54
|
+
information.push(household.phone_number) if options[:phone_number]
|
55
|
+
information.push(household.members.first.email) if options[:email]
|
56
|
+
|
57
|
+
pdf.image(household.has_image? ? StringIO.new(household.image_data) : './extra/placeholder.jpg',
|
58
|
+
:position => :center, :fit => [box_width, box_height - (padding*2 + pdf.font.height*info_lines)] )
|
59
|
+
|
60
|
+
pdf.move_down(padding)
|
61
|
+
information.compact.each do |line|
|
62
|
+
pdf.text(line, :align => :center, :size => pdf.font_size.downto(1).detect() { |size| pdf.width_of(line.to_s, :size => size) <= box_width })
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'wagon/address'
|
2
|
+
require 'wagon/phone_number'
|
3
|
+
require 'wagon/member'
|
4
|
+
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module Wagon
|
8
|
+
class Household
|
9
|
+
attr_reader :connection
|
10
|
+
attr_writer :name
|
11
|
+
attr_accessor :address, :phone_number, :image_path, :members
|
12
|
+
|
13
|
+
def initialize(connection)
|
14
|
+
@members = []
|
15
|
+
@connection = connection
|
16
|
+
end
|
17
|
+
|
18
|
+
def name
|
19
|
+
self.individual? ? "#{members.first.name} #{@name}" : "#{@name} Household"
|
20
|
+
end
|
21
|
+
|
22
|
+
def reversed_name
|
23
|
+
self.individual? ? "#{@name}, #{members.first.name}" : name
|
24
|
+
end
|
25
|
+
|
26
|
+
def individual?
|
27
|
+
members.count == 1
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_image?
|
31
|
+
!image_path.to_s.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def image_data
|
35
|
+
@image_data ||= image_path.to_s.empty? ? "" : connection.get(image_path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def <=>(other)
|
39
|
+
if has_image? == other.has_image?
|
40
|
+
reversed_name <=> other.reversed_name
|
41
|
+
else
|
42
|
+
has_image? ? -1 : 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.create_from_td(connection, td)
|
47
|
+
Household.new(connection).instance_eval do
|
48
|
+
name_element, phone_element, *member_elements = *td.search('table > tr > td.eventsource[width="45%"] > table > tr > td.eventsource')
|
49
|
+
@address = Address.extract_from_string(td.search('table > tr > td.eventsource[width="25%"]').inner_text)
|
50
|
+
@image_path = td.at('table > tr > td.eventsource[width="30%"] > img')['src'] rescue nil
|
51
|
+
@name = name_element.inner_text
|
52
|
+
@phone_number = PhoneNumber.extract_from_string(phone_element.inner_text)
|
53
|
+
|
54
|
+
member_elements.each_slice(2) do |name_and_email|
|
55
|
+
name, email = *name_and_email.collect { |element| element.inner_text.gsub(/\302\240/, '').strip() }
|
56
|
+
@members << Member.new(self, name, email)
|
57
|
+
end
|
58
|
+
|
59
|
+
self
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/wagon/member.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
class Member
|
2
|
+
attr_reader :household, :name, :email
|
3
|
+
|
4
|
+
def initialize(household, name, email)
|
5
|
+
@household, @name, @email = household, name, email
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
[name, household.name, email.to_s.empty? ? nil : "<#{email}>"].compact.join(" ")
|
10
|
+
end
|
11
|
+
end
|
data/lib/wagon/page.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Wagon
|
4
|
+
class Page
|
5
|
+
attr_reader :connection
|
6
|
+
|
7
|
+
def initialize(connection, url)
|
8
|
+
@connection, @url = connection, url
|
9
|
+
end
|
10
|
+
|
11
|
+
def source
|
12
|
+
@source ||= Nokogiri::HTML(get(@url))
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(*args)
|
16
|
+
connection.get(*args)
|
17
|
+
end
|
18
|
+
|
19
|
+
def post(*args)
|
20
|
+
connection.post(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(method, *args, &block)
|
24
|
+
unless source.respond_to?(method)
|
25
|
+
super(method, *args, &block)
|
26
|
+
else
|
27
|
+
source.send(method, *args, &block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Wagon
|
2
|
+
class PhoneNumber
|
3
|
+
attr_reader :type, :value
|
4
|
+
|
5
|
+
def self.extract_from_string(string)
|
6
|
+
string.strip =~ /([\)\(\+\s\-\d]+)(\((.*)\))?$/
|
7
|
+
self.new($3 || 'Home', $1.strip)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(type, value)
|
11
|
+
@type, @value = type, value
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
type == other.type &&
|
16
|
+
value == other.value
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
self.value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'wagon/household'
|
2
|
+
|
3
|
+
module Wagon
|
4
|
+
class PhotoDirectory < Page
|
5
|
+
def households
|
6
|
+
@households ||= _parse_households
|
7
|
+
end
|
8
|
+
|
9
|
+
def members
|
10
|
+
households.collect(&:members).flatten()
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_pdf(options)
|
14
|
+
Prawn::Document.new() do |pdf|
|
15
|
+
households.each do |household|
|
16
|
+
pdf.text household.name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def _parse_households
|
23
|
+
self.search('body > table > tr > td.eventsource[@width="25%"]').collect do |household_td|
|
24
|
+
household = Household.create_from_td(connection, household_td)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/wagon/ward.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'wagon/directory'
|
2
|
+
|
3
|
+
module Wagon
|
4
|
+
class Ward < Page
|
5
|
+
def name
|
6
|
+
@name ||= self.at('a.channeltitle[href^="/units/home"]').inner_text.strip
|
7
|
+
end
|
8
|
+
|
9
|
+
def directory_path
|
10
|
+
@directory_path ||= self.at('a.directory[href^="/units/a/directory"]')['href']
|
11
|
+
end
|
12
|
+
|
13
|
+
def directory
|
14
|
+
@directory ||= Directory.new(connection, directory_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def households
|
18
|
+
directory.households
|
19
|
+
end
|
20
|
+
|
21
|
+
def members
|
22
|
+
households.collect(&:members).flatten()
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_pdf(options)
|
26
|
+
directory.to_pdf(options)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
BASE_PATH = File.join(File.dirname(__FILE__), '..')
|
2
|
+
USER_FILE = File.join(BASE_PATH, 'spec', 'user.dat')
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
5
|
+
$LOAD_PATH.unshift(File.join(BASE_PATH, 'lib'))
|
6
|
+
|
7
|
+
require 'spec'
|
8
|
+
require 'spec/autorun'
|
9
|
+
require 'rubygems'
|
10
|
+
require 'wagon'
|
11
|
+
require 'highline/import'
|
12
|
+
|
13
|
+
$user = nil
|
14
|
+
|
15
|
+
def establish_connection
|
16
|
+
username = ask("What is your lds.org username? ")
|
17
|
+
password = ask("What is your lds.org password? ") { |prompt| prompt.echo = "*" }
|
18
|
+
|
19
|
+
$user = Wagon::connect(username, password)
|
20
|
+
open(USER_FILE, 'w').write(Marshal.dump($user))
|
21
|
+
end
|
22
|
+
|
23
|
+
def restore_connection
|
24
|
+
$user = Marshal.restore(open(USER_FILE).read)
|
25
|
+
end
|
26
|
+
|
27
|
+
if File.exists?(USER_FILE)
|
28
|
+
restore_connection()
|
29
|
+
else
|
30
|
+
establish_connection()
|
31
|
+
end
|
32
|
+
|
33
|
+
Spec::Runner.configure do |config|
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
|
2
|
+
|
3
|
+
describe "Wagon::Directory" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@page = Wagon::Directory.new($user, $user.ward.directory_path)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should find the photo directory link" do
|
10
|
+
@page.photo_directory_path.should_not be_nil
|
11
|
+
@page.photo_directory_path.should match(%r{^/units/a/directory/photoprint})
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should be able to generate a pdf" do
|
15
|
+
@page.to_pdf.render_file("./photo_directory.pdf")
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
|
2
|
+
|
3
|
+
describe "Wagon::PhotoDirectory" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@page = Wagon::PhotoDirectory.new($user, $user.ward.directory.photo_directory_path)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should parse out the households correctly" do
|
10
|
+
@page.households.should have_at_least(10).items
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
|
2
|
+
|
3
|
+
describe "Wagon::Ward" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@page = Wagon::Ward.new($user, $user.home_path)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should have a name" do
|
10
|
+
@page.name.to_s.should_not be_empty
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should find the directory link" do
|
14
|
+
@page.directory_path.should_not be_nil
|
15
|
+
@page.directory_path.should match(%r{^/units/a/directory})
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
data/spec/wagon_spec.rb
ADDED
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wagon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Devin Christensen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-07 00:00:00 -07:00
|
13
|
+
default_executable: wagon
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: nokogiri
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.4.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: highline
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.5.1
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: prawn
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.5.1
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: rspec
|
47
|
+
type: :development
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.2.9
|
54
|
+
version:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
type: :development
|
58
|
+
version_requirement:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
description: Provided a valid lds.org username and password, Wagon will download all the information from the Photo Directory page and compile it into a convenient PDF.
|
66
|
+
email: devin@threetrieslater.com
|
67
|
+
executables:
|
68
|
+
- wagon
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
extra_rdoc_files:
|
72
|
+
- LICENSE
|
73
|
+
- README.rdoc
|
74
|
+
files:
|
75
|
+
- .document
|
76
|
+
- .gitignore
|
77
|
+
- LICENSE
|
78
|
+
- README.rdoc
|
79
|
+
- Rakefile
|
80
|
+
- VERSION
|
81
|
+
- bin/wagon
|
82
|
+
- cache/.gitignore
|
83
|
+
- extra/placeholder.jpg
|
84
|
+
- lib/wagon.rb
|
85
|
+
- lib/wagon/address.rb
|
86
|
+
- lib/wagon/connection.rb
|
87
|
+
- lib/wagon/directory.rb
|
88
|
+
- lib/wagon/household.rb
|
89
|
+
- lib/wagon/member.rb
|
90
|
+
- lib/wagon/page.rb
|
91
|
+
- lib/wagon/phone_number.rb
|
92
|
+
- lib/wagon/photo_directory.rb
|
93
|
+
- lib/wagon/ward.rb
|
94
|
+
- spec/spec.opts
|
95
|
+
- spec/spec_helper.rb
|
96
|
+
- spec/wagon/directory_spec.rb
|
97
|
+
- spec/wagon/photo_directory_spec.rb
|
98
|
+
- spec/wagon/ward_spec.rb
|
99
|
+
- spec/wagon_spec.rb
|
100
|
+
has_rdoc: true
|
101
|
+
homepage: http://github.com/threetrieslater/wagon
|
102
|
+
licenses: []
|
103
|
+
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options:
|
106
|
+
- --charset=UTF-8
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: "0"
|
114
|
+
version:
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: "0"
|
120
|
+
version:
|
121
|
+
requirements: []
|
122
|
+
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 1.3.5
|
125
|
+
signing_key:
|
126
|
+
specification_version: 3
|
127
|
+
summary: Create a PDF from the lds.org ward Photo Directory.
|
128
|
+
test_files:
|
129
|
+
- spec/spec_helper.rb
|
130
|
+
- spec/wagon/directory_spec.rb
|
131
|
+
- spec/wagon/photo_directory_spec.rb
|
132
|
+
- spec/wagon/ward_spec.rb
|
133
|
+
- spec/wagon_spec.rb
|