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