sportdb 0.9.7 → 1.0.0

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 (76) hide show
  1. data/Manifest.txt +12 -55
  2. data/Rakefile +11 -10
  3. data/bin/sportdb +1 -2
  4. data/config/fixtures.de.yml +42 -0
  5. data/config/fixtures.en.yml +52 -0
  6. data/config/fixtures.es.yml +44 -0
  7. data/config/fixtures.pt.yml +44 -0
  8. data/lib/sportdb/cli/main.rb +189 -0
  9. data/lib/sportdb/cli/opts.rb +15 -69
  10. data/lib/sportdb/data/fixtures.rb +205 -0
  11. data/lib/sportdb/{keys.rb → data/keys.rb} +9 -2
  12. data/lib/sportdb/data/models.rb +43 -0
  13. data/lib/sportdb/deleter.rb +27 -0
  14. data/lib/sportdb/lang.rb +265 -0
  15. data/lib/sportdb/models/event.rb +0 -34
  16. data/lib/sportdb/reader.rb +322 -78
  17. data/lib/sportdb/stats.rb +28 -0
  18. data/lib/sportdb/utils.rb +90 -13
  19. data/lib/sportdb/version.rb +1 -1
  20. data/lib/sportdb.rb +49 -198
  21. data/tasks/test.rb +163 -0
  22. metadata +46 -71
  23. data/data/america/2011.rb +0 -36
  24. data/data/america/2011.txt +0 -110
  25. data/data/america/2011.yml +0 -30
  26. data/data/america/teams.txt +0 -26
  27. data/data/at/2011_12/bl.rb +0 -30
  28. data/data/at/2011_12/bl.txt +0 -79
  29. data/data/at/2011_12/cup.rb +0 -66
  30. data/data/at/2012_13/bl.rb +0 -33
  31. data/data/at/2012_13/bl.txt +0 -198
  32. data/data/at/2012_13/cup.rb +0 -111
  33. data/data/at/2012_13/cup.txt +0 -89
  34. data/data/at/badges.rb +0 -22
  35. data/data/at/teams.txt +0 -88
  36. data/data/cl/2011_12/cl.rb +0 -88
  37. data/data/cl/2011_12/el.rb +0 -65
  38. data/data/cl/2012_13/cl.rb +0 -324
  39. data/data/cl/badges.rb +0 -68
  40. data/data/cl/teams.txt +0 -46
  41. data/data/copa/sud_2012_13.rb +0 -38
  42. data/data/copa/sud_2012_13.txt +0 -49
  43. data/data/copa/teams.txt +0 -35
  44. data/data/de/2012_13/bl.rb +0 -38
  45. data/data/de/2012_13/bl.txt +0 -216
  46. data/data/de/teams.txt +0 -23
  47. data/data/en/2012_13/pl.rb +0 -39
  48. data/data/en/2012_13/pl.txt +0 -291
  49. data/data/en/teams.txt +0 -26
  50. data/data/es/teams.txt +0 -8
  51. data/data/euro/2008.rb +0 -30
  52. data/data/euro/2008.txt +0 -114
  53. data/data/euro/2012.rb +0 -25
  54. data/data/euro/2012.txt +0 -114
  55. data/data/euro/teams.txt +0 -36
  56. data/data/leagues.rb +0 -68
  57. data/data/mx/apertura_2012.rb +0 -35
  58. data/data/mx/apertura_2012.txt +0 -106
  59. data/data/mx/teams.txt +0 -20
  60. data/data/nfl/teams.txt +0 -46
  61. data/data/nhl/teams.txt +0 -40
  62. data/data/ro/l1_2012_13.rb +0 -137
  63. data/data/ro/teams.txt +0 -26
  64. data/data/seasons.rb +0 -22
  65. data/data/world/2010.rb +0 -93
  66. data/data/world/2010.txt +0 -215
  67. data/data/world/quali_2012_13_america.rb +0 -25
  68. data/data/world/quali_2012_13_america.txt +0 -41
  69. data/data/world/quali_2012_13_europe.rb +0 -32
  70. data/data/world/quali_2012_13_europe_c.txt +0 -85
  71. data/data/world/quali_2012_13_europe_i.txt +0 -74
  72. data/data/world/teams.txt +0 -25
  73. data/lib/sportdb/cli/runner.rb +0 -147
  74. data/lib/sportdb/loader.rb +0 -82
  75. data/lib/sportdb/templater.rb +0 -81
  76. data/templates/fixtures.rb.erb +0 -51
@@ -2,82 +2,291 @@
2
2
 
3
3
  module SportDB
4
4
 
5
+ class Reader
5
6
 
6
- ##
7
- ## fix/todo: move to/merge into LineReader itself
7
+ ## make models available in sportdb module by default with namespace
8
+ # e.g. lets you use Team instead of Models::Team
9
+ include SportDB::Models
8
10
 
9
- class StringLineReader
10
11
 
11
- def initialize( logger=nil, data )
12
+ def initialize( logger=nil )
12
13
  if logger.nil?
13
- @logger = Logger.new(STDOUT)
14
- @logger.level = Logger::INFO
14
+ @logger = LogUtils::Logger.new
15
+ ## @logger = Logger.new(STDOUT)
16
+ ## @logger.level = Logger::INFO
15
17
  else
16
18
  @logger = logger
17
19
  end
18
-
19
- @data = data
20
20
  end
21
21
 
22
22
  attr_reader :logger
23
23
 
24
+ def load_setup_with_include_path( setup, include_path )
25
+ ary = load_fixture_setup_with_include_path( setup, include_path )
26
+ load_with_include_path( ary, include_path )
27
+ end # method load_setup_with_include_path
24
28
 
25
- def each_line
26
- @data.each_line do |line|
27
-
28
- if line =~ /^\s*#/
29
- # skip komments and do NOT copy to result (keep comments secret!)
30
- logger.debug 'skipping comment line'
31
- next
29
+
30
+ ## fix/todo: rename ??
31
+ def load_fixture_setup_with_include_path( name, include_path )
32
+
33
+ ## todo/fix: cleanup quick and dirty code
34
+
35
+ path = "#{include_path}/#{name}.yml"
36
+
37
+ logger.info "parsing data '#{name}' (#{path})..."
38
+
39
+ text = File.read_utf8( path )
40
+
41
+ hash = YAML.load( text )
42
+
43
+ ### build up array for fixtures from hash
44
+
45
+ ary = []
46
+
47
+ hash.each do |key_wild, value_wild|
48
+ key = key_wild.to_s.strip
49
+
50
+ logger.debug "yaml key:#{key_wild.class.name} >>#{key}<<, value:#{value_wild.class.name} >>#{value_wild}<<"
51
+
52
+ if value_wild.kind_of?( String ) # assume non-event data
53
+ ary << value_wild
54
+ elsif value_wild.kind_of?( Array ) # assume non_event data as array of strings
55
+ ary = ary + value_wild
56
+ elsif value_wild.kind_of?( Hash ) # assume event data
57
+
58
+ value_wild.each do |event_key, event_values|
59
+ # e.g.
60
+ # at.2012/13: at/2012_13/bl, at/2012_13/bl2
61
+ # becomes
62
+ # [ 'at.2012/13', 'at/2012_13/bl', 'at/2012_13/bl2' ]
63
+ ary << ( [ event_key.to_s ] + event_values.split(',') )
64
+ end
65
+
66
+ else
67
+ logger.error "unknow fixture type in setup (yaml key:#{key_wild.class.name} >>#{key}<<, value:#{value_wild.class.name} >>#{value_wild}<<); skipping"
32
68
  end
69
+
70
+ end
71
+
72
+ puts "[debug] fixture setup:"
73
+ pp ary
74
+
75
+ ary
76
+
77
+ end # load_fixture_setup_with_include_path
78
+
79
+
80
+ def load_with_include_path( ary, include_path ) # convenience helper for all-in-one reader
81
+
82
+ puts "[debug] enter load_with_include_path (include_path=>>#{include_path}<<):"
83
+ pp ary
84
+
85
+ ary.each do |rec|
86
+ if rec.kind_of?( String )
87
+ ## assume single fixture name
88
+ name = rec
33
89
 
34
- if line =~ /^\s*$/
35
- # kommentar oder leerzeile überspringen
36
- logger.debug 'skipping blank line'
37
- next
90
+ if name =~ /^seasons/
91
+ load_seasons_with_include_path( name, include_path )
92
+ elsif name =~ /^leagues/
93
+ if name =~ /club/
94
+ # e.g. leagues_club
95
+ load_leagues_with_include_path( name, include_path, { club: true } )
96
+ else
97
+ # e.g. leagues
98
+ load_leagues_with_include_path( name, include_path )
99
+ end
100
+ elsif name =~ /^([a-z]{2})\/teams/
101
+ # auto-add country code (from folder structure) for country-specific teams
102
+ # e.g. at/teams at/teams2 de/teams etc.
103
+ country_key = $1
104
+ country = Country.find_by_key!( country_key )
105
+ load_teams_with_include_path( name, include_path, { club: true, country_id: country.id } )
106
+ elsif name =~ /\/teams/
107
+ if name =~ /club/
108
+ # club teams (many countries)
109
+ # e.g. club/europe/teams
110
+ load_teams_with_include_path( name, include_path, { club: true } )
111
+ else
112
+ # assume national teams
113
+ # e.g. world/teams amercia/teams_n
114
+ load_teams_with_include_path( name, include_path, { national: true } )
115
+ end
116
+ else
117
+ logger.error "unknown sportdb fixture type >#{name}<"
118
+ # todo/fix: exit w/ error
119
+ end
120
+ else # more than one item in record? assume fixture starting w/ event key
121
+
122
+ # assume first item is key
123
+ # assume second item is event plus fixture
124
+ # assume option third,etc are fixtures (e.g. bl2, etc.)
125
+ event_key = rec[0] # e.g. at.2012/13
126
+ event_name = rec[1] # e.g. at/2012_13/bl
127
+ fixture_names = rec[1..-1] # e.g. at/2012_13/bl, at/2012_13/bl2
128
+
129
+ load_event_with_include_path( event_name, include_path )
130
+ fixture_names.each do |fixture_name|
131
+ load_fixtures_with_include_path( event_key, fixture_name, include_path )
132
+ end
38
133
  end
134
+
135
+ end # each ary
136
+ end # method load_with_include_path
39
137
 
40
- # remove leading and trailing whitespace
41
- line = line.strip
42
-
43
- yield( line )
44
- end # each lines
45
- end # method each_line
46
138
 
47
- end
139
+ def load_leagues_with_include_path( name, include_path, more_values={} )
140
+
141
+ path = "#{include_path}/#{name}.txt"
48
142
 
143
+ logger.info "parsing data '#{name}' (#{path})..."
49
144
 
50
- class Reader
145
+ reader = ValuesReader.new( logger, path, more_values )
51
146
 
52
- ## make models available in sportdb module by default with namespace
53
- # e.g. lets you use Team instead of Models::Team
54
- include SportDB::Models
147
+ load_leagues_worker( reader )
148
+
149
+ ### todo/fix: add prop
150
+ ### Prop.create!( key: "db.#{fixture_name_to_prop_key(name)}.version", value: "file.txt.#{File.mtime(path).strftime('%Y.%m.%d')}" )
151
+
152
+ end # load_leagues_with_include_path
55
153
 
56
154
 
57
- def initialize( logger=nil )
58
- if logger.nil?
59
- @logger = Logger.new(STDOUT)
60
- @logger.level = Logger::INFO
61
- else
62
- @logger = logger
63
- end
64
- end
155
+ def load_seasons_with_include_path( name, include_path )
156
+ path = "#{include_path}/#{name}.yml"
65
157
 
66
- attr_reader :logger
158
+ puts "*** parsing data '#{name}' (#{path})..."
67
159
 
68
- def run( opts, args )
69
-
70
- args.each do |arg|
71
- name = arg # File.basename( arg, '.*' )
160
+ reader = HashReader.new( logger, path )
161
+
162
+ reader.each_typed do |key, value|
72
163
 
73
- if opts.load?
74
- load_fixtures_builtin( opts.event, name )
164
+ ## puts "processing event attrib >>#{key}<< >>#{value}<<..."
165
+
166
+ if key == 'seasons'
167
+
168
+ puts "#{value.class.name}: >>#{value}<<"
169
+
170
+ ## nb: assume value is an array
171
+ value.each do |item|
172
+ season_attribs = {}
173
+
174
+ season = Season.find_by_key( item.to_s.strip )
175
+
176
+ ## check if it exists
177
+ if season.present?
178
+ puts "*** update season #{season.id}-#{season.key}:"
179
+ else
180
+ puts "*** create season:"
181
+ season = Season.new
182
+ season_attribs[ :key ] = item.to_s.strip
183
+ end
184
+
185
+ season_attribs[:title] = item.to_s.strip
186
+
187
+ puts season_attribs.to_json
188
+
189
+ season.update_attributes!( season_attribs )
190
+ end
191
+
75
192
  else
76
- load_fixtures_with_include_path( opts.event, name, opts.data_path )
193
+ logger.error "unknown seasons key; skipping"
77
194
  end
78
- end
195
+
196
+ end # each key,value
197
+
198
+ ### todo/fix: add prop
199
+ ### Prop.create_from_sportdb_fixture!( name, path )
200
+
201
+ end # load_seasons_with_include_path
79
202
 
80
- end
203
+
204
+
205
+ def load_event_with_include_path( name, include_path )
206
+ path = "#{include_path}/#{name}.yml"
207
+
208
+ logger.info "parsing data '#{name}' (#{path})..."
209
+
210
+ reader = HashReader.new( logger, path )
211
+
212
+ event_attribs = {}
213
+
214
+ reader.each_typed do |key, value|
215
+
216
+ ## puts "processing event attrib >>#{key}<< >>#{value}<<..."
217
+
218
+ if key == 'league'
219
+ league = League.find_by_key( value.to_s.strip )
220
+
221
+ ## check if it exists
222
+ if league.present?
223
+ event_attribs['league_id'] = league.id
224
+ else
225
+ logger.error "league with key >>#{value.to_s.strip}<< missing"
226
+ exit 1
227
+ end
228
+
229
+ elsif key == 'season'
230
+ season = Season.find_by_key( value.to_s.strip )
231
+
232
+ ## check if it exists
233
+ if season.present?
234
+ event_attribs['season_id'] = season.id
235
+ else
236
+ logger.error "season with key >>#{value.to_s.strip}<< missing"
237
+ exit 1
238
+ end
239
+
240
+ elsif key == 'start_at'
241
+
242
+ if value.is_a?(DateTime) || value.is_a?(Date)
243
+ start_at = value
244
+ else # assume it's a string
245
+ start_at = DateTime.strptime( value.to_s.strip, '%Y-%m-%d %H:%M' )
246
+ end
247
+
248
+ event_attribs['start_at'] = start_at
249
+
250
+ elsif key == 'teams'
251
+
252
+ ## assume teams value is an array
253
+
254
+ team_ids = []
255
+ value.each do |item|
256
+ team_key = item.to_s.strip
257
+ team = Team.find_by_key!( team_key )
258
+ team_ids << team.id
259
+ end
260
+
261
+ event_attribs['team_ids'] = team_ids
262
+
263
+ elsif key == 'team3'
264
+ ## for now always assume false # todo: fix - use value and convert to boolean if not boolean
265
+ event_attribs['team3'] = false
266
+ else
267
+ ## todo: add a source location struct to_s or similar (file, line, col)
268
+ logger.error "unknown event attrib; skipping attrib"
269
+ end
270
+
271
+ end # each key,value
272
+
273
+ event = Event.find_by_league_id_and_season_id( event_attribs['league_id'], event_attribs['season_id'])
274
+
275
+ ## check if it exists
276
+ if event.present?
277
+ logger.debug "*** update event #{event.id}-#{event.key}:"
278
+ else
279
+ logger.debug "*** create event:"
280
+ event = Event.new
281
+ end
282
+
283
+ puts event_attribs.to_json
284
+
285
+ event.update_attributes!( event_attribs )
286
+
287
+ ### todo/fix: add prop
288
+
289
+ end # load_event_with_include_path
81
290
 
82
291
 
83
292
  def load_fixtures_from_string( event_key, text ) # load from string (e.g. passed in via web form)
@@ -92,46 +301,75 @@ class Reader
92
301
  end
93
302
 
94
303
  def load_fixtures_with_include_path( event_key, name, include_path ) # load from file system
304
+
95
305
  path = "#{include_path}/#{name}.txt"
96
306
 
97
307
  puts "*** parsing data '#{name}' (#{path})..."
308
+
309
+ SportDB.lang.lang = LangChecker.new.analyze( name, include_path )
98
310
 
99
311
  reader = LineReader.new( logger, path )
100
312
 
101
313
  load_fixtures_worker( event_key, reader )
102
314
 
103
- Prop.create!( key: "db.#{fixture_name_to_prop_key(name)}.version", value: "file.txt.#{File.mtime(path).strftime('%Y.%m.%d')}" )
315
+ ## fix add prop
316
+ ## Prop.create!( key: "db.#{fixture_name_to_prop_key(name)}.version", value: "file.txt.#{File.mtime(path).strftime('%Y.%m.%d')}" )
104
317
  end
105
318
 
106
- def load_fixtures_builtin( event_key, name ) # load from gem (built-in)
107
- path = "#{SportDB.data_path}/#{name}.txt"
319
+
320
+ def load_teams_with_include_path( name, include_path, more_values={} )
321
+ path = "#{include_path}/#{name}.txt"
108
322
 
109
323
  puts "*** parsing data '#{name}' (#{path})..."
110
324
 
111
- reader = LineReader.new( logger, path )
325
+ reader = ValuesReader.new( logger, path, more_values )
112
326
 
113
- load_fixtures_worker( event_key, reader )
327
+ load_teams_worker( reader )
114
328
 
115
- Prop.create!( key: "db.#{fixture_name_to_prop_key(name)}.version", value: "sport.txt.#{SportDB::VERSION}" )
116
- end
329
+ ## todo/fix: add prop
330
+ ## Prop.create!( key: "db.#{fixture_name_to_prop_key(name)}.version", value: "sport.txt.#{SportDB::VERSION}" )
331
+ end # load_teams_with_include_path
117
332
 
333
+ private
118
334
 
119
- def load_teams_builtin( name, more_values={} )
120
- path = "#{SportDB.data_path}/#{name}.txt"
335
+ include SportDB::FixtureHelpers
121
336
 
122
- puts "*** parsing data '#{name}' (#{path})..."
337
+ def load_leagues_worker( reader )
123
338
 
124
- reader = ValuesReader.new( logger, path, more_values )
339
+ reader.each_line do |attribs, values|
125
340
 
126
- load_teams_worker( reader )
127
-
128
- Prop.create!( key: "db.#{fixture_name_to_prop_key(name)}.version", value: "sport.txt.#{SportDB::VERSION}" )
129
- end
341
+ ## check optional values
342
+ values.each_with_index do |value, index|
343
+ if value =~ /^club$/ # club flag
344
+ attribs[ :club ] = true
345
+ elsif value =~ /^[a-z]{2}$/ ## assume two-letter country key e.g. at,de,mx,etc.
346
+ value_country = Country.find_by_key!( value )
347
+ attribs[ :country_id ] = value_country.id
348
+ else
349
+ ## todo: assume title2 ??
350
+ ## assume title2 if title2 is empty (not already in use)
351
+ ## and if it title2 contains at least two letter e.g. [a-zA-Z].*[a-zA-Z]
352
+ # issue warning: unknown type for value
353
+ logger.warn "unknown type for value >#{value}<"
354
+ end
355
+ end
356
+
357
+ rec = League.find_by_key( attribs[ :key ] )
358
+ if rec.present?
359
+ logger.debug "update League #{rec.id}-#{rec.key}:"
360
+ else
361
+ logger.debug "create League:"
362
+ rec = League.new
363
+ end
364
+
365
+ puts attribs.to_json
366
+
367
+ rec.update_attributes!( attribs )
130
368
 
369
+ end # each lines
131
370
 
132
- private
371
+ end # load_leagues_worker
133
372
 
134
- include SportDB::FixtureHelpers
135
373
 
136
374
  def load_teams_worker( reader )
137
375
 
@@ -141,8 +379,14 @@ private
141
379
  values.each_with_index do |value, index|
142
380
  if value =~ /^city:/ ## city:
143
381
  value_city_key = value[5..-1] ## cut off city: prefix
144
- value_city = City.find_by_key!( value_city_key )
145
- attribs[ :city_id ] = value_city.id
382
+ value_city = City.find_by_key( value_city_key )
383
+ if value_city.present?
384
+ attribs[ :city_id ] = value_city.id
385
+ else
386
+ ## todo/fix: add strict mode flag - fail w/ exit 1 in strict mode
387
+ logger.warn "city with key #{value_city_key} missing"
388
+ ## todo: log errors to db log???
389
+ end
146
390
  elsif value =~ /^[A-Z]{3}$/ ## assume three-letter code e.g. FCB, RBS, etc.
147
391
  attribs[ :code ] = value
148
392
  elsif value =~ /^[a-z]{2}$/ ## assume two-letter country key e.g. at,de,mx,etc.
@@ -151,15 +395,15 @@ private
151
395
  else
152
396
  ## todo: assume title2 ??
153
397
  # issue warning: unknown type for value
154
- puts "*** warning: unknown type for value >#{value}<"
398
+ logger.warn "unknown type for value >#{value}<"
155
399
  end
156
400
  end
157
401
 
158
402
  rec = Team.find_by_key( attribs[ :key ] )
159
403
  if rec.present?
160
- puts "*** update Team #{rec.id}-#{rec.key}:"
404
+ logger.debug "update Team #{rec.id}-#{rec.key}:"
161
405
  else
162
- puts "*** create Team:"
406
+ logger.debug "create Team:"
163
407
  rec = Team.new
164
408
  end
165
409
 
@@ -183,7 +427,7 @@ private
183
427
 
184
428
  @event = Event.find_by_key!( event_key )
185
429
 
186
- puts "Event #{@event.key} >#{@event.title}<"
430
+ logger.info "Event #{@event.key} >#{@event.title}<"
187
431
 
188
432
  @known_teams = @event.known_teams_table
189
433
 
@@ -193,14 +437,14 @@ private
193
437
 
194
438
 
195
439
  def parse_group( line )
196
- puts "parsing group line: >#{line}<"
440
+ logger.debug "parsing group line: >#{line}<"
197
441
 
198
442
  match_teams!( line )
199
443
  team_keys = find_teams!( line )
200
444
 
201
445
  title, pos = find_group_title_and_pos!( line )
202
446
 
203
- puts " line: >#{line}<"
447
+ logger.debug " line: >#{line}<"
204
448
 
205
449
  group_attribs = {
206
450
  title: title
@@ -208,9 +452,9 @@ private
208
452
 
209
453
  @group = Group.find_by_event_id_and_pos( @event.id, pos )
210
454
  if @group.present?
211
- puts "*** update group #{@group.id}:"
455
+ logger.debug "update group #{@group.id}:"
212
456
  else
213
- puts "*** create group:"
457
+ logger.debug "create group:"
214
458
  @group = Group.new
215
459
  group_attribs = group_attribs.merge( {
216
460
  event_id: @event.id,
@@ -226,13 +470,13 @@ private
226
470
  ## add new teams
227
471
  team_keys.each do |team_key|
228
472
  team = Team.find_by_key!( team_key )
229
- puts " adding team #{team.title} (#{team.code})"
473
+ logger.debug " adding team #{team.title} (#{team.code})"
230
474
  @group.teams << team
231
475
  end
232
476
  end
233
477
 
234
478
  def parse_round( line )
235
- puts "parsing round line: >#{line}<"
479
+ logger.debug "parsing round line: >#{line}<"
236
480
  pos = find_round_pos!( line )
237
481
 
238
482
  @knockout_flag = is_knockout_round?( line )
@@ -245,7 +489,7 @@ private
245
489
  @group = nil # reset group to no group
246
490
  end
247
491
 
248
- puts " line: >#{line}<"
492
+ logger.debug " line: >#{line}<"
249
493
 
250
494
  ## NB: dummy/placeholder start_at, end_at date
251
495
  ## replace/patch after adding all games for round
@@ -0,0 +1,28 @@
1
+
2
+ module SportDB
3
+
4
+ class Stats
5
+ include SportDB::Models
6
+
7
+ def tables
8
+ puts "Stats:"
9
+ puts " #{Event.count} events / #{Round.count} rounds / #{Group.count} groups"
10
+ puts " #{League.count} leagues / #{Season.count} seasons"
11
+ puts " #{Country.count} countries / #{Region.count} regions / #{City.count} cities"
12
+ puts " #{Team.count} teams"
13
+ puts " #{Game.count} games"
14
+ puts " #{Badge.count} badges"
15
+
16
+ ## todo: add tags / taggings from worlddb
17
+ end
18
+
19
+ def props
20
+ puts "Props:"
21
+ Prop.order( 'created_at asc' ).all.each do |prop|
22
+ puts " #{prop.key} / #{prop.value} || #{prop.created_at}"
23
+ end
24
+ end
25
+
26
+ end # class Stats
27
+
28
+ end # module SportDB