Active 0.0.14 → 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,7 @@ require 'cgi'
4
4
  require 'rubygems'
5
5
  require 'mysql'
6
6
  require 'active_record'
7
+ require 'digest/md5'
7
8
 
8
9
  module Active
9
10
  module Services
@@ -30,7 +31,7 @@ module Active
30
31
 
31
32
  class Search
32
33
  attr_accessor :api_key, :start_date, :end_date, :location, :channels, :keywords, :search, :radius, :limit, :sort, :page, :offset, :latitude, :longitude,
33
- :view, :facet, :sort, :num_results, :asset_ids, :dma
34
+ :view, :facet, :sort, :num_results, :asset_ids, :dma, :city, :state, :country, :bounding_box
34
35
 
35
36
  attr_reader :results, :endIndex, :pageSize, :searchTime, :numberOfResults, :end_point, :meta
36
37
 
@@ -39,11 +40,11 @@ module Active
39
40
 
40
41
  def initialize(data={})
41
42
  self.api_key = data[:api_key] || ""
42
- self.location = data[:location] || ""
43
+ self.location = data[:location]
43
44
  self.zips = data[:zips] || []
44
45
  self.channels = data[:channels] || []
45
46
  self.keywords = data[:keywords] || []
46
- self.radius = data[:radius] || "50"
47
+ self.radius = data[:radius] || nil
47
48
  self.limit = data[:limit] || "10"
48
49
  self.sort = data[:sort] || Sort.DATE_ASC
49
50
  self.page = data[:page] || "1"
@@ -59,6 +60,10 @@ module Active
59
60
  self.latitude = data[:latitude]
60
61
  self.longitude = data[:longitude]
61
62
  self.dma = data[:dma]
63
+ self.city = data[:city]
64
+ self.state = data[:state]
65
+ self.country = data[:country]
66
+ self.bounding_box = data[:bounding_box]
62
67
  end
63
68
 
64
69
  # Example
@@ -78,6 +83,7 @@ module Active
78
83
  end
79
84
 
80
85
  def location=(value)
86
+ return if value.nil?
81
87
  @location = CGI.escape(value)
82
88
  end
83
89
 
@@ -100,6 +106,16 @@ module Active
100
106
  def asset_ids=(value)
101
107
  @asset_ids = value
102
108
  end
109
+
110
+ def bounding_box=(value)
111
+ return if value==nil
112
+ value = HashWithIndifferentAccess.new(value)
113
+ if value.has_key?("sw") && value.has_key?("ne")
114
+ @bounding_box=value
115
+ else
116
+ raise "bounding_box must be hash with keys sw and ne"
117
+ end
118
+ end
103
119
 
104
120
  def asset_id=(value)
105
121
  return if value.empty?
@@ -107,7 +123,8 @@ module Active
107
123
  end
108
124
 
109
125
  def end_point
110
- meta_data = ""
126
+ meta_data = ""
127
+ loc_str = ""
111
128
  # CHANNELS
112
129
  channel_keys = []
113
130
  @channels.each do |c|
@@ -127,16 +144,27 @@ module Active
127
144
  meta_data += temp_ids.join("+OR+")
128
145
  end
129
146
  # LOCATION
130
- # 1 Look for zip codes
131
- # 2 Look for lat lng
132
- # 3 Look for a formatted string "San Diego, CA, US"
133
- # 4 Look for a dma
134
- if not @zips.empty?
147
+ # 1 Look for :city, :state, :country
148
+ # 2 Look for zip codes
149
+ # 3 Look for lat lng
150
+ # 4 Look for a formatted string "San Diego, CA, US"
151
+ # 5 Look for a dma
152
+ if @city or @state or @country
153
+ if @city
154
+ meta_data += "+AND+" unless meta_data == ""
155
+ meta_data += "meta:city=#{Search.double_encode_channel(@city)}"
156
+ end
157
+ if @state
158
+ meta_data += "+AND+" unless meta_data == ""
159
+ meta_data += "meta:state=#{Search.double_encode_channel(@state)}"
160
+ end
161
+ elsif !@zips.empty?
162
+ # if not @zips.empty?
135
163
  loc_str = @zips.join(",")
136
164
  elsif @latitude and @longitude
137
165
  loc_str = "#{@latitude};#{@longitude}"
138
166
  elsif @dma
139
- loc_str = ""
167
+
140
168
  meta_data += "+AND+" unless meta_data == ""
141
169
  meta_data += "meta:dma=#{Search.double_encode_channel(@dma)}"
142
170
  else
@@ -154,7 +182,42 @@ module Active
154
182
  end
155
183
  meta_data += "meta:startDate:daterange:#{@start_date}..#{@end_date}"
156
184
 
157
- url = "#{SEARCH_URL}/search?api_key=#{@api_key}&num=#{@num_results}&page=#{@page}&l=#{loc_str}&f=#{@facet}&v=#{@view}&r=#{@radius}&s=#{@sort}&k=#{@keywords.join("+")}&m=#{meta_data}"
185
+ # BOUNDING BOX
186
+ if @bounding_box!=nil
187
+ #The values in the GSA metadata are shifted to prevent negative values. This was done b/c lat/long
188
+ # are searched as a number range and the GSA doesn't allow negative values in number ranges.
189
+ # We shift latitude values by 90 and longitude values by 180.
190
+
191
+ if @bounding_box[:sw].class==String
192
+ #String :bounding_box => { :sw => "37.695141,-123.013657", :ne => "37.832371,-122.356979"}
193
+ latitude1 = @bounding_box[:sw].split(",").first.to_f+90
194
+ latitude2 = @bounding_box[:ne].split(",").first.to_f+90
195
+ longitude1 = @bounding_box[:sw].split(",").last.to_f+180
196
+ longitude2 = @bounding_box[:ne].split(",").last.to_f+180
197
+ else
198
+ #hash query[:bounding_box] = { :sw => [123, 10], :ne => [222,10] }
199
+ latitude1 = @bounding_box[:sw].first.to_f+90
200
+ latitude2 = @bounding_box[:ne].first.to_f+90
201
+ longitude1 = @bounding_box[:sw].last.to_f+180
202
+ longitude2 = @bounding_box[:ne].last.to_f+180
203
+ end
204
+ meta_data += "+AND+" unless meta_data == ""
205
+ meta_data += "meta:latitudeShifted:#{latitude1}..#{latitude2}+AND+meta:longitudeShifted:#{longitude1}..#{longitude2}"
206
+ end
207
+
208
+ # url = "#{SEARCH_URL}/search?api_key=#{@api_key}&num=#{@num_results}&page=#{@page}&l=#{loc_str}&f=#{@facet}&v=#{@view}&r=#{@radius}&s=#{@sort}&k=#{@keywords.join("+")}&m=#{meta_data}"
209
+ urla = ["#{SEARCH_URL}/search?api_key=#{@api_key}"]
210
+ urla << "num=#{@num_results}" if @num_results
211
+ urla << "page=#{@page}" if @page
212
+ urla << "l=#{loc_str}" unless loc_str.nil? or loc_str.empty?
213
+ urla << "f=#{@facet}" if @facet
214
+ urla << "v=#{@view}" if @view
215
+ urla << "r=#{@radius}" if @radius
216
+ urla << "s=#{@sort}" if @sort
217
+ urla << "k=#{@keywords.join("+")}" if @keywords and !@keywords.empty?
218
+ urla << "m=#{meta_data}" if meta_data
219
+
220
+ return urla.join("&")
158
221
  end
159
222
 
160
223
  def search
@@ -163,8 +226,7 @@ module Active
163
226
  http = Net::HTTP.new(searchurl.host, searchurl.port)
164
227
  http.read_timeout = DEFAULT_TIMEOUT
165
228
 
166
- puts "#{searchurl.path}?#{searchurl.query}"
167
-
229
+ puts "Active Search [GET] #{"#{searchurl.path}?#{searchurl.query}"}"
168
230
  res = http.start { |http|
169
231
  http.get("#{searchurl.path}?#{searchurl.query}")
170
232
  }
@@ -176,7 +238,10 @@ module Active
176
238
  @pageSize = parsed_json["pageSize"]
177
239
  @searchTime = parsed_json["searchTime"]
178
240
  @numberOfResults = parsed_json["numberOfResults"]
179
- @results = parsed_json['_results'].collect { |a| Activity.new(a) }
241
+ @results = parsed_json['_results'].collect { |a| Activity.new(GSA.new(a)) }
242
+
243
+ Active.CACHE.set( Digest::MD5.hexdigest(end_point), self) if Active.CACHE
244
+
180
245
  rescue JSON::ParserError => e
181
246
  raise RuntimeError, "JSON::ParserError json=#{res.body}"
182
247
  @endIndex = 0
@@ -223,10 +288,29 @@ module Active
223
288
  # http://developer.active.com/docs/Activecom_Search_API_Reference
224
289
  # returns an array of results and query info
225
290
  def self.search(data=nil)
226
- search = Search.new(data)
227
- search.search
228
- return search
291
+ search = Search.new(data)
292
+ search_hash = Digest::MD5.hexdigest(search.end_point)
293
+ puts "search_hash #{search_hash}"
294
+ cache = Search.return_cached(search_hash)
295
+ if cache != nil
296
+ return cache
297
+ else
298
+ search.search
299
+ return search
300
+ end
229
301
  end
302
+
303
+ def self.return_cached key
304
+ if Active.CACHE
305
+ cached_version = Active.CACHE.get(key)
306
+ if cached_version
307
+ puts "Active Search [CACHE] #{key}"
308
+ return cached_version
309
+ end
310
+ end
311
+ nil
312
+ end
313
+
230
314
 
231
315
  def []
232
316
  @results
@@ -239,7 +323,6 @@ module Active
239
323
  str.gsub!(/\-/,"%252D")
240
324
  str
241
325
  end
242
-
243
326
  end
244
327
 
245
328
  # TODO move to a reflection service
@@ -0,0 +1,403 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'cgi'
4
+ require 'rubygems'
5
+ require 'mysql'
6
+ require 'active_record'
7
+
8
+ module Active
9
+ module Services
10
+
11
+ # we should remove this class and just replace with symbols
12
+ class Sort
13
+ def self.DATE_ASC
14
+ "date_asc"
15
+ end
16
+ def self.DATE_DESC
17
+ "date_desc"
18
+ end
19
+ def self.RELEVANCE
20
+ "relevance"
21
+ end
22
+ end
23
+
24
+ # we should remove this class and just replace with symbols
25
+ class Facet
26
+ def self.ACTIVITIES
27
+ "activities"
28
+ end
29
+ end
30
+
31
+ class Search
32
+ attr_accessor :api_key, :start_date, :end_date, :location, :channels, :keywords, :search, :radius, :limit, :sort, :page, :offset, :latitude, :longitude,
33
+ :view, :facet, :sort, :num_results, :asset_ids, :dma, :city, :state, :country
34
+
35
+ attr_reader :results, :endIndex, :pageSize, :searchTime, :numberOfResults, :end_point, :meta
36
+
37
+ SEARCH_URL = "http://search.active.com"
38
+ DEFAULT_TIMEOUT = 60
39
+
40
+ def initialize(data={})
41
+ self.api_key = data[:api_key] || ""
42
+ self.location = data[:location] || ""
43
+ self.zips = data[:zips] || []
44
+ self.channels = data[:channels] || []
45
+ self.keywords = data[:keywords] || []
46
+ self.radius = data[:radius] || nil
47
+ self.limit = data[:limit] || "10"
48
+ self.sort = data[:sort] || Sort.DATE_ASC
49
+ self.page = data[:page] || "1"
50
+ self.offset = data[:offset] || "0"
51
+ self.view = data[:view] || "json"
52
+ self.facet = data[:facet] || Facet.ACTIVITIES
53
+ self.num_results = data[:num_results] || "10"
54
+ self.search = data[:search] || ""
55
+ self.start_date = data[:start_date] || "today"
56
+ self.end_date = data[:end_date] || "+"
57
+ self.asset_ids = data[:asset_ids] || []
58
+ self.asset_id = data[:asset_id] || ""
59
+ self.latitude = data[:latitude]
60
+ self.longitude = data[:longitude]
61
+ self.dma = data[:dma]
62
+ self.city = data[:city]
63
+ self.state = data[:state]
64
+ self.country = data[:country]
65
+ end
66
+
67
+ # Example
68
+ # Search.search( {:zips => "92121, 92078, 92114"} )
69
+ # or
70
+ # Search.new( {:zips => [92121, 92078, 92114]} )
71
+ def zips=(value)
72
+ if value.class == String
73
+ @zips = value.split(",").each { |k| k.strip! }
74
+ else
75
+ @zips = value
76
+ end
77
+ end
78
+
79
+ def zips
80
+ @zips
81
+ end
82
+
83
+ def location=(value)
84
+ @location = CGI.escape(value)
85
+ end
86
+
87
+ def keywords=(value)
88
+ if value.class == String
89
+ @keywords = value.gsub(",", " ").split(" ").each { |k| k.strip! }
90
+ else
91
+ @keywords = value
92
+ end
93
+ end
94
+
95
+ def channels=(value)
96
+ if value.class == String
97
+ @channels = value.split(",").each { |k| k.strip! }
98
+ else
99
+ @channels = value
100
+ end
101
+ end
102
+
103
+ def asset_ids=(value)
104
+ @asset_ids = value
105
+ end
106
+
107
+ def asset_id=(value)
108
+ return if value.empty?
109
+ @asset_ids<<value
110
+ end
111
+
112
+ def end_point
113
+ meta_data = ""
114
+ loc_str = ""
115
+ # CHANNELS
116
+ channel_keys = []
117
+ @channels.each do |c|
118
+ c.to_sym
119
+ if Categories.CHANNELS.include?(c)
120
+ channel_keys << Categories.CHANNELS[c]
121
+ end
122
+ end
123
+ meta_data = channel_keys.collect { |channel| "meta:channel=#{Search.double_encode_channel(channel)}" }.join("+OR+")
124
+ # ASSET IDS
125
+ unless asset_ids.empty?
126
+ meta_data += "+AND+" unless meta_data == ""
127
+ temp_ids = []
128
+ asset_ids.each do |id|
129
+ temp_ids << "meta:" + CGI.escape("assetId=#{id.gsub("-","%2d")}")
130
+ end
131
+ meta_data += temp_ids.join("+OR+")
132
+ end
133
+ # LOCATION
134
+ # 1 Look for :city, :state, :country
135
+ # 2 Look for zip codes
136
+ # 3 Look for lat lng
137
+ # 4 Look for a formatted string "San Diego, CA, US"
138
+ # 5 Look for a dma
139
+
140
+ if @city or @state or @country
141
+ if @city
142
+ meta_data += "+AND+" unless meta_data == ""
143
+ meta_data += "meta:city=#{Search.double_encode_channel(@city)}"
144
+ end
145
+ if @state
146
+ meta_data += "+AND+" unless meta_data == ""
147
+ meta_data += "meta:state=#{Search.double_encode_channel(@state)}"
148
+ end
149
+ elsif !@zips.empty?
150
+ # if not @zips.empty?
151
+ loc_str = @zips.join(",")
152
+ elsif @latitude and @longitude
153
+ loc_str = "#{@latitude};#{@longitude}"
154
+ elsif @dma
155
+
156
+ meta_data += "+AND+" unless meta_data == ""
157
+ meta_data += "meta:dma=#{Search.double_encode_channel(@dma)}"
158
+ else
159
+ loc_str = @location
160
+ end
161
+
162
+
163
+ # AND DATE
164
+ meta_data += "+AND+" unless meta_data == ""
165
+ if @start_date.class == Date
166
+ @start_date = URI.escape(@start_date.strftime("%m/%d/%Y")).gsub(/\//,"%2F")
167
+ end
168
+ if @end_date.class == Date
169
+ @end_date = URI.escape(@end_date.strftime("%m/%d/%Y")).gsub(/\//,"%2F")
170
+ end
171
+ meta_data += "meta:startDate:daterange:#{@start_date}..#{@end_date}"
172
+
173
+ # url = "#{SEARCH_URL}/search?api_key=#{@api_key}&num=#{@num_results}&page=#{@page}&l=#{loc_str}&f=#{@facet}&v=#{@view}&r=#{@radius}&s=#{@sort}&k=#{@keywords.join("+")}&m=#{meta_data}"
174
+ urla = ["#{SEARCH_URL}/search?api_key=#{@api_key}"]
175
+ urla << "num=#{@num_results}" if @num_results
176
+ urla << "page=#{@page}" if @page
177
+ urla << "l=#{loc_str}" unless loc_str.empty?
178
+ urla << "f=#{@facet}" if @facet
179
+ urla << "v=#{@view}" if @view
180
+ urla << "r=#{@radius}" if @radius
181
+ urla << "s=#{@sort}" if @sort
182
+ urla << "k=#{@keywords.join("+")}" if @keywords and !@keywords.empty?
183
+ urla << "m=#{meta_data}" if meta_data
184
+
185
+ return urla.join("&")
186
+ end
187
+
188
+ def search
189
+ searchurl = URI.parse(end_point)
190
+ req = Net::HTTP::Get.new(searchurl.path)
191
+ http = Net::HTTP.new(searchurl.host, searchurl.port)
192
+ http.read_timeout = DEFAULT_TIMEOUT
193
+
194
+ puts "#{searchurl.path}?#{searchurl.query}"
195
+
196
+ res = http.start { |http|
197
+ http.get("#{searchurl.path}?#{searchurl.query}")
198
+ }
199
+
200
+ if (200..307).include?(res.code.to_i)
201
+ begin
202
+ parsed_json = JSON.parse(res.body)
203
+ @endIndex = parsed_json["endIndex"]
204
+ @pageSize = parsed_json["pageSize"]
205
+ @searchTime = parsed_json["searchTime"]
206
+ @numberOfResults = parsed_json["numberOfResults"]
207
+ @results = parsed_json['_results'].collect { |a| GSA.new(a) }
208
+ rescue JSON::ParserError => e
209
+ raise RuntimeError, "JSON::ParserError json=#{res.body}"
210
+ @endIndex = 0
211
+ @pageSize = 0
212
+ @searchTime = 0
213
+ @numberOfResults = 0
214
+ @results = []
215
+ end
216
+
217
+ else
218
+ raise RuntimeError, "Active Search responded with a #{res.code} for your query."
219
+ end
220
+ end
221
+
222
+ # examples
223
+ #
224
+ #
225
+ # = Keywords =
226
+ # Keywords can be set like this
227
+ # Search.new({:keywords => "Dog,Cat,Cow"})
228
+ # Search.new({:keywords => %w(Dog Cat Cow)})
229
+ # Search.new({:keywords => ["Dog","Cat","Cow"]})
230
+ #
231
+ # = Location =
232
+ # The location will be set in this order and will override other values. For example is you set a zip code and a location only the zip will be used.
233
+ #
234
+ # 1 Look for zip codes
235
+ # Search.search( {:zips => "92121, 92078, 92114"} )
236
+ # Search.search( {:zips => %w(92121, 92078, 92114)} )
237
+ # Search.search( {:zips => [92121, 92078, 92114]} )
238
+ #
239
+ # 2 Look for lat lng
240
+ # Search.search( {:latitude=>"37.785895", :longitude=>"-122.40638"} )
241
+ #
242
+ # 3 Look for a formatted string "San Diego, CA, US"
243
+ # Search.search( {:location = "San Diego, CA, US"} )
244
+ #
245
+ # 4 Look for a DMA
246
+ # Search.new({:dma=>"San Francisco - Oakland - San Jose"})
247
+ #
248
+ # = How to look at the results =
249
+ #
250
+ #
251
+ # http://developer.active.com/docs/Activecom_Search_API_Reference
252
+ # returns an array of results and query info
253
+ def self.search(data=nil)
254
+ search = Search.new(data)
255
+ search.search
256
+ return search
257
+ end
258
+
259
+ def []
260
+ @results
261
+ end
262
+
263
+ private
264
+ def self.double_encode_channel str
265
+ str = URI.escape(str, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
266
+ str = URI.escape(str, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
267
+ str.gsub!(/\-/,"%252D")
268
+ str
269
+ end
270
+
271
+ end
272
+
273
+ # TODO move to a reflection service
274
+ class Categories
275
+ def self.CHANNELS
276
+ {
277
+ :active_lifestyle => '',
278
+ :fitness => 'Fitness',
279
+ :body_building => 'Fitness\Body Building',
280
+ :boxing => 'Fitness\Boxing',
281
+ :weight_lifting => 'Fitness\Weight Lifting',
282
+ :gear => 'Gear',
283
+ :lifestyle_vehicles => 'Lifestyle Vehicles',
284
+ :mind_mody => 'Mind & Body',
285
+ :meditation => 'Mind & Body\Meditation',
286
+ :pilates => 'Mind & Body\Pilates',
287
+ :yoga => 'Mind & Body\Yoga',
288
+ :nutrition => 'Nutrition',
289
+ :travel => 'Travel',
290
+ :women => 'Women',
291
+ :other => 'Other',
292
+ :corporate => 'Corporate',
293
+ :not_specified => 'Not Specified',
294
+ :unknown => 'Unknown',
295
+ :special_interest => 'Special+Interest',
296
+ :giving => 'Giving',
297
+ :parks_recreation => 'Parks & Recreation',
298
+ :gear => 'Parks & Recreation\Gear',
299
+ :mind_body => 'Parks & Recreation\Mind & Body',
300
+ :travel => 'Parks & Recreation\Travel',
301
+ :vehicles => 'Parks & Recreation\Vehicles',
302
+ :women => 'Parks & Recreation\Women',
303
+ :reunions => 'Reunions',
304
+ :sports => 'Sports',
305
+ :action_sports => 'Action Sports',
306
+ :auto_racing => 'Action Sports\Auto Racing',
307
+ :bmx => 'Action Sports\BMX',
308
+ :dirt_bike_racing => 'Action Sports\Dirt Bike Racing',
309
+ :motocross => 'Action Sports\Motocross',
310
+ :motorcycle_racing => 'Action Sports\Motorcycle Racing',
311
+ :skateboarding => 'Action Sports\Skateboarding',
312
+ :skydiving => 'Action Sports\Skydiving',
313
+ :surfing => 'Action Sports\Surfing',
314
+ :wake_kite_boarding => 'Action Sports\Wake/Kite Boarding',
315
+ :water_skiing => 'Action Sports\Water Skiing',
316
+ :wind_surfing => 'Action Sports\Wind Surfing',
317
+ :baseball => 'Baseball',
318
+ :little_league_baseball => 'Baseball\Little League Baseball',
319
+ :tee_ball => 'Baseball\Tee Ball',
320
+ :basketball => 'Basketball',
321
+ :cheerleading => 'Cheerleading',
322
+ :cycling => 'Cycling',
323
+ :field_hockey => 'Field Hockey',
324
+ :football => 'Football',
325
+ :flag_football => 'Football\Flag Football',
326
+ :football_au => 'Football\Football-AU',
327
+ :golf => 'Golf',
328
+ :ice_hockey => 'Ice Hockey',
329
+ :lacrosse => 'Lacrosse',
330
+ :more_sports => 'More Sports',
331
+ :adventure_racing => 'More Sports\Adventure Racing',
332
+ :archery => 'More Sports\Archery',
333
+ :badminton => 'More Sports\Badminton',
334
+ :billiards => 'More Sports\Billiards',
335
+ :bowling => 'More Sports\Bowling',
336
+ :cricket => 'More Sports\Cricket',
337
+ :croquet => 'More Sports\Croquet',
338
+ :curling => 'More Sports\Curling',
339
+ :dance => 'More Sports\Dance',
340
+ :disc_sports => 'More Sports\Disc Sports',
341
+ :dodgeball => 'More Sports\Dodgeball',
342
+ :duathlon => 'More Sports\Duathlon',
343
+ :equestrian => 'More Sports\Equestrian',
344
+ :fencing => 'More Sports\Fencing',
345
+ :figure_skating => 'More Sports\Figure Skating',
346
+ :gymnastics => 'More Sports\Gymnastics',
347
+ :inline_hockey => 'More Sports\Inline Hockey',
348
+ :inline_skating => 'More Sports\Inline Skating',
349
+ :kickball => 'More Sports\Kickball',
350
+ :martial_arts => 'More Sports\Martial Arts',
351
+ :paintball => 'More Sports\Paintball',
352
+ :polo => 'More Sports\Polo',
353
+ :racquetball => 'More Sports\Racquetball',
354
+ :rowing => 'More Sports\Rowing',
355
+ :rugby => 'More Sports\Rugby',
356
+ :scouting => 'More Sports\Scouting',
357
+ :scuba_diving => 'More Sports\Scuba Diving',
358
+ :skating => 'More Sports\Skating',
359
+ :squash => 'More Sports\Squash',
360
+ :ultimate_frisbee => 'More Sports\Ultimate Frisbee',
361
+ :water_polo => 'More Sports\Water Polo',
362
+ :mountain_biking => 'Mountain Biking',
363
+ :outdoors => 'Outdoors',
364
+ :canoeing => 'Outdoors\Canoeing',
365
+ :climbing => 'Outdoors\Climbing',
366
+ :hiking => 'Outdoors\Hiking',
367
+ :kayaking => 'Outdoors\Kayaking',
368
+ :orienteering => 'Outdoors\Orienteering',
369
+ :outrigging => 'Outdoors\Outrigging',
370
+ :rafting => 'Outdoors\Rafting',
371
+ :racquetball => 'Racquetball',
372
+ :rugby => 'Rugby',
373
+ :running => 'Running',
374
+ :cross_country => 'Running\Cross Country',
375
+ :marathon_running => 'Running\Marathon Running',
376
+ :track_field => 'Running\Track & Field',
377
+ :trail_running => 'Running\Trail Running',
378
+ :sailing => 'Sailing',
379
+ :snow_sports => 'Snow Sports',
380
+ :skiing => 'Snow Sports\Skiing',
381
+ :snowboarding => 'Snow Sports\Snowboarding',
382
+ :snowshoeing => 'Snow Sports\Snowshoeing',
383
+ :soccer => 'Soccer',
384
+ :softball => 'Softball',
385
+ :softball_dixie => 'Softball\Softball-Dixie',
386
+ :softball_fast_pitch => 'Softball\Softball-Fast Pitch',
387
+ :softball_slow_pitch => 'Softball\Softball-Slow Pitch',
388
+ :squash => 'Squash',
389
+ :swimming => 'Swimming',
390
+ :diving => 'Swimming\Diving',
391
+ :tennis => 'Tennis',
392
+ :other_tennis => 'Tennis\Other Tennis',
393
+ :usta => 'Tennis\USTA',
394
+ :triathlon => 'Triathlon',
395
+ :volleyball => 'Volleyball',
396
+ :walking => 'Walking',
397
+ :wrestling => 'Wrestling'
398
+ }
399
+ end
400
+ end
401
+
402
+ end
403
+ end