campfire_logic 1.1.7
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/Capfile +4 -0
- data/Gemfile +29 -0
- data/Gemfile.local +27 -0
- data/README.rdoc +17 -0
- data/Rakefile +43 -0
- data/VERSION +1 -0
- data/app/controllers/application_controller.rb +5 -0
- data/app/controllers/directory_controller.rb +65 -0
- data/app/controllers/locations_controller.rb +65 -0
- data/app/controllers/services_controller.rb +46 -0
- data/app/helpers/application_helper.rb +27 -0
- data/app/models/google_maps_geocoder.rb +108 -0
- data/app/models/locale.rb +204 -0
- data/app/models/location.rb +216 -0
- data/app/models/location_import.rb +48 -0
- data/app/models/service.rb +27 -0
- data/app/models/zip_code.rb +48 -0
- data/app/models/zip_code_import.rb +19 -0
- data/app/views/directory/_search_form.html.erb +4 -0
- data/app/views/directory/_show_children.html.erb +29 -0
- data/app/views/directory/_show_location.html.erb +14 -0
- data/app/views/directory/search.html.erb +31 -0
- data/app/views/directory/show.html.erb +32 -0
- data/app/views/layouts/application.html.erb +49 -0
- data/app/views/locations/_form.html.erb +69 -0
- data/app/views/locations/edit.html.erb +3 -0
- data/app/views/locations/export.erb +4 -0
- data/app/views/locations/import.html.erb +13 -0
- data/app/views/locations/index.html.erb +37 -0
- data/app/views/locations/new.html.erb +3 -0
- data/app/views/locations/show.html.erb +70 -0
- data/app/views/services/_form.html.erb +29 -0
- data/app/views/services/edit.html.erb +3 -0
- data/app/views/services/index.html.erb +25 -0
- data/app/views/services/new.html.erb +3 -0
- data/app/views/services/show.html.erb +15 -0
- data/app/views/shared/_location.html.erb +18 -0
- data/app/views/shared/_map.html.erb +2 -0
- data/app/views/shared/_nav_tabs.html.erb +5 -0
- data/autotest/discover.rb +1 -0
- data/campfire_logic.gemspec +208 -0
- data/config/application.rb +44 -0
- data/config/boot.rb +13 -0
- data/config/cucumber.yml +10 -0
- data/config/deploy.rb +40 -0
- data/config/environment.rb +6 -0
- data/config/environments/development.rb +28 -0
- data/config/environments/production.rb +49 -0
- data/config/environments/test.rb +35 -0
- data/config/initializers/campfire_logic.rb +3 -0
- data/config/initializers/metric_fu.rb +9 -0
- data/config/initializers/secret_token.rb +7 -0
- data/config/initializers/session_store.rb +8 -0
- data/config/locales/en.yml +5 -0
- data/config/mongoid.yml +25 -0
- data/config/routes.rb +96 -0
- data/config.ru +4 -0
- data/db/seeds.rb +5 -0
- data/db/zip_codes.txt +42742 -0
- data/doc/google_maps_response.rb +56 -0
- data/features/admin_manages_locations.feature +10 -0
- data/features/customer_browses_directory.feature +25 -0
- data/features/customer_searches_directory.feature +29 -0
- data/features/step_definitions/directory_steps.rb +22 -0
- data/features/step_definitions/location_steps.rb +10 -0
- data/features/step_definitions/web_steps.rb +211 -0
- data/features/support/env.rb +31 -0
- data/features/support/paths.rb +33 -0
- data/features/support/selectors.rb +39 -0
- data/init.rb +1 -0
- data/lib/campfire_logic/engine.rb +7 -0
- data/lib/campfire_logic/railtie.rb +10 -0
- data/lib/campfire_logic.rb +31 -0
- data/lib/tasks/campfire_logic.rake +7 -0
- data/lib/tasks/cucumber.rake +71 -0
- data/public/404.html +26 -0
- data/public/422.html +26 -0
- data/public/500.html +26 -0
- data/public/favicon.ico +0 -0
- data/public/images/icons/collapsed.gif +0 -0
- data/public/images/icons/delete.png +0 -0
- data/public/images/icons/drag.png +0 -0
- data/public/images/icons/edit.png +0 -0
- data/public/images/icons/expanded.gif +0 -0
- data/public/images/icons/help_icon.png +0 -0
- data/public/images/icons/link_icon.png +0 -0
- data/public/images/icons/move.png +0 -0
- data/public/images/icons/move_white.png +0 -0
- data/public/images/icons/note.png +0 -0
- data/public/images/icons/note_white.png +0 -0
- data/public/images/icons/notification_icon_sprite.png +0 -0
- data/public/images/icons/spinner.gif +0 -0
- data/public/images/icons/view.png +0 -0
- data/public/images/icons/warning.png +0 -0
- data/public/images/icons/warning_2.png +0 -0
- data/public/images/icons/warning_box.png +0 -0
- data/public/images/icons/warning_icon.png +0 -0
- data/public/images/icons/warning_white.png +0 -0
- data/public/images/layout/arrow_asc.png +0 -0
- data/public/images/layout/arrow_desc.png +0 -0
- data/public/images/layout/black_bar.png +0 -0
- data/public/images/layout/branding.png +0 -0
- data/public/images/layout/button_bg.png +0 -0
- data/public/images/layout/content_left_bg.png +0 -0
- data/public/images/layout/content_right_bg.png +0 -0
- data/public/images/layout/footer_bg.png +0 -0
- data/public/images/layout/h2_bg.png +0 -0
- data/public/images/layout/h2_bg_for_table.png +0 -0
- data/public/images/layout/header_bg_grey.png +0 -0
- data/public/images/layout/header_bg_purple.png +0 -0
- data/public/images/layout/legend_bg.png +0 -0
- data/public/images/layout/text_field_bg.jpg +0 -0
- data/public/images/layout/text_field_error_bg.png +0 -0
- data/public/images/layout/th_bg.png +0 -0
- data/public/images/layout/th_bg_selected.png +0 -0
- data/public/images/rails.png +0 -0
- data/public/index.html +9 -0
- data/public/javascripts/application.js +2 -0
- data/public/javascripts/controls.js +965 -0
- data/public/javascripts/dragdrop.js +974 -0
- data/public/javascripts/effects.js +1123 -0
- data/public/javascripts/prototype.js +6001 -0
- data/public/javascripts/rails.js +175 -0
- data/public/robots.txt +6 -0
- data/public/sample-locations.xls +1 -0
- data/public/stylesheets/.gitkeep +0 -0
- data/public/stylesheets/application.css +682 -0
- data/public/stylesheets/core.css +1147 -0
- data/public/stylesheets/core_ie.css +52 -0
- data/public/stylesheets/csshover3.htc +14 -0
- data/script/cucumber +10 -0
- data/script/rails +6 -0
- data/spec/controllers/directory_controller_spec.rb +11 -0
- data/spec/controllers/services_controller_spec.rb +62 -0
- data/spec/models/google_maps_geocoder_spec.rb +62 -0
- data/spec/models/locale_spec.rb +64 -0
- data/spec/models/location_import_spec.rb +41 -0
- data/spec/models/location_spec.rb +195 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/test-locations.xls +1 -0
- metadata +361 -0
data/Capfile
ADDED
data/Gemfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
source 'http://rubygems.org'
|
|
2
|
+
source 'http://jose.seologic.com:8808/'
|
|
3
|
+
|
|
4
|
+
gem 'bson_ext'
|
|
5
|
+
gem 'fastercsv'
|
|
6
|
+
gem 'mongoid-tree'
|
|
7
|
+
gem 'rake'
|
|
8
|
+
gem 'rails', '>= 3.0'
|
|
9
|
+
gem 'scaffold_logic'
|
|
10
|
+
gem 'stateflow'
|
|
11
|
+
gem 'stringex'
|
|
12
|
+
gem 'SystemTimer'
|
|
13
|
+
|
|
14
|
+
group :development do
|
|
15
|
+
gem 'jeweler'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
group :test do
|
|
19
|
+
gem 'be_valid_asset'
|
|
20
|
+
gem 'capybara'
|
|
21
|
+
gem 'cucumber-rails'
|
|
22
|
+
gem 'database_cleaner'
|
|
23
|
+
gem 'launchy'
|
|
24
|
+
gem 'metric_fu'
|
|
25
|
+
gem 'mocha'
|
|
26
|
+
gem 'nokogiri'
|
|
27
|
+
gem 'rcov'
|
|
28
|
+
gem 'rspec-rails'
|
|
29
|
+
end
|
data/Gemfile.local
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
source 'http://rubygems.org'
|
|
2
|
+
source 'http://jose.seologic.com:8808/'
|
|
3
|
+
|
|
4
|
+
gem 'bson_ext'
|
|
5
|
+
gem 'fastercsv'
|
|
6
|
+
gem 'mongo', '>= 1.0.7'
|
|
7
|
+
gem 'mongoid', '>= 2.0.0.beta.20'
|
|
8
|
+
gem 'mongoid-tree'
|
|
9
|
+
gem 'rails'
|
|
10
|
+
gem 'scaffold_logic', :path => '~/Documents/projects/scaffold_logic'
|
|
11
|
+
gem 'stateflow'
|
|
12
|
+
gem 'stringex'
|
|
13
|
+
|
|
14
|
+
group :development do
|
|
15
|
+
gem 'jeweler'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
group :test do
|
|
19
|
+
gem 'capybara'
|
|
20
|
+
gem 'cucumber-rails'
|
|
21
|
+
gem 'database_cleaner'
|
|
22
|
+
gem 'launchy'
|
|
23
|
+
gem 'mocha'
|
|
24
|
+
gem 'nokogiri'
|
|
25
|
+
gem 'rcov'
|
|
26
|
+
gem 'rspec-rails'
|
|
27
|
+
end
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
= CampfireLogic
|
|
2
|
+
|
|
3
|
+
The engine that powers CAMP. Provides location browsing, search, and management.
|
|
4
|
+
|
|
5
|
+
== Note on Patches/Pull Requests
|
|
6
|
+
|
|
7
|
+
* Fork the project.
|
|
8
|
+
* Make your feature addition or bug fix.
|
|
9
|
+
* Add tests for it. This is important so I don't break it in a
|
|
10
|
+
future version unintentionally.
|
|
11
|
+
* Commit, do not mess with rakefile, version, or history.
|
|
12
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
|
13
|
+
* Send me a pull request. Bonus points for topic branches.
|
|
14
|
+
|
|
15
|
+
== Copyright
|
|
16
|
+
|
|
17
|
+
Copyright (c) 2010 Roderick Monje. See LICENSE for details.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'metric_fu'
|
|
2
|
+
require 'rake'
|
|
3
|
+
require 'rspec/core/rake_task'
|
|
4
|
+
require 'rubygems'
|
|
5
|
+
require File.expand_path('../config/application', __FILE__)
|
|
6
|
+
|
|
7
|
+
CampfireLogic::Application.load_tasks
|
|
8
|
+
Rspec::Core::RakeTask.new
|
|
9
|
+
|
|
10
|
+
begin
|
|
11
|
+
require 'jeweler'
|
|
12
|
+
Jeweler::Tasks.new do |gem|
|
|
13
|
+
gem.name = 'campfire_logic'
|
|
14
|
+
gem.summary = %Q{Rails engine that adds a location directory to your web app}
|
|
15
|
+
gem.description = %Q{Users can browse locations by country, city, and state and search locations by string or zip code. Administrators can manage locations and the services they offer.}
|
|
16
|
+
gem.email = 'rod@seologic.com'
|
|
17
|
+
gem.homepage = 'http://github.com/ivanoblomov/campfire_logic'
|
|
18
|
+
gem.authors = ['Roderick Monje']
|
|
19
|
+
gem.add_development_dependency 'rspec', '>= 1.2.9'
|
|
20
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
|
21
|
+
end
|
|
22
|
+
Jeweler::GemcutterTasks.new
|
|
23
|
+
rescue LoadError
|
|
24
|
+
puts 'Jeweler (or a dependency) not available. Install it with: gem install jeweler'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
require 'rake/rdoctask'
|
|
28
|
+
Rake::RDocTask.new do |rdoc|
|
|
29
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ''
|
|
30
|
+
|
|
31
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
32
|
+
rdoc.title = "CampfireLogic #{version}"
|
|
33
|
+
rdoc.rdoc_files.include('README*')
|
|
34
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
namespace :spec do
|
|
38
|
+
desc 'Run specs with RCov'
|
|
39
|
+
RSpec::Core::RakeTask.new('rcov') do |t|
|
|
40
|
+
t.rcov = true
|
|
41
|
+
t.rcov_opts = IO.readlines('spec/rcov.opts').map{ |l| l.chomp.split } * ' '
|
|
42
|
+
end
|
|
43
|
+
end
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.1.7
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class DirectoryController < ApplicationController
|
|
2
|
+
def search
|
|
3
|
+
if searching_by_zip?
|
|
4
|
+
search_by_zip
|
|
5
|
+
else
|
|
6
|
+
search_by_string
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@locale = find_location_locale || find_city || find_state || find_country || Locale.init_root
|
|
12
|
+
@location = find_location_locale.location if find_location_locale
|
|
13
|
+
Rails.logger.info "Found a #{@locale.kind} locale: #{@locale.name}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def searching_by_zip?
|
|
17
|
+
params[:criteria] =~ /[0-9]{5}/
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def find_city
|
|
23
|
+
@city ||= find_state.children.to_a.detect{ |c| c.slug == params[:city] } if params[:city]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def find_country
|
|
27
|
+
@country ||= Locale.root.children.to_a.detect{ |c| c.slug == params[:country] } if params[:country]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def find_location_locale
|
|
31
|
+
@location_locale ||= find_city.children.to_a.detect{ |c| c.slug == params[:location] } if params[:location]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def find_state
|
|
35
|
+
@state ||= Locale.first(:conditions => {:slug => params[:state]}) if params[:state]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def search_by_string
|
|
39
|
+
_query = params[:criteria]
|
|
40
|
+
|
|
41
|
+
# find matching locations
|
|
42
|
+
@locations = Location.all( :conditions => search_conditions(_query) ).to_a
|
|
43
|
+
|
|
44
|
+
if @locations.blank?
|
|
45
|
+
# find a matching locale
|
|
46
|
+
@locale = Locale.first( :conditions => search_conditions(_query) )
|
|
47
|
+
|
|
48
|
+
# find locations in locale
|
|
49
|
+
@locations = @locale.locations if @locale
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def search_by_zip
|
|
54
|
+
# find matching zip or create one
|
|
55
|
+
_zip = ZipCode.first(:conditions => {:zip => params[:criteria]}) || ZipCode.create(:zip => params[:criteria])
|
|
56
|
+
|
|
57
|
+
@locations = Location.near(:lat_lng => _zip.lat_lng).limit(10)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns a hash specifying the abbreviation for a two-letter query or the name otherwise. Blank queries return null.
|
|
61
|
+
def search_conditions(query)
|
|
62
|
+
return if query.blank?
|
|
63
|
+
{query.size == 2 ? :abbreviation : :name => /#{query}/i}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class LocationsController < ApplicationController
|
|
2
|
+
before_filter :authenticate_user!, :only => [:destroy, :edit, :export, :import, :index, :new, :update, :upload] if Object.const_defined?('Devise')
|
|
3
|
+
before_filter :scope_location, :only => [:show, :edit, :update, :destroy]
|
|
4
|
+
|
|
5
|
+
# Custom =========================================================================================
|
|
6
|
+
def export
|
|
7
|
+
headers["Content-Type"] = "application/x-excel"
|
|
8
|
+
headers["Content-disposition"] = %{attachment; filename="locations.xls"}
|
|
9
|
+
@locations = Location.all
|
|
10
|
+
self.response_body = render :layout => false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def import
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def upload
|
|
17
|
+
if params[:upload].blank?
|
|
18
|
+
flash[:error] = 'You must choose an import file.'
|
|
19
|
+
render :action => 'import'
|
|
20
|
+
else
|
|
21
|
+
unless (errors = LocationImport.save(params[:upload])).blank?
|
|
22
|
+
flash[:error] = (errors * '<br />').html_safe
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
flash[:notice] = 'File has been uploaded successfully.' unless flash[:error]
|
|
26
|
+
redirect_to locations_path
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# CRUD ===========================================================================================
|
|
31
|
+
def destroy
|
|
32
|
+
@location.destroy
|
|
33
|
+
flash[:notice] = 'Deleted location.'
|
|
34
|
+
redirect_to locations_path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def edit
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def index
|
|
41
|
+
@locations = Location.all.order_by([params[:by] || :name, params[:dir] || :asc])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def new
|
|
45
|
+
@location = Location.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def show
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def update
|
|
52
|
+
if @location.update_attributes(params[:location])
|
|
53
|
+
flash[:notice] = 'Updated location.'
|
|
54
|
+
redirect_to locations_path
|
|
55
|
+
else
|
|
56
|
+
render :action => 'edit'
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def scope_location
|
|
63
|
+
@location = Location.find(params[:id])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
class ServicesController < ApplicationController
|
|
2
|
+
before_filter :authenticate_user! if Object.const_defined?('Devise')
|
|
3
|
+
|
|
4
|
+
def index
|
|
5
|
+
@services = Service.all
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
@service = Service.find(params[:id])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def new
|
|
13
|
+
@service = Service.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create
|
|
17
|
+
@service = Service.new(params[:service])
|
|
18
|
+
if @service.save
|
|
19
|
+
flash[:notice] = 'Successfully created service.'
|
|
20
|
+
redirect_to @service
|
|
21
|
+
else
|
|
22
|
+
render :action => 'new'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def edit
|
|
27
|
+
@service = Service.find(params[:id])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update
|
|
31
|
+
@service = Service.find(params[:id])
|
|
32
|
+
if @service.update_attributes(params[:service])
|
|
33
|
+
flash[:notice] = 'Successfully updated service.'
|
|
34
|
+
redirect_to @service
|
|
35
|
+
else
|
|
36
|
+
render :action => 'edit'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def destroy
|
|
41
|
+
@service = Service.find(params[:id])
|
|
42
|
+
@service.destroy
|
|
43
|
+
flash[:notice] = 'Successfully destroyed service.'
|
|
44
|
+
redirect_to services_url
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module ApplicationHelper
|
|
2
|
+
include ScaffoldLogic::FormHelper
|
|
3
|
+
include ScaffoldLogic::Helper
|
|
4
|
+
|
|
5
|
+
def breadcrumbs
|
|
6
|
+
html = [ link_to('Home', root_path) ]
|
|
7
|
+
html << link_to( 'Locations', locations_path ) if controller?('locations')
|
|
8
|
+
html << link_to( 'Search', directory_search_root_path ) if controller?('directory') && action?('search')
|
|
9
|
+
html << link_to( 'Services', services_path ) if controller?('services')
|
|
10
|
+
html << @locale.non_root_ancestors_and_self.map{ |l| link_to l.name, l.to_url } if @locale && ! @locale.country?
|
|
11
|
+
(html * ' > ').html_safe
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def set_title(title = nil)
|
|
15
|
+
if title.nil?
|
|
16
|
+
content_for(:title) { APPLICATION_NAME }
|
|
17
|
+
content_for(:page_title) { }
|
|
18
|
+
else
|
|
19
|
+
content_for(:title) { title + " - #{APPLICATION_NAME}" }
|
|
20
|
+
content_for(:page_title) { title }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
APPLICATION_NAME = Rails.application.class.to_s.split('::').first.freeze
|
|
27
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
|
|
3
|
+
class GoogleMapsGeocoder
|
|
4
|
+
# Returns the complete formatted address with standardized abbreviations.
|
|
5
|
+
attr_reader :formatted_address
|
|
6
|
+
# Returns the formatted street address with standardized abbreviations.
|
|
7
|
+
attr_reader :formatted_street_address
|
|
8
|
+
# Self-explanatory
|
|
9
|
+
attr_reader :city, :country_long_name, :country_short_name, :county, :lat, :lng, :postal_code, :state_long_name, :state_short_name
|
|
10
|
+
|
|
11
|
+
# Instance Methods: Overrides ====================================================================
|
|
12
|
+
|
|
13
|
+
# Geocodes the specified address and wraps the results in a geocoder object.
|
|
14
|
+
#
|
|
15
|
+
# ==== Attributes
|
|
16
|
+
#
|
|
17
|
+
# * +address+ - a geocodable address
|
|
18
|
+
#
|
|
19
|
+
# ==== Examples
|
|
20
|
+
#
|
|
21
|
+
# white_house = GoogleMapsGeocoder.new('1600 Pennsylvania Washington')
|
|
22
|
+
# white_house.formatted_address
|
|
23
|
+
# => "1600 Pennsylvania Ave NW, Washington D.C., DC 20500, USA"
|
|
24
|
+
def initialize(address)
|
|
25
|
+
response = Net::HTTP.get_response(URI.parse("http://maps.googleapis.com/maps/api/geocode/json?address=#{Rack::Utils.escape(address)}&sensor=false"))
|
|
26
|
+
@json = ActiveSupport::JSON.decode(response.body)
|
|
27
|
+
raise "Geocoding \"#{address}\" exceeded query limit! Google returned...\n#{@json.inspect}" if @json.blank? || @json['status'] != 'OK'
|
|
28
|
+
|
|
29
|
+
Rails.logger.info "Geocoded \"#{address}\" and Google returned...\n#{@json.inspect}"
|
|
30
|
+
|
|
31
|
+
@city, @country_short_name, @country_long_name, @county, @formatted_address, @formatted_street_address, @lat, @lng, @postal_code, @state_long_name, @state_short_name = parse_city, parse_country_short_name, parse_country_long_name, parse_county, parse_formatted_address, parse_formatted_street_address, parse_lat, parse_lng, parse_postal_code, parse_state_long_name, parse_state_short_name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Instance Methods ===============================================================================
|
|
35
|
+
|
|
36
|
+
# Returns true if the address Google returns is an exact match.
|
|
37
|
+
#
|
|
38
|
+
# ==== Examples
|
|
39
|
+
#
|
|
40
|
+
# white_house = GoogleMapsGeocoder.new('1600 Pennsylvania Ave')
|
|
41
|
+
# white_house.exact_match?
|
|
42
|
+
# => true
|
|
43
|
+
def exact_match?
|
|
44
|
+
! self.partial_match?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns true if the address Google returns isn't an exact match.
|
|
48
|
+
#
|
|
49
|
+
# ==== Examples
|
|
50
|
+
#
|
|
51
|
+
# white_house = GoogleMapsGeocoder.new('1600 Pennsylvania Washington')
|
|
52
|
+
# white_house.exact_match?
|
|
53
|
+
# => false
|
|
54
|
+
def partial_match?
|
|
55
|
+
@json['results'][0]['partial_match'] == true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def parse_address_component_type(type, name='long_name')
|
|
61
|
+
_address_component = @json['results'][0]['address_components'].detect{ |ac| ac['types'] && ac['types'].include?(type) }
|
|
62
|
+
_address_component && _address_component[name]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_city
|
|
66
|
+
parse_address_component_type('sublocality') || parse_address_component_type('locality')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_country_long_name
|
|
70
|
+
parse_address_component_type('country')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_country_short_name
|
|
74
|
+
parse_address_component_type('country', 'short_name')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_county
|
|
78
|
+
parse_address_component_type('administrative_area_level_2')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_formatted_address
|
|
82
|
+
@json['results'][0]['formatted_address']
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def parse_formatted_street_address
|
|
86
|
+
"#{parse_address_component_type('street_number')} #{parse_address_component_type('route')}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_lat
|
|
90
|
+
@json['results'][0]['geometry']['location']['lat']
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_lng
|
|
94
|
+
@json['results'][0]['geometry']['location']['lng']
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_postal_code
|
|
98
|
+
parse_address_component_type('postal_code')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def parse_state_long_name
|
|
102
|
+
parse_address_component_type('administrative_area_level_1')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_state_short_name
|
|
106
|
+
parse_address_component_type('administrative_area_level_1', 'short_name')
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
class Locale
|
|
2
|
+
require 'mongoid/tree'
|
|
3
|
+
require 'stringex'
|
|
4
|
+
|
|
5
|
+
include LuckySneaks::StringExtensions
|
|
6
|
+
include Mongoid::Document
|
|
7
|
+
include Mongoid::Timestamps
|
|
8
|
+
include Mongoid::Tree
|
|
9
|
+
|
|
10
|
+
# Constants ======================================================================================
|
|
11
|
+
|
|
12
|
+
# Mongo Config ===================================================================================
|
|
13
|
+
field :name
|
|
14
|
+
field :abbreviation
|
|
15
|
+
field :slug
|
|
16
|
+
field :accuracy, :type => Integer
|
|
17
|
+
field :lat_lng, :type => Array
|
|
18
|
+
field :kind
|
|
19
|
+
index [[ :lat_lng, Mongo::GEO2D ]], :min => -200, :max => 200
|
|
20
|
+
|
|
21
|
+
# Relationships ==================================================================================
|
|
22
|
+
referenced_in :location
|
|
23
|
+
|
|
24
|
+
# Scopes =========================================================================================
|
|
25
|
+
scope :canada, :where => {:name => 'Canada'}
|
|
26
|
+
scope :cities, :where => {:kind => 'city' }
|
|
27
|
+
scope :locations, :where => {:kind => 'location'}
|
|
28
|
+
scope :states, :where => {:kind => 'state'}
|
|
29
|
+
scope :us, :where => {:name => 'United States'}
|
|
30
|
+
|
|
31
|
+
# Callbacks ======================================================================================
|
|
32
|
+
after_create :post_process
|
|
33
|
+
|
|
34
|
+
# Macros =========================================================================================
|
|
35
|
+
attr_accessor :skip_geocoding
|
|
36
|
+
|
|
37
|
+
# Class Methods: Initialization ==================================================================
|
|
38
|
+
|
|
39
|
+
# Initialize the root-level locale.
|
|
40
|
+
def self.init_root
|
|
41
|
+
Locale.find_or_create_by(:name => 'Earth')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.find_or_create_country_for(location)
|
|
45
|
+
Locale.first(:conditions => {:name => location.country_long_name}) || Locale.create_country_for(location)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.find_or_create_state_for(location)
|
|
49
|
+
Locale.first(:conditions => {:name => location.state_long_name}) || Locale.find_or_create_country_for(location).children.create(:abbreviation => location.state, :name => location.state_long_name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Instance methods: Initialization ===============================================================
|
|
53
|
+
|
|
54
|
+
def post_process
|
|
55
|
+
self.set_slug
|
|
56
|
+
self.geocode
|
|
57
|
+
self.set_kind
|
|
58
|
+
self.save
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Instance methods: Overrides ====================================================================
|
|
62
|
+
|
|
63
|
+
# Returns this locale's latitude/longitude, delegating to location as needed (to prevent duplicate/conflicting coordinates).
|
|
64
|
+
def lat_lng
|
|
65
|
+
self.location ? self.location.lat_lng : self[:lat_lng]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def friendly_name
|
|
69
|
+
self.city? ? "#{self.name}, #{self.parent.name}" : self.name
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns this locale's sorted children.
|
|
73
|
+
def sorted_children
|
|
74
|
+
if self.root?
|
|
75
|
+
self.children.map{ |l| l.children }.flatten.sort_by{ |c| c.name.to_s }
|
|
76
|
+
else
|
|
77
|
+
self.children.sort_by{ |c| c.name.to_s }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Instance methods: Geocoding ====================================================================
|
|
82
|
+
|
|
83
|
+
# Geocodes this location.
|
|
84
|
+
def geocode
|
|
85
|
+
return if self.skip_geocoding || self.geocoding_address.blank?
|
|
86
|
+
|
|
87
|
+
unless @geocoder
|
|
88
|
+
@geocoder = GoogleMapsGeocoder.new(self.geocoding_address)
|
|
89
|
+
self.lat_lng = [@geocoder.lat, @geocoder.lng]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@geocoder
|
|
93
|
+
rescue SocketError
|
|
94
|
+
Rails.logger.error "Can't geocode without a network connection!"
|
|
95
|
+
rescue RuntimeError
|
|
96
|
+
Rails.logger.error "Can't geocode, over query limit!"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns true if this location has a latitude and longitude.
|
|
100
|
+
def geocoded?
|
|
101
|
+
self.lat_lng.is_a?(Array)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns this locale's address for geocoding.
|
|
105
|
+
def geocoding_address
|
|
106
|
+
a = case self.kind
|
|
107
|
+
when 'country' then self.name
|
|
108
|
+
when 'state' then self.name
|
|
109
|
+
when 'city' then [self.name, self.parent.name]
|
|
110
|
+
when 'location' then nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
a && a.select{|s| ! s.blank?} * ' '
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Return an appropriate Google-Maps zoom level.
|
|
117
|
+
def zoom_level
|
|
118
|
+
case self.kind
|
|
119
|
+
when 'country' then 3
|
|
120
|
+
when 'state' then 5
|
|
121
|
+
when 'city' then 9
|
|
122
|
+
when 'location' then 10
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Instance Methods: Tree =========================================================================
|
|
127
|
+
def destroy
|
|
128
|
+
if self.parent && self.parent.children.count == 1
|
|
129
|
+
Rails.logger.info "Destroyed #{self.parent.name}"
|
|
130
|
+
self.parent.destroy
|
|
131
|
+
elsif self.parent
|
|
132
|
+
Rails.logger.info "Did not destroy #{self.parent.name}: has #{self.parent.children.to_a.map{|x|x.name.to_s}.sort * ', '}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
super unless self.country?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns location-locales found within this locale.
|
|
139
|
+
def location_locales
|
|
140
|
+
self.descendants.select{ |d| d.location? && d.location }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Returns locations found within this locale.
|
|
144
|
+
def locations
|
|
145
|
+
(self.location_locales.map{ |l| l.location } | [self.location]).compact
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns an array of this locale's ancestors and itself. Countries are omitted.
|
|
149
|
+
def non_root_ancestors_and_self
|
|
150
|
+
self.ancestors_and_self.select{ |l| ! l.root? }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Sets the slug for this locale. Slugs from the locale tree are used to build this locale's URL.
|
|
154
|
+
def set_slug
|
|
155
|
+
self.slug = self.root? ? nil : self.name.to_s.to_url
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Returns the permalink for this locale.
|
|
159
|
+
def to_url
|
|
160
|
+
"/directory/#{(non_root_ancestors_and_self_names * '/').to_url.gsub('-slash-', '/')}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Instance Methods: Typing =======================================================================
|
|
164
|
+
def city?
|
|
165
|
+
self.kind == 'city'
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def country?
|
|
169
|
+
self.kind == 'country'
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def location?
|
|
173
|
+
self.kind == 'location'
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def planet?
|
|
177
|
+
self.kind == 'planet'
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def state?
|
|
181
|
+
self.kind == 'state'
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def set_kind
|
|
185
|
+
case self.depth
|
|
186
|
+
when 0; self.kind = 'planet'
|
|
187
|
+
when 1; self.kind = 'country'
|
|
188
|
+
when 2; self.kind = 'state'
|
|
189
|
+
when 3; self.kind = 'city'
|
|
190
|
+
when 4; self.kind = 'location'
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
def self.create_country_for(location)
|
|
197
|
+
Locale.init_root.children.create(:abbreviation => location.country, :name => location.country_long_name)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Returns an array of this locale's and its ancestors' names. Countries are omitted.
|
|
201
|
+
def non_root_ancestors_and_self_names
|
|
202
|
+
self.non_root_ancestors_and_self.map{ |l| l.name }
|
|
203
|
+
end
|
|
204
|
+
end
|