itinerary 0.1

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,30 @@
1
+ class Cache
2
+
3
+ def initialize(dir)
4
+ @dir = dir
5
+ @dir.mkpath unless @dir.exist?
6
+ end
7
+
8
+ def path_for_key(key)
9
+ @dir + URI.encode(key).gsub(%r{/}, '_')
10
+ end
11
+
12
+ def read(key)
13
+ path = path_for_key(key)
14
+ if path.exist?
15
+ Marshal.load(path.read)
16
+ else
17
+ warn "[MISS #{key}]"
18
+ end
19
+ end
20
+
21
+ def write(key, data)
22
+ path = path_for_key(key)
23
+ path.open('w') { |io| io.write(Marshal.dump(data)) }
24
+ end
25
+
26
+ def fetch(key)
27
+ read(key) || yield.tap { |data| write(key, data) }
28
+ end
29
+
30
+ end
@@ -0,0 +1,12 @@
1
+ require 'nokogiri'
2
+ require 'faraday_middleware/response_middleware'
3
+
4
+ module FaradayMiddleware
5
+
6
+ class ParseHTML < ResponseMiddleware
7
+ define_parser do |body|
8
+ Nokogiri::HTML(body)
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,336 @@
1
+ class Itinerary
2
+
3
+ class Record < HashStruct
4
+
5
+ class Field
6
+
7
+ attr_accessor :key
8
+ attr_accessor :type
9
+ attr_accessor :name
10
+
11
+ def initialize(key, options={})
12
+ @key = key
13
+ @type = options[:type]
14
+ @name = options[:name]
15
+ end
16
+
17
+ end
18
+
19
+ # include Geocoder::Model::Record
20
+
21
+ @@fields = {}
22
+
23
+ def self.define_field(key, options={})
24
+ @@fields[key] = Field.new(key, options)
25
+ end
26
+
27
+ def self.field(key)
28
+ @@fields[key]
29
+ end
30
+
31
+ def self.fields
32
+ @@fields
33
+ end
34
+
35
+ def self.field_keys
36
+ @@fields.keys
37
+ end
38
+
39
+ attr_accessor :path
40
+
41
+ define_field :person, type: String, name: 'Person'
42
+ define_field :organization, type: String, name: 'Organization'
43
+ define_field :address, type: String, name: 'Address'
44
+ define_field :geocoding, type: Object, name: 'Geocoding'
45
+ define_field :email, type: String, name: 'Email'
46
+ define_field :phone, type: String, name: 'Phone'
47
+ define_field :uri, type: URI, name: 'URL'
48
+ define_field :description, type: String, name: 'Description'
49
+ define_field :ref, type: String, name: 'Reference'
50
+ define_field :group, type: String, name: 'Group'
51
+ define_field :visited, type: Date, name: 'Visited'
52
+ define_field :contacted, type: Date, name: 'Contacted'
53
+ define_field :declined, type: Date, name: 'Declined'
54
+ define_field :notes, type: String, name: 'Notes'
55
+
56
+ MaxFieldNameLength = @@fields.map { |k, f| f.name.length }.max
57
+
58
+ def self.load(path, options={})
59
+ io = path.open
60
+ rec = new(:path => path)
61
+ last_key = nil
62
+ while !io.eof? && (line = io.readline) do
63
+ case line.chomp
64
+ when ''
65
+ break
66
+ when /^\s*(\w+):\s*(.*)\s*$/
67
+ field_name, value = $1, $2.strip
68
+ next if value.empty?
69
+ field = @@fields[field_name.to_sym] || @@fields.values.find { |f| f.name == field_name } \
70
+ or raise "#{path}: Unknown field: #{field_name.inspect}"
71
+ if field.type == URI
72
+ #FIXME: sometimes this field is a space-separated list
73
+ # value = URI.parse(value)
74
+ elsif field.type == Object
75
+ value = eval(value)
76
+ end
77
+ rec[field.key] = value
78
+ last_key = field.key
79
+ when /^\s+(.+)\s*$/
80
+ raise "#{path}: Can't continue line without initial key or name" unless last_key
81
+ if rec[last_key]
82
+ rec[last_key] += ' ' + $1
83
+ else
84
+ rec[last_key] = $1
85
+ end
86
+ else
87
+ warn "#{path}: Bad line: #{line.inspect}"
88
+ next
89
+ end
90
+ end
91
+ notes = io.read
92
+ unless notes.empty?
93
+ if rec.notes
94
+ rec.notes += "\n\n" + notes
95
+ else
96
+ rec.notes = notes
97
+ end
98
+ end
99
+ io.close
100
+ rec
101
+ end
102
+
103
+ ###
104
+
105
+ def name
106
+ organization || person
107
+ end
108
+
109
+ def city
110
+ geocoding[:city] if geocoded?
111
+ end
112
+
113
+ def state
114
+ geocoding[:state] if geocoded?
115
+ end
116
+
117
+ def country
118
+ geocoding[:country] if geocoded?
119
+ end
120
+
121
+ def latitude
122
+ geocoding[:latitude] if geocoded?
123
+ end
124
+
125
+ def longitude
126
+ geocoding[:longitude] if geocoded?
127
+ end
128
+
129
+ def coordinates
130
+ [latitude, longitude] if geocoded?
131
+ end
132
+
133
+ def geocoded?
134
+ !geocoding.nil?
135
+ end
136
+
137
+ def geocode
138
+ results = Geocoder.search(address)
139
+ if (result = results.first)
140
+ self.geocoding = {
141
+ :city => result.city,
142
+ :state => result.state_code,
143
+ :country => result.country_code,
144
+ :latitude => result.coordinates[0],
145
+ :longitude => result.coordinates[1],
146
+ }
147
+ true
148
+ else
149
+ false
150
+ end
151
+ end
152
+
153
+ def near(coords, radius)
154
+ if geocoded? && (distance = Haversine.distance(*coords, latitude, longitude).to_miles) <= radius
155
+ distance
156
+ else
157
+ nil
158
+ end
159
+ end
160
+
161
+ def visited?
162
+ visited && visited < DateTime.now
163
+ end
164
+
165
+ def to_visit?
166
+ visited && visited >= DateTime.now
167
+ end
168
+
169
+ def string_to_key(str)
170
+ key = str.dup
171
+ key.downcase!
172
+ key.gsub!(/[^\w]+/, '-')
173
+ key.sub!(/^-+/, '')
174
+ key.sub!(/-+$/, '')
175
+ key
176
+ end
177
+
178
+ def make_path(root)
179
+ path = root.dup
180
+ raise "Not geocoded" unless geocoded?
181
+ path += string_to_key(country) if country
182
+ path += string_to_key(state) if state
183
+ raise "Can't make key from empty name" unless name
184
+ key = string_to_key(name)
185
+ i = 1
186
+ p = nil
187
+ loop do
188
+ p = key.dup
189
+ p += i.to_s if i > 1
190
+ break unless (path + p).exist?
191
+ i += 1
192
+ end
193
+ path += p
194
+ path
195
+ end
196
+
197
+ def clean!
198
+ @@fields.values.select { |f| f.type == String }.each do |field|
199
+ if (value = self[field.key])
200
+ value = value.gsub(/[[:space:]]+/, ' ').strip
201
+ end
202
+ if value.nil? || value.empty?
203
+ delete(field.key)
204
+ else
205
+ self[field.key] = value
206
+ end
207
+ end
208
+ end
209
+
210
+ def convert
211
+ false
212
+ end
213
+
214
+ def to_text(options={})
215
+ t = StringIO.new
216
+ field_keys = options[:field_keys] || @@fields.keys
217
+ field_keys.map { |k| @@fields[k] }.each do |field|
218
+ next if field.key == :notes
219
+ value = self[field.key] or next
220
+ if value =~ /\n/
221
+ value = "\n" + value.gsub(/^/, "\t")
222
+ end
223
+ t.puts "%-#{MaxFieldNameLength + 1}.#{MaxFieldNameLength + 1}s %s" % [field.name + ':', value]
224
+ end
225
+ t.puts
226
+ t.puts notes if notes && field_keys.include?(:notes)
227
+ t.rewind
228
+ t.read
229
+ end
230
+
231
+ def to_tab(options={})
232
+ field_keys = options[:field_keys] || self.field_keys
233
+ field_keys.map { |k| @@fields[k] }.map do |field|
234
+ value = self[field.key] || ''
235
+ if value =~ /\n/
236
+ value = value.gsub(/\n+/, ' | ')
237
+ end
238
+ value
239
+ end.join("\t")
240
+ end
241
+
242
+ def to_html(options={})
243
+ use_dl = options[:use_dl]
244
+ fields_html = fields_to_html(options[:field_keys] || self.field_keys)
245
+ html = Builder::XmlMarkup.new
246
+ html.h2(name)
247
+ if use_dl
248
+ html.dl do
249
+ fields_html.each do |display_name, h|
250
+ html.dt(display_name)
251
+ html.dd << h
252
+ end
253
+ end
254
+ else
255
+ fields_html.each do |display_name, h|
256
+ html.p do
257
+ html.b("#{display_name}: ")
258
+ html << h
259
+ end
260
+ end
261
+ end
262
+ html.target!
263
+ end
264
+
265
+ def fields_to_html(field_keys)
266
+ fields_html = {}
267
+ field_keys.each do |key|
268
+ field = @@fields[key] or raise "Unknown field: #{key.inspect}"
269
+ display_name = field.name
270
+ if (value = self[field.key])
271
+ case field.key
272
+ when :geocoding
273
+ display_name = 'Location'
274
+ value = [city, state].compact.join(', ')
275
+ when :contacted, :declined, :visited
276
+ value = value.strftime('%-d %b %Y')
277
+ when :description
278
+ html = Builder::XmlMarkup.new
279
+ html.i(value)
280
+ value = html
281
+ when :uri
282
+ display_name = 'Website'
283
+ #FIXME: anything beyond first URI is ignored -- make into list of links
284
+ value = URI.parse(value.split(/\s+/).first) if value.kind_of?(String)
285
+ end
286
+ html = Builder::XmlMarkup.new
287
+ case value
288
+ when URI
289
+ html.a(value.to_s, :href => value.to_s)
290
+ when Builder::XmlMarkup
291
+ html << value
292
+ else
293
+ html.text!(value)
294
+ end
295
+ fields_html[display_name] = html.target!
296
+ end
297
+ end
298
+ fields_html
299
+ end
300
+
301
+ def save!
302
+ raise "Record has no path" unless @path
303
+ @path.dirname.mkpath unless @path.dirname.exist?
304
+ @path.open('w') { |io| io.write(to_text) }
305
+ end
306
+
307
+ def edit(options={})
308
+ system(
309
+ 'subl',
310
+ options[:wait] ? '--wait' : (),
311
+ @path.to_s)
312
+ end
313
+
314
+ def open_in_editor_link
315
+ URI.parse("subl://open/?url=file://#{URI.escape(full_path)}")
316
+ end
317
+
318
+ def print_diff(other)
319
+ (self.keys + other.keys).sort.uniq.each do |key|
320
+ if self[key] != other[key]
321
+ puts "\t" + "#{key}:"
322
+ if self[key] && !other[key]
323
+ puts "\t\t" + "- #{self[key].inspect}"
324
+ elsif !self[key] && other[key]
325
+ puts "\t\t" + "+ #{other[key].inspect}"
326
+ elsif self[key] != other[key]
327
+ puts "\t\t" + "< #{self[key].inspect}"
328
+ puts "\t\t" + "> #{other[key].inspect}"
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ end
335
+
336
+ end
@@ -0,0 +1,23 @@
1
+ class Itinerary
2
+ class Tool
3
+
4
+ def self.inherited(subclass)
5
+ @@tools ||= []
6
+ @@tools << subclass
7
+ end
8
+
9
+ def self.tools
10
+ @@tools
11
+ end
12
+
13
+ def self.find_tool(cmd)
14
+ tool_class = @@tools.find { |t| t.name == cmd }
15
+ end
16
+
17
+ def initialize(itinerary, args)
18
+ @itinerary = itinerary
19
+ parse(args) if respond_to?(:parse)
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ class Itinerary
2
+ class ConvertTool < Tool
3
+
4
+ def self.name
5
+ 'convert'
6
+ end
7
+
8
+ def parse(args)
9
+ if args.first == '-n'
10
+ args.shift
11
+ @dry_run = true
12
+ end
13
+ end
14
+
15
+ def run
16
+ @itinerary.each do |rec1|
17
+ rec2 = rec1.dup
18
+ if rec2.convert && rec2 != rec1
19
+ puts "#{rec2.path} changed in cleaning"
20
+ rec2.print_diff(rec1)
21
+ rec2.save! unless @dry_run
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ class Itinerary
2
+ class CreateTool < Tool
3
+
4
+ def self.name
5
+ 'create'
6
+ end
7
+
8
+ def run
9
+ tmp = Pathname.new('/tmp/new-entry')
10
+ unless tmp.exist?
11
+ rec = Record.new(
12
+ :path => tmp,
13
+ :person => 'FIXME',
14
+ :organization => 'FIXME',
15
+ :address => 'FIXME',
16
+ :email => 'FIXME',
17
+ :phone => 'FIXME',
18
+ :uri => 'FIXME',
19
+ :description => 'FIXME',
20
+ :ref => 'FIXME',
21
+ )
22
+ rec.save!
23
+ rec.edit(:wait => true)
24
+ end
25
+ rec = Record.load(tmp)
26
+ rec.geocode or begin
27
+ warn "Failed to geocode #{rec.address.inspect} (entry left in #{tmp})"
28
+ exit(1)
29
+ end
30
+ rec.path = rec.make_path(@itinerary.entries_path)
31
+ rec.save!
32
+ warn "Saved to #{rec.path}"
33
+ tmp.unlink
34
+ @itinerary.import_entry(rec.path)
35
+ end
36
+
37
+ end
38
+ end