campfire_logic 1.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. data/Capfile +4 -0
  2. data/Gemfile +29 -0
  3. data/Gemfile.local +27 -0
  4. data/README.rdoc +17 -0
  5. data/Rakefile +43 -0
  6. data/VERSION +1 -0
  7. data/app/controllers/application_controller.rb +5 -0
  8. data/app/controllers/directory_controller.rb +65 -0
  9. data/app/controllers/locations_controller.rb +65 -0
  10. data/app/controllers/services_controller.rb +46 -0
  11. data/app/helpers/application_helper.rb +27 -0
  12. data/app/models/google_maps_geocoder.rb +108 -0
  13. data/app/models/locale.rb +204 -0
  14. data/app/models/location.rb +216 -0
  15. data/app/models/location_import.rb +48 -0
  16. data/app/models/service.rb +27 -0
  17. data/app/models/zip_code.rb +48 -0
  18. data/app/models/zip_code_import.rb +19 -0
  19. data/app/views/directory/_search_form.html.erb +4 -0
  20. data/app/views/directory/_show_children.html.erb +29 -0
  21. data/app/views/directory/_show_location.html.erb +14 -0
  22. data/app/views/directory/search.html.erb +31 -0
  23. data/app/views/directory/show.html.erb +32 -0
  24. data/app/views/layouts/application.html.erb +49 -0
  25. data/app/views/locations/_form.html.erb +69 -0
  26. data/app/views/locations/edit.html.erb +3 -0
  27. data/app/views/locations/export.erb +4 -0
  28. data/app/views/locations/import.html.erb +13 -0
  29. data/app/views/locations/index.html.erb +37 -0
  30. data/app/views/locations/new.html.erb +3 -0
  31. data/app/views/locations/show.html.erb +70 -0
  32. data/app/views/services/_form.html.erb +29 -0
  33. data/app/views/services/edit.html.erb +3 -0
  34. data/app/views/services/index.html.erb +25 -0
  35. data/app/views/services/new.html.erb +3 -0
  36. data/app/views/services/show.html.erb +15 -0
  37. data/app/views/shared/_location.html.erb +18 -0
  38. data/app/views/shared/_map.html.erb +2 -0
  39. data/app/views/shared/_nav_tabs.html.erb +5 -0
  40. data/autotest/discover.rb +1 -0
  41. data/campfire_logic.gemspec +208 -0
  42. data/config/application.rb +44 -0
  43. data/config/boot.rb +13 -0
  44. data/config/cucumber.yml +10 -0
  45. data/config/deploy.rb +40 -0
  46. data/config/environment.rb +6 -0
  47. data/config/environments/development.rb +28 -0
  48. data/config/environments/production.rb +49 -0
  49. data/config/environments/test.rb +35 -0
  50. data/config/initializers/campfire_logic.rb +3 -0
  51. data/config/initializers/metric_fu.rb +9 -0
  52. data/config/initializers/secret_token.rb +7 -0
  53. data/config/initializers/session_store.rb +8 -0
  54. data/config/locales/en.yml +5 -0
  55. data/config/mongoid.yml +25 -0
  56. data/config/routes.rb +96 -0
  57. data/config.ru +4 -0
  58. data/db/seeds.rb +5 -0
  59. data/db/zip_codes.txt +42742 -0
  60. data/doc/google_maps_response.rb +56 -0
  61. data/features/admin_manages_locations.feature +10 -0
  62. data/features/customer_browses_directory.feature +25 -0
  63. data/features/customer_searches_directory.feature +29 -0
  64. data/features/step_definitions/directory_steps.rb +22 -0
  65. data/features/step_definitions/location_steps.rb +10 -0
  66. data/features/step_definitions/web_steps.rb +211 -0
  67. data/features/support/env.rb +31 -0
  68. data/features/support/paths.rb +33 -0
  69. data/features/support/selectors.rb +39 -0
  70. data/init.rb +1 -0
  71. data/lib/campfire_logic/engine.rb +7 -0
  72. data/lib/campfire_logic/railtie.rb +10 -0
  73. data/lib/campfire_logic.rb +31 -0
  74. data/lib/tasks/campfire_logic.rake +7 -0
  75. data/lib/tasks/cucumber.rake +71 -0
  76. data/public/404.html +26 -0
  77. data/public/422.html +26 -0
  78. data/public/500.html +26 -0
  79. data/public/favicon.ico +0 -0
  80. data/public/images/icons/collapsed.gif +0 -0
  81. data/public/images/icons/delete.png +0 -0
  82. data/public/images/icons/drag.png +0 -0
  83. data/public/images/icons/edit.png +0 -0
  84. data/public/images/icons/expanded.gif +0 -0
  85. data/public/images/icons/help_icon.png +0 -0
  86. data/public/images/icons/link_icon.png +0 -0
  87. data/public/images/icons/move.png +0 -0
  88. data/public/images/icons/move_white.png +0 -0
  89. data/public/images/icons/note.png +0 -0
  90. data/public/images/icons/note_white.png +0 -0
  91. data/public/images/icons/notification_icon_sprite.png +0 -0
  92. data/public/images/icons/spinner.gif +0 -0
  93. data/public/images/icons/view.png +0 -0
  94. data/public/images/icons/warning.png +0 -0
  95. data/public/images/icons/warning_2.png +0 -0
  96. data/public/images/icons/warning_box.png +0 -0
  97. data/public/images/icons/warning_icon.png +0 -0
  98. data/public/images/icons/warning_white.png +0 -0
  99. data/public/images/layout/arrow_asc.png +0 -0
  100. data/public/images/layout/arrow_desc.png +0 -0
  101. data/public/images/layout/black_bar.png +0 -0
  102. data/public/images/layout/branding.png +0 -0
  103. data/public/images/layout/button_bg.png +0 -0
  104. data/public/images/layout/content_left_bg.png +0 -0
  105. data/public/images/layout/content_right_bg.png +0 -0
  106. data/public/images/layout/footer_bg.png +0 -0
  107. data/public/images/layout/h2_bg.png +0 -0
  108. data/public/images/layout/h2_bg_for_table.png +0 -0
  109. data/public/images/layout/header_bg_grey.png +0 -0
  110. data/public/images/layout/header_bg_purple.png +0 -0
  111. data/public/images/layout/legend_bg.png +0 -0
  112. data/public/images/layout/text_field_bg.jpg +0 -0
  113. data/public/images/layout/text_field_error_bg.png +0 -0
  114. data/public/images/layout/th_bg.png +0 -0
  115. data/public/images/layout/th_bg_selected.png +0 -0
  116. data/public/images/rails.png +0 -0
  117. data/public/index.html +9 -0
  118. data/public/javascripts/application.js +2 -0
  119. data/public/javascripts/controls.js +965 -0
  120. data/public/javascripts/dragdrop.js +974 -0
  121. data/public/javascripts/effects.js +1123 -0
  122. data/public/javascripts/prototype.js +6001 -0
  123. data/public/javascripts/rails.js +175 -0
  124. data/public/robots.txt +6 -0
  125. data/public/sample-locations.xls +1 -0
  126. data/public/stylesheets/.gitkeep +0 -0
  127. data/public/stylesheets/application.css +682 -0
  128. data/public/stylesheets/core.css +1147 -0
  129. data/public/stylesheets/core_ie.css +52 -0
  130. data/public/stylesheets/csshover3.htc +14 -0
  131. data/script/cucumber +10 -0
  132. data/script/rails +6 -0
  133. data/spec/controllers/directory_controller_spec.rb +11 -0
  134. data/spec/controllers/services_controller_spec.rb +62 -0
  135. data/spec/models/google_maps_geocoder_spec.rb +62 -0
  136. data/spec/models/locale_spec.rb +64 -0
  137. data/spec/models/location_import_spec.rb +41 -0
  138. data/spec/models/location_spec.rb +195 -0
  139. data/spec/rcov.opts +2 -0
  140. data/spec/spec_helper.rb +12 -0
  141. data/spec/test-locations.xls +1 -0
  142. metadata +361 -0
data/Capfile ADDED
@@ -0,0 +1,4 @@
1
+ load 'deploy' if respond_to?(:namespace) # cap2 differentiator
2
+ Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) }
3
+
4
+ load 'config/deploy' # remove this line to skip loading any of the default tasks
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,5 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery
3
+
4
+ ActionView::Base.default_form_builder = ScaffoldLogic::FormBuilder
5
+ end
@@ -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 * ' &gt; ').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