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.
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