wagon 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|