barton 0.0.2 → 0.0.3
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/Gemfile.lock +12 -5
- data/README.md +78 -45
- data/Rakefile +2 -3
- data/TODO.md +16 -0
- data/barton.gemspec +5 -2
- data/bin/barton +17 -2
- data/config.ru +2 -1
- data/data/nsw-state.yaml +2 -0
- data/data/qld-state.yaml +133 -5
- data/lib/barton.rb +14 -5
- data/lib/barton/app.rb +45 -19
- data/lib/barton/core.rb +158 -101
- data/lib/barton/docs.html +173 -0
- data/lib/barton/version.rb +1 -1
- data/specs/data/data-processed.yaml +19 -13
- data/specs/data/data-raw.yaml +10 -13
- data/specs/data/geo/data-processed.yaml +7 -7
- data/specs/data/geo/data-raw.yaml +4 -4
- data/specs/data/master/data-processed.yaml +19 -13
- data/specs/data/master/data-raw.yaml +7 -8
- data/specs/data/master/geo.yaml +7 -7
- data/specs/find_spec.rb +80 -0
- data/specs/route_spec.rb +21 -7
- data/specs/search_spec.rb +18 -39
- data/specs/setup_spec.rb +31 -30
- metadata +59 -3
data/lib/barton.rb
CHANGED
@@ -2,12 +2,21 @@ require "barton/core"
|
|
2
2
|
|
3
3
|
module Barton
|
4
4
|
class << self
|
5
|
-
def
|
6
|
-
@@
|
5
|
+
def environment
|
6
|
+
@@environment = ENV['BARTON'] || 'development'
|
7
7
|
end
|
8
|
-
|
9
|
-
def
|
10
|
-
|
8
|
+
|
9
|
+
def environment=(env)
|
10
|
+
ENV['BARTON'] = env
|
11
|
+
@@environment = env
|
12
|
+
end
|
13
|
+
|
14
|
+
def api_url
|
15
|
+
@@api_url ||= 'http://localhost:4567'
|
16
|
+
end
|
17
|
+
|
18
|
+
def api_url=(env)
|
19
|
+
@@api_url = env
|
11
20
|
end
|
12
21
|
end
|
13
22
|
end
|
data/lib/barton/app.rb
CHANGED
@@ -1,20 +1,22 @@
|
|
1
1
|
require 'sinatra'
|
2
2
|
require 'json'
|
3
|
+
require 'uri'
|
3
4
|
require 'barton'
|
4
5
|
require 'barton/core'
|
5
6
|
|
6
|
-
module Barton
|
7
|
+
module Barton
|
7
8
|
class App < Sinatra::Base
|
8
|
-
|
9
|
-
# routes
|
9
|
+
|
10
|
+
# routes
|
10
11
|
allowed_formats = ['', 'json']
|
11
12
|
|
12
|
-
|
13
|
-
|
13
|
+
# helpers
|
14
|
+
before do
|
15
|
+
Barton.api_url = "#{request.base_url }"
|
14
16
|
end
|
15
17
|
|
16
|
-
get '/
|
17
|
-
|
18
|
+
get '/' do
|
19
|
+
File.read( 'lib/barton/docs.html' )
|
18
20
|
end
|
19
21
|
|
20
22
|
# electorate resource
|
@@ -23,10 +25,12 @@ module Barton
|
|
23
25
|
geo, tags, address = params[:geo], params[:tags], params[:address]
|
24
26
|
tags = tags.split(',') unless tags.nil?
|
25
27
|
raise Sinatra::NotFound unless allowed_formats.include?( format )
|
26
|
-
geo = Barton::Data.address( address ) if address
|
27
28
|
if not id.empty?
|
28
29
|
results = Barton.electorates( {:id => id} )
|
29
30
|
prepare_response( results )
|
31
|
+
elsif address
|
32
|
+
results = Barton.electorates( {:tags => tags, :address => address} )
|
33
|
+
prepare_response( results )
|
30
34
|
elsif geo or tags
|
31
35
|
results = Barton.electorates( {:tags => tags, :geo => geo} )
|
32
36
|
prepare_response( results )
|
@@ -35,31 +39,53 @@ module Barton
|
|
35
39
|
end
|
36
40
|
end
|
37
41
|
|
42
|
+
# electorate resource
|
43
|
+
get %r{^/api/members/?(\w*)/?.?([A-z]*)} do
|
44
|
+
id, format = params[:captures]
|
45
|
+
geo, tags, address = params[:geo], params[:tags], params[:address]
|
46
|
+
tags = tags.split(',') unless tags.nil?
|
47
|
+
raise Sinatra::NotFound unless allowed_formats.include?( format )
|
48
|
+
if not id.empty?
|
49
|
+
results = Barton.members( {:id => id} )
|
50
|
+
prepare_response( results )
|
51
|
+
elsif address
|
52
|
+
results = Barton.members( {:tags => tags, :address => address} )
|
53
|
+
prepare_response( results )
|
54
|
+
elsif geo or tags
|
55
|
+
results = Barton.members( {:tags => tags, :geo => geo} )
|
56
|
+
prepare_response( results )
|
57
|
+
else
|
58
|
+
prepare_response
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
38
63
|
# base api
|
39
64
|
get %r{^/api/?.?([A-z]*)$} do
|
40
65
|
prepare_response
|
41
66
|
end
|
42
|
-
|
67
|
+
|
43
68
|
# views
|
44
|
-
|
69
|
+
|
45
70
|
# Prepares and array of results for the API response
|
46
71
|
def prepare_response( args=nil )
|
47
72
|
# add generic API meta data
|
48
|
-
app_url = 'http://barton.experimentsindemocracy.org'
|
49
73
|
response = {}
|
50
74
|
response[:name] = "Barton API"
|
51
|
-
response[:disclaimer] = "This data is crowded sourced and provided free of charge for informational purposes only. No guarantees
|
75
|
+
response[:disclaimer] = "This data is crowded sourced and provided free of charge for informational purposes only. No guarantees regarding data quality are made whatsoever apart from it's rather considerable coolness."
|
52
76
|
response[:license] = "MIT License http://www.opensource.org/licenses/mit-license.php"
|
53
77
|
unless args.nil?
|
54
|
-
response[:result_count] = args.length
|
78
|
+
response[:result_count] = args.length
|
55
79
|
response[:results] = args
|
80
|
+
status 404 if args.length == 0
|
56
81
|
end
|
57
|
-
response[:resources] = { :home =>
|
58
|
-
response[:examples] = {
|
59
|
-
:
|
60
|
-
:geo => "#{
|
61
|
-
:tags => "#{
|
62
|
-
:mixed => "#{
|
82
|
+
response[:resources] = { :home => Barton.api_url, :api => "#{Barton.api_url}/api", :electorates => "#{Barton.api_url}/api/electorates", :members => "#{Barton.api_url}/api/members" }
|
83
|
+
response[:examples] = {
|
84
|
+
:id => "#{Barton.api_url}/api/members/0db2ec",
|
85
|
+
:geo => "#{Barton.api_url}/api/electorates?geo=151.2054563,-33.8438383",
|
86
|
+
:tags => "#{Barton.api_url}/api/members?tags=sydney,local",
|
87
|
+
:mixed => "#{Barton.api_url}/api/electorates?geo=151.2054563,-33.8438383&tags=federal",
|
88
|
+
}
|
63
89
|
# format
|
64
90
|
content_type 'application/json;charset=utf-8'
|
65
91
|
JSON.pretty_generate( response )
|
data/lib/barton/core.rb
CHANGED
@@ -5,7 +5,7 @@ require 'json'
|
|
5
5
|
require 'barton/app'
|
6
6
|
|
7
7
|
module Barton
|
8
|
-
|
8
|
+
|
9
9
|
# Returns an array of electorates matching the search criteria
|
10
10
|
# Accepts a hash of criteria
|
11
11
|
# :id
|
@@ -13,50 +13,75 @@ module Barton
|
|
13
13
|
# :geo
|
14
14
|
# :address
|
15
15
|
# Returns an array of hashes
|
16
|
-
def
|
16
|
+
def self.electorates( query = {} )
|
17
17
|
Find.electorates( query )
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
# Returns an array of member matching the search criteria
|
21
21
|
# Accepts a hash of criteria
|
22
|
-
# Returns an array of hashes
|
23
|
-
def members
|
24
|
-
|
22
|
+
# Returns an array of hashes
|
23
|
+
def self.members( query={} )
|
24
|
+
Find.members( query )
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
# Loads electoral data from the yaml files into elasticsearch
|
28
28
|
def self.setup
|
29
|
-
Data.config
|
30
29
|
Data.purge_es
|
31
30
|
Dir['data/*.yaml'].each do |f|
|
32
31
|
Setup.load_file( f )
|
33
32
|
end
|
34
|
-
Barton.data_loaded = true
|
35
33
|
end
|
36
|
-
|
37
|
-
# Configuration options for the Gem
|
34
|
+
|
38
35
|
def self.config
|
39
|
-
|
40
|
-
#Data.config
|
36
|
+
Data.config
|
41
37
|
end
|
42
|
-
|
38
|
+
|
43
39
|
module Find
|
44
|
-
|
40
|
+
def self.electorates( query )
|
41
|
+
docs = Find.documents( query, 'electorate' )
|
42
|
+
# remove unwanted fields
|
43
|
+
docs.each do |e|
|
44
|
+
e.keys.each { |k| e.delete( k ) if k.match( /_|geobox|boundaries|highlight|sort/ ) }
|
45
|
+
e[:url] = "#{Barton.api_url}/api/electorates/#{e[:id]}"
|
46
|
+
members = Array.new
|
47
|
+
if e.has_key?( :members )
|
48
|
+
e[:members].each do |m|
|
49
|
+
m = m.to_hash
|
50
|
+
m[:url] = "#{Barton.api_url}/api/members/#{m[:id]}"
|
51
|
+
m.delete( :id )
|
52
|
+
members.push( m )
|
53
|
+
end
|
54
|
+
end
|
55
|
+
e[:members] = members if members.length > 0
|
56
|
+
end
|
57
|
+
docs
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.members( query )
|
61
|
+
results = Array.new
|
62
|
+
docs = Find.documents( query, 'member' )
|
63
|
+
docs.each do |m|
|
64
|
+
m.keys.each { |k| m.delete( k ) if k.match( /_|highlight|sort/ ) }
|
65
|
+
m[:url] = "#{Barton.api_url}/api/members/#{m[:id]}"
|
66
|
+
e = m[:electorate].to_hash
|
67
|
+
e[:url] = "#{Barton.api_url}/api/electorates/#{e[:id]}"
|
68
|
+
e.delete( :id )
|
69
|
+
m[:electorate] = e
|
70
|
+
end
|
71
|
+
docs
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.documents( query={}, type=nil )
|
45
75
|
results = Array.new
|
46
76
|
terms = ["id:#{query[:id]}"] if query.has_key?( :id )
|
47
77
|
terms = query[:tags] if query.has_key?( :tags )
|
48
78
|
geo = query[:geo] if query.has_key?( :geo )
|
49
79
|
geo = self.address( query[:address] ) if query.has_key?( :address ) and not query.has_key?( :geo )
|
50
|
-
s = Data.search( terms, geo )
|
51
|
-
s.results.each
|
52
|
-
e = e.to_hash
|
53
|
-
e.keys.each { |k| e.delete( k ) if k.match( /_|geobox|boundaries|highlight|sort/ ) }
|
54
|
-
e[:url] = "#{@api_url}/electorates/#{e[:id]}"
|
55
|
-
results.push( e )
|
56
|
-
end
|
80
|
+
s = Data.search( terms, geo, type )
|
81
|
+
s.results.each { |e| results.push( e.to_hash ) }
|
57
82
|
results
|
58
|
-
|
59
|
-
|
83
|
+
end
|
84
|
+
|
60
85
|
# Geocode lookup to google
|
61
86
|
def self.address( address )
|
62
87
|
# http://maps.googleapis.com/maps/api/geocode/json?address=address&sensor=false®ion=au
|
@@ -73,7 +98,7 @@ module Barton
|
|
73
98
|
puts e
|
74
99
|
end
|
75
100
|
end
|
76
|
-
|
101
|
+
|
77
102
|
# Ray casting algorithm to find if point is in polygon
|
78
103
|
def self.point_in_poly?( geo, boundaries )
|
79
104
|
# get pairs of points of boundaries
|
@@ -102,19 +127,93 @@ module Barton
|
|
102
127
|
ax, ay, bx, by = ax.to_f, ay.to_f, bx.to_f, by.to_f
|
103
128
|
return 0 if ay < long and by < long
|
104
129
|
return ( ( ax < lat and lat < bx ) or ( bx < lat and lat < ax ) ) ? 1 : 0
|
105
|
-
end
|
130
|
+
end
|
106
131
|
end
|
107
|
-
|
132
|
+
|
133
|
+
module Data
|
134
|
+
@index_name = 'electorates'
|
135
|
+
|
136
|
+
# Set elasticsearch config
|
137
|
+
def self.config
|
138
|
+
case Barton.environment
|
139
|
+
when 'test'
|
140
|
+
@index_name = 'test_electorates'
|
141
|
+
when ENV['FOUNDELASTICSEARCH_URL']
|
142
|
+
Tire::Configuration.url ENV['FOUNDELASTICSEARCH_URL']
|
143
|
+
@index_name = 'electorates'
|
144
|
+
else
|
145
|
+
@index_name = 'electorates'
|
146
|
+
end
|
147
|
+
#if ENV['BONSAI_INDEX_URL']
|
148
|
+
# Tire.configure do
|
149
|
+
# url "http://index.bonsai.io"
|
150
|
+
# end
|
151
|
+
# @index_name = ENV['BONSAI_INDEX_URL'][/[^\/]+$/]
|
152
|
+
#end
|
153
|
+
return Barton.environment
|
154
|
+
end
|
155
|
+
|
156
|
+
# Purge electorate data from elasticsearch
|
157
|
+
def self.purge_es
|
158
|
+
self.query_es( 'purge' )
|
159
|
+
end
|
160
|
+
|
161
|
+
# Load electorate data to elasticsearch
|
162
|
+
def self.update_es( data )
|
163
|
+
self.query_es( 'update', data )
|
164
|
+
end
|
165
|
+
|
166
|
+
# Query elasticsearch
|
167
|
+
def self.query_es( action, data=nil )
|
168
|
+
self.config
|
169
|
+
begin
|
170
|
+
Tire.index "#{@index_name}" do
|
171
|
+
delete if action == 'purge'
|
172
|
+
create if action == 'purge'
|
173
|
+
import data if action == 'update' and not data.nil?
|
174
|
+
end
|
175
|
+
rescue Exception => e
|
176
|
+
puts "Elasticsearch error: #{e}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.search( terms, geo, type=nil )
|
181
|
+
self.config
|
182
|
+
s = Tire.search "#{@index_name}" do
|
183
|
+
query do
|
184
|
+
boolean do
|
185
|
+
if geo
|
186
|
+
long, lat = geo.split( ',' )
|
187
|
+
must { range :north, { :gte => lat } }
|
188
|
+
must { range :south, { :lte => lat } }
|
189
|
+
must { range :east, { :gte => long } }
|
190
|
+
must { range :west, { :lte => long } }
|
191
|
+
elsif terms
|
192
|
+
terms.each { |t| must { string t } }
|
193
|
+
end
|
194
|
+
must { string "type:#{type}" } if type
|
195
|
+
end
|
196
|
+
end
|
197
|
+
# apply filters only with geo search
|
198
|
+
filter :terms, :_all => terms if geo and terms
|
199
|
+
size 100
|
200
|
+
end
|
201
|
+
s
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
108
205
|
module Setup
|
109
206
|
# Parses a electorate yaml file, merges it with a geo yaml file,
|
110
207
|
# loads them to the datastore and resaves the yaml files
|
111
208
|
def self.parse_yaml( filename )
|
112
|
-
data =
|
209
|
+
data = Array.new
|
210
|
+
electorates = YAML::load_file( filename ) if File.exist?( filename )
|
113
211
|
geofile = "#{File.dirname( filename )}/geo/#{File.basename( filename )}"
|
114
212
|
geo = YAML::load_file( geofile ) if File.exist?( geofile )
|
115
|
-
if
|
116
|
-
|
117
|
-
|
213
|
+
if electorates.respond_to?( 'each' )
|
214
|
+
electorates.each do |e|
|
215
|
+
# id is the first 6 digits of a hash of object name with file name EG BulimbaQLD-State.yaml
|
216
|
+
e['id'] = Digest::SHA1.hexdigest( [e['name'], filename].join )[0..5].force_encoding('utf-8') unless e['id'] or not e['name']
|
118
217
|
# make geobox and update geo.yaml if boundaries present
|
119
218
|
if e.has_key?( 'boundaries')
|
120
219
|
e['geobox'] = self.create_geobox( e['boundaries'] )
|
@@ -123,14 +222,37 @@ module Barton
|
|
123
222
|
e['boundaries'] = geo[e['id']]['boundaries']
|
124
223
|
e['geobox'] = geo[e['id']]['geobox']
|
125
224
|
end
|
225
|
+
|
226
|
+
ei = e.clone # this will be the electorate that is indexed
|
227
|
+
ei['type'] = 'electorate'
|
228
|
+
|
229
|
+
if e.has_key?( 'members' )
|
230
|
+
# create seperate types for member details
|
231
|
+
members = Array.new
|
232
|
+
e['members'].each do |m|
|
233
|
+
m['role'] = "Member for #{e['name']}" unless m['role']
|
234
|
+
m['id'] = Digest::SHA1.hexdigest( [m['role'], filename].join )[0..5].force_encoding('utf-8') unless m['id'] or not m['role']
|
235
|
+
# only add this for indexing
|
236
|
+
mi = m.clone
|
237
|
+
mi['type'] = 'member'
|
238
|
+
mi['tags'] = [] unless m.has_key?( 'tags' )
|
239
|
+
mi['tags'] |= e['tags'] if e.has_key?( 'tags' )
|
240
|
+
mi['electorate'] = {'name' => e['name'], 'id' => e['id']}
|
241
|
+
data.push( mi )
|
242
|
+
members.push( {'role' => m['role'], 'name' => m['name'], 'id' => m['id']})
|
243
|
+
end
|
244
|
+
# only index limited member data
|
245
|
+
ei['members'] = members
|
246
|
+
end
|
247
|
+
data.push( ei )
|
126
248
|
end
|
127
249
|
# save parsed yaml
|
128
250
|
data_yaml = Array.new
|
129
251
|
geo_yaml = Hash.new
|
130
|
-
|
252
|
+
electorates.each do |e|
|
131
253
|
# remove geo from data
|
132
254
|
geo_yaml[e['id']] = Hash['boundaries' => e['boundaries'], 'geobox' => e['geobox']] if e.has_key?( 'boundaries' )
|
133
|
-
data_yaml.push( e.select { |k,v| not ['boundaries', 'geobox'].include? k } )
|
255
|
+
data_yaml.push( e.select { |k,v| not ['boundaries', 'geobox', 'type'].include? k } )
|
134
256
|
end
|
135
257
|
File.open( filename, 'w+' ) { |f| f.puts( YAML.dump( data_yaml ) ) }
|
136
258
|
File.open( geofile, 'w+' ) { |f| f.puts( YAML.dump( geo_yaml ) ) }
|
@@ -139,7 +261,7 @@ module Barton
|
|
139
261
|
nil
|
140
262
|
end
|
141
263
|
end
|
142
|
-
|
264
|
+
|
143
265
|
# Creates a minimum bounded box encapsulating the boundary polygons
|
144
266
|
def self.create_geobox( boundaries )
|
145
267
|
return nil unless boundaries.instance_of? Array
|
@@ -155,79 +277,14 @@ module Barton
|
|
155
277
|
end
|
156
278
|
box
|
157
279
|
end
|
158
|
-
|
280
|
+
|
159
281
|
# Load data from yaml source file
|
160
282
|
def self.load_file( filename )
|
161
|
-
puts "Loading data from #{filename}....."
|
283
|
+
puts "Loading data from #{filename} to #{Barton.environment}....."
|
162
284
|
electorates = self.parse_yaml( filename )
|
163
285
|
puts "Failed to load #{filename}" if electorates.nil?
|
164
286
|
connected = Data.update_es( electorates )
|
165
287
|
abort "Connection to Elasticsearch failed" if connected.nil?
|
166
288
|
end
|
167
289
|
end
|
168
|
-
|
169
|
-
|
170
|
-
module Data
|
171
|
-
|
172
|
-
@index_name = ENV['RAKE_ENV']
|
173
|
-
|
174
|
-
# Set elasticsearch config
|
175
|
-
def self.config
|
176
|
-
@index_name = ENV['RAKE_ENV']
|
177
|
-
if ENV['BONSAI_INDEX_URL']
|
178
|
-
Tire.configure do
|
179
|
-
url "http://index.bonsai.io"
|
180
|
-
end
|
181
|
-
@index_name = ENV['BONSAI_INDEX_URL'][/[^\/]+$/]
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
# Purge electorate data from elasticsearch
|
186
|
-
def self.purge_es
|
187
|
-
self.query_es( 'purge' )
|
188
|
-
end
|
189
|
-
|
190
|
-
# Load electorate data to elasticsearch
|
191
|
-
def self.update_es( data )
|
192
|
-
self.query_es( 'update', data )
|
193
|
-
end
|
194
|
-
|
195
|
-
# Query elasticsearch
|
196
|
-
def self.query_es( action, data=nil )
|
197
|
-
begin
|
198
|
-
Tire.index "#{@index_name}-electorates" do
|
199
|
-
delete if action == 'purge'
|
200
|
-
create if action == 'purge'
|
201
|
-
import data if action == 'update' and not data.nil?
|
202
|
-
end
|
203
|
-
rescue => e
|
204
|
-
puts e
|
205
|
-
nil
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
def self.search( terms, geo )
|
210
|
-
s = Tire.search "#{@index_name}-electorates" do
|
211
|
-
query do
|
212
|
-
boolean do
|
213
|
-
if geo
|
214
|
-
long, lat = geo.split( ',' )
|
215
|
-
must { range :north, { :gte => lat } }
|
216
|
-
must { range :south, { :lte => lat } }
|
217
|
-
must { range :east, { :gte => long } }
|
218
|
-
must { range :west, { :lte => long } }
|
219
|
-
elsif terms
|
220
|
-
terms.each { |t| must { string t } }
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
224
|
-
# apply filters only with geo search
|
225
|
-
if geo and terms
|
226
|
-
filter :terms, :_all => terms
|
227
|
-
end
|
228
|
-
size 100
|
229
|
-
end
|
230
|
-
s
|
231
|
-
end
|
232
|
-
end
|
233
290
|
end
|