radiant-race_results-extension 1.4.3 → 1.4.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/app/controllers/admin/race_instances_controller.rb +1 -1
  2. data/app/controllers/race_instances_controller.rb +7 -1
  3. data/app/controllers/race_performances_controller.rb +8 -0
  4. data/app/models/race.rb +7 -0
  5. data/app/models/race_checkpoint.rb +27 -1
  6. data/app/models/race_checkpoint_time.rb +33 -5
  7. data/app/models/race_instance.rb +38 -9
  8. data/app/models/race_performance.rb +81 -3
  9. data/app/views/admin/races/_form.html.haml +1 -2
  10. data/app/views/race_clubs/show.html.haml +3 -3
  11. data/app/views/race_instances/_results_header.html.haml +10 -0
  12. data/app/views/race_instances/_splits_header.html.haml +9 -0
  13. data/app/views/race_instances/show.html.haml +17 -8
  14. data/app/views/race_instances/splits.html.haml +19 -14
  15. data/app/views/race_performances/_performance.html.haml +12 -2
  16. data/app/views/race_performances/_splits.html.haml +30 -8
  17. data/app/views/race_performances/show.html.haml +18 -76
  18. data/app/views/races/show.html.haml +2 -1
  19. data/config/locales/en.yml +20 -0
  20. data/config/routes.rb +1 -1
  21. data/db/migrate/20111103150827_mapping_routes.rb +22 -0
  22. data/db/migrate/20111115150827_finish_checkpoint.rb +8 -0
  23. data/lib/race_tags.rb +3 -3
  24. data/lib/radiant-race_results-extension.rb +1 -1
  25. data/public/images/race_results/sorts.png +0 -0
  26. data/public/javascripts/flot/API.txt +1201 -0
  27. data/public/javascripts/flot/FAQ.txt +76 -0
  28. data/public/javascripts/flot/LICENSE.txt +22 -0
  29. data/public/javascripts/flot/Makefile +9 -0
  30. data/public/javascripts/flot/NEWS.txt +508 -0
  31. data/public/javascripts/flot/PLUGINS.txt +137 -0
  32. data/public/javascripts/flot/README.txt +90 -0
  33. data/public/javascripts/flot/examples/ajax.html +143 -0
  34. data/public/javascripts/flot/examples/annotating.html +75 -0
  35. data/public/javascripts/flot/examples/arrow-down.gif +0 -0
  36. data/public/javascripts/flot/examples/arrow-left.gif +0 -0
  37. data/public/javascripts/flot/examples/arrow-right.gif +0 -0
  38. data/public/javascripts/flot/examples/arrow-up.gif +0 -0
  39. data/public/javascripts/flot/examples/basic.html +38 -0
  40. data/public/javascripts/flot/examples/data-eu-gdp-growth-1.json +4 -0
  41. data/public/javascripts/flot/examples/data-eu-gdp-growth-2.json +4 -0
  42. data/public/javascripts/flot/examples/data-eu-gdp-growth-3.json +4 -0
  43. data/public/javascripts/flot/examples/data-eu-gdp-growth-4.json +4 -0
  44. data/public/javascripts/flot/examples/data-eu-gdp-growth-5.json +4 -0
  45. data/public/javascripts/flot/examples/data-eu-gdp-growth.json +4 -0
  46. data/public/javascripts/flot/examples/data-japan-gdp-growth.json +4 -0
  47. data/public/javascripts/flot/examples/data-usa-gdp-growth.json +4 -0
  48. data/public/javascripts/flot/examples/graph-types.html +75 -0
  49. data/public/javascripts/flot/examples/hs-2004-27-a-large_web.jpg +0 -0
  50. data/public/javascripts/flot/examples/image.html +45 -0
  51. data/public/javascripts/flot/examples/index.html +44 -0
  52. data/public/javascripts/flot/examples/interacting-axes.html +97 -0
  53. data/public/javascripts/flot/examples/interacting.html +93 -0
  54. data/public/javascripts/flot/examples/layout.css +6 -0
  55. data/public/javascripts/flot/examples/multiple-axes.html +60 -0
  56. data/public/javascripts/flot/examples/navigate.html +118 -0
  57. data/public/javascripts/flot/examples/percentiles.html +57 -0
  58. data/public/javascripts/flot/examples/pie.html +756 -0
  59. data/public/javascripts/flot/examples/realtime.html +83 -0
  60. data/public/javascripts/flot/examples/resize.html +61 -0
  61. data/public/javascripts/flot/examples/selection.html +114 -0
  62. data/public/javascripts/flot/examples/setting-options.html +61 -0
  63. data/public/javascripts/flot/examples/stacking.html +77 -0
  64. data/public/javascripts/flot/examples/symbols.html +49 -0
  65. data/public/javascripts/flot/examples/thresholding.html +54 -0
  66. data/public/javascripts/flot/examples/time.html +71 -0
  67. data/public/javascripts/flot/examples/tracking.html +95 -0
  68. data/public/javascripts/flot/examples/turning-series.html +98 -0
  69. data/public/javascripts/flot/examples/visitors.html +90 -0
  70. data/public/javascripts/flot/examples/zooming.html +98 -0
  71. data/public/javascripts/flot/excanvas.js +1427 -0
  72. data/public/javascripts/flot/excanvas.min.js +1 -0
  73. data/public/javascripts/flot/jquery.colorhelpers.js +179 -0
  74. data/public/javascripts/flot/jquery.colorhelpers.min.js +1 -0
  75. data/public/javascripts/flot/jquery.flot.crosshair.js +167 -0
  76. data/public/javascripts/flot/jquery.flot.crosshair.min.js +1 -0
  77. data/public/javascripts/flot/jquery.flot.fillbetween.js +183 -0
  78. data/public/javascripts/flot/jquery.flot.fillbetween.min.js +1 -0
  79. data/public/javascripts/flot/jquery.flot.image.js +238 -0
  80. data/public/javascripts/flot/jquery.flot.image.min.js +1 -0
  81. data/public/javascripts/flot/jquery.flot.js +2599 -0
  82. data/public/javascripts/flot/jquery.flot.min.js +6 -0
  83. data/public/javascripts/flot/jquery.flot.navigate.js +336 -0
  84. data/public/javascripts/flot/jquery.flot.navigate.min.js +1 -0
  85. data/public/javascripts/flot/jquery.flot.pie.js +750 -0
  86. data/public/javascripts/flot/jquery.flot.pie.min.js +1 -0
  87. data/public/javascripts/flot/jquery.flot.resize.js +60 -0
  88. data/public/javascripts/flot/jquery.flot.resize.min.js +1 -0
  89. data/public/javascripts/flot/jquery.flot.selection.js +344 -0
  90. data/public/javascripts/flot/jquery.flot.selection.min.js +1 -0
  91. data/public/javascripts/flot/jquery.flot.stack.js +184 -0
  92. data/public/javascripts/flot/jquery.flot.stack.min.js +1 -0
  93. data/public/javascripts/flot/jquery.flot.symbol.js +70 -0
  94. data/public/javascripts/flot/jquery.flot.symbol.min.js +1 -0
  95. data/public/javascripts/flot/jquery.flot.threshold.js +103 -0
  96. data/public/javascripts/flot/jquery.flot.threshold.min.js +1 -0
  97. data/public/javascripts/flot/jquery.js +8316 -0
  98. data/public/javascripts/flot/jquery.min.js +23 -0
  99. data/public/javascripts/jquery.qtip.js +2675 -0
  100. data/public/javascripts/jquery.sparkline.js +1271 -0
  101. data/public/javascripts/races.js +245 -0
  102. data/public/stylesheets/sass/admin/races.sass +65 -70
  103. data/public/stylesheets/sass/jquery.flot.sass +416 -0
  104. data/public/stylesheets/sass/race_results.sass +38 -2
  105. data/radiant-race_results-extension.gemspec +1 -1
  106. metadata +95 -11
  107. data/public/javascripts/tablesorter.js +0 -3
@@ -1,6 +1,6 @@
1
1
  class Admin::RaceInstancesController < Admin::ResourceController
2
2
  paginate_models
3
-
3
+
4
4
  protected
5
5
 
6
6
  def continue_url(options)
@@ -4,10 +4,16 @@ class RaceInstancesController < SiteController
4
4
  before_filter :establish_context
5
5
 
6
6
  def show
7
-
7
+ expires_in 1.month, :private => false, :public => true
8
+ respond_to do |format|
9
+ format.html {}
10
+ format.csv {}
11
+ format.json { render :json => @performances.completed }
12
+ end
8
13
  end
9
14
 
10
15
  def splits
16
+ expires_in 1.month, :private => false, :public => true
11
17
  @checkpoints = @instance.checkpoints
12
18
  @splits = @instance.assembled_checkpoint_times
13
19
  end
@@ -5,6 +5,14 @@ class RacePerformancesController < SiteController
5
5
 
6
6
  def show
7
7
  expires_in 1.month, :private => false, :public => true
8
+ respond_to do |format|
9
+ format.html {
10
+ render
11
+ }
12
+ format.json {
13
+ render :json => @performance.neighbourhood(10).to_json(:vs => @performance)
14
+ }
15
+ end
8
16
  end
9
17
 
10
18
  private
data/app/models/race.rb CHANGED
@@ -17,6 +17,8 @@ class Race < ActiveRecord::Base
17
17
  belongs_to :map_asset, :class_name => 'Asset'
18
18
  accepts_nested_attributes_for :map_asset, :allow_destroy => true
19
19
 
20
+ before_save :ensure_finish_checkpoint
21
+
20
22
  validates_presence_of :name, :slug
21
23
  validates_uniqueness_of :name, :slug
22
24
  validates_length_of :slug, :maximum => 100, :message => '%{count}-character limit'
@@ -62,6 +64,11 @@ class Race < ActiveRecord::Base
62
64
  checkpoints.at(checkpoints.index(cp) + 1) if checkpoints.include?(cp) and checkpoints.last != cp
63
65
  end
64
66
 
67
+ protected
68
+
69
+ def ensure_finish_checkpoint
70
+ self.checkpoints << RaceCheckpoint.new(:name => "Finish") unless checkpoints.find_by_name('Finish')
71
+ end
65
72
 
66
73
  end
67
74
 
@@ -1,5 +1,5 @@
1
1
  class RaceCheckpoint < ActiveRecord::Base
2
-
2
+
3
3
  has_site if respond_to? :has_site
4
4
  belongs_to :created_by, :class_name => 'User'
5
5
  belongs_to :updated_by, :class_name => 'User'
@@ -26,6 +26,32 @@ class RaceCheckpoint < ActiveRecord::Base
26
26
  def self.find_by_normalized_name(name)
27
27
  find_by_name(name.gsub(/\s+/, "_").downcase)
28
28
  end
29
+
30
+ def fastest_time(instance)
31
+ self.times.in(instance).first.time_in_seconds
32
+ end
33
+
34
+ def fastest_leg(instance)
35
+ self.times.in(instance).by_interval.first.interval_in_seconds
36
+ end
37
+
38
+ def median_time(instance)
39
+ count = self.times.in(instance).count
40
+ if count && count !=0
41
+ @median = self.times.in(instance).by_time.single(count/2).first.time_in_seconds
42
+ else
43
+ 0
44
+ end
45
+ end
46
+
47
+ def median_leg(instance)
48
+ count = self.times.in(instance).count
49
+ if count && count !=0
50
+ @median = self.times.in(instance).by_interval.single(count/2).first.interval_in_seconds
51
+ else
52
+ 0
53
+ end
54
+ end
29
55
 
30
56
  end
31
57
 
@@ -15,6 +15,18 @@ class RaceCheckpointTime < ActiveRecord::Base
15
15
  default_scope :include => [:race_performance, :race_checkpoint]
16
16
  before_save :calculate_interval # position would be nice too but we may not have imported all the data at this stage
17
17
 
18
+ named_scope :with_context, :include => [:race_performance, :race_checkpoint]
19
+ named_scope :by_time, { :order => 'race_checkpoint_times.elapsed_time' }
20
+ named_scope :by_interval, { :order => 'race_checkpoint_times.interval' }
21
+
22
+
23
+ named_scope :single, lambda { |offset|
24
+ {
25
+ :limit => 1,
26
+ :offset => offset.to_i
27
+ }
28
+ }
29
+
18
30
  named_scope :in, lambda {|instance|
19
31
  {
20
32
  :joins => "INNER JOIN race_performances as performances ON race_checkpoint_times.race_performance_id = performances.id",
@@ -22,8 +34,6 @@ class RaceCheckpointTime < ActiveRecord::Base
22
34
  }
23
35
  }
24
36
 
25
- named_scope :with_context, :include => [:race_performance, :race_checkpoint]
26
-
27
37
  named_scope :at_checkpoint, lambda {|checkpoint|
28
38
  {
29
39
  :conditions => ["race_checkpoint_id = ?", checkpoint.id]
@@ -42,6 +52,7 @@ class RaceCheckpointTime < ActiveRecord::Base
42
52
  }
43
53
  }
44
54
 
55
+
45
56
  def to_s
46
57
  time = read_attribute(:elapsed_time)
47
58
  if time && time != 0
@@ -55,6 +66,10 @@ class RaceCheckpointTime < ActiveRecord::Base
55
66
  faster = self.class.in(race_instance).at_checkpoint(checkpoint).ahead_of(elapsed_time.seconds)
56
67
  faster.length + 1
57
68
  end
69
+
70
+ def inverted_position
71
+ race_instance.total_runners - position
72
+ end
58
73
 
59
74
  def leg_position
60
75
  if !previous
@@ -65,6 +80,10 @@ class RaceCheckpointTime < ActiveRecord::Base
65
80
  end
66
81
  end
67
82
 
83
+ def inverted_leg_position
84
+ race_instance.total_runners - leg_position
85
+ end
86
+
68
87
  def elapsed_time
69
88
  if s = read_attribute(:elapsed_time)
70
89
  s.to_timecode
@@ -73,6 +92,10 @@ class RaceCheckpointTime < ActiveRecord::Base
73
92
  end
74
93
  end
75
94
 
95
+ def time_in_seconds
96
+ read_attribute(:elapsed_time)
97
+ end
98
+
76
99
  def elapsed_time=(time)
77
100
  write_attribute(:elapsed_time, time.seconds) # numbers will pass through unchanged. strings will be timecode-parsed
78
101
  end
@@ -85,6 +108,10 @@ class RaceCheckpointTime < ActiveRecord::Base
85
108
  end
86
109
  end
87
110
 
111
+ def interval_in_seconds
112
+ read_attribute(:interval)
113
+ end
114
+
88
115
  def previous
89
116
  performance.time_at(checkpoint.previous) if checkpoint.previous
90
117
  end
@@ -104,9 +131,10 @@ class RaceCheckpointTime < ActiveRecord::Base
104
131
  private
105
132
 
106
133
  def calculate_interval
107
- if previous
108
- write_attribute(:interval, (elapsed_time - previous.elapsed_time).seconds)
109
- end
134
+ previous_time = previous.elapsed_time if previous
135
+ previous_time ||= 0
136
+ write_attribute(:interval, (elapsed_time - previous_time).seconds)
137
+ save if changed?
110
138
  end
111
139
 
112
140
  end
@@ -1,7 +1,8 @@
1
1
  require 'csv'
2
2
 
3
3
  class RaceInstance < ActiveRecord::Base
4
-
4
+ attr_accessor :checkpoint_times, :performances_count
5
+
5
6
  has_site if respond_to? :has_site
6
7
  belongs_to :created_by, :class_name => 'User'
7
8
  belongs_to :updated_by, :class_name => 'User'
@@ -80,12 +81,14 @@ class RaceInstance < ActiveRecord::Base
80
81
  end
81
82
 
82
83
  def assembled_checkpoint_times
83
- checkpoint_times = {}
84
- RaceCheckpointTime.in(self).with_context.each do |cpt|
85
- checkpoint_times[cpt.performance.id] ||= {}
86
- checkpoint_times[cpt.performance.id][cpt.checkpoint.id] = cpt.elapsed_time
84
+ unless @checkpoint_times
85
+ @checkpoint_times = {}
86
+ RaceCheckpointTime.in(self).with_context.each do |cpt|
87
+ @checkpoint_times[cpt.performance.id] ||= {}
88
+ @checkpoint_times[cpt.performance.id][cpt.checkpoint.id] = cpt.elapsed_time
89
+ end
87
90
  end
88
- checkpoint_times
91
+ @checkpoint_times
89
92
  end
90
93
 
91
94
  def performance_by(competitor)
@@ -123,14 +126,40 @@ class RaceInstance < ActiveRecord::Base
123
126
  categories.include?(category)
124
127
  end
125
128
 
129
+ def total_runners
130
+ @performances_count ||= performances.count
131
+ end
132
+
133
+ def fastest_checkpoint_times
134
+ @leading ||= self.checkpoints.map{|cp| cp.fastest_time(self) }
135
+ end
136
+
137
+ def fastest_checkpoint_legs
138
+ @fastest_legs ||= self.checkpoints.map{|cp| cp.fastest_leg(self) }
139
+ end
140
+
141
+ def median_checkpoint_legs
142
+ @median_legs ||= self.checkpoints.map{|cp| cp.median_leg(self) }
143
+ end
144
+
145
+ def median_checkpoint_times
146
+ carry = 0
147
+ @medians ||= self.checkpoints.map{|cp| cp.median_time(self) }
148
+ end
149
+
150
+ def as_json(options={})
151
+ json = {
152
+ :name => full_name,
153
+ :performances => performances.map(&:as_json)
154
+ }
155
+ end
156
+
126
157
  protected
127
158
 
128
159
  def process_results_file
129
160
  if csv_data = read_results_file
130
161
  headers = csv_data.shift.map(&:to_s)
131
- Rails.logger.warn "!!! headers: #{headers.inspect}"
132
162
  checkpoints = headers.map{|h| race.checkpoints.find_by_name(h.strip) }.compact
133
- Rails.logger.warn "!!! checkpoints: #{checkpoints.inspect}"
134
163
  race_data = csv_data.map {|row| row.map {|cell| cell.to_s } }.map {|row| Hash[*headers.zip(row).flatten] } # build AoA and then hash the second level
135
164
  RaceInstance.transaction do
136
165
  performances.destroy_all
@@ -153,7 +182,7 @@ protected
153
182
  :elapsed_time => runner.delete('elapsed_time'),
154
183
  :status_id => status.id
155
184
  })
156
-
185
+
157
186
  checkpoints.each do |cp|
158
187
  value = runner[normalize(cp.name)]
159
188
  cpt = performance.checkpoint_times.create!(:race_checkpoint_id => cp.id, :elapsed_time => value) unless value.blank?
@@ -1,3 +1,5 @@
1
+ require 'enumerator'
2
+
1
3
  class RacePerformance < ActiveRecord::Base
2
4
 
3
5
  has_site if respond_to? :has_site
@@ -9,6 +11,7 @@ class RacePerformance < ActiveRecord::Base
9
11
  has_many :checkpoint_times, :class_name => 'RaceCheckpointTime', :dependent => :destroy
10
12
 
11
13
  delegate :name, :reader, :club, :to => :competitor
14
+ delegate :race, :to => :race_instance
12
15
 
13
16
  before_validation_on_create :times_from_checkpoints
14
17
  validates_presence_of :race_competitor_id, :race_instance_id
@@ -75,6 +78,13 @@ class RacePerformance < ActiveRecord::Base
75
78
  :conditions => ["elapsed_time IS NOT NULL AND elapsed_time > 0 AND elapsed_time < ?", seconds]
76
79
  }
77
80
  }
81
+
82
+ named_scope :finishing_between, lambda { |bottom, top|
83
+ bottom, top = [top, bottom] if bottom > top
84
+ {
85
+ :conditions => ["position > ? AND position < ?", bottom, top]
86
+ }
87
+ }
78
88
 
79
89
  named_scope :completed, {
80
90
  :conditions => 'status_id >= 100'
@@ -122,6 +132,18 @@ class RacePerformance < ActiveRecord::Base
122
132
  race_instance.performances.eligible_for_category(cat).quicker_than(time_in_seconds).count + 1
123
133
  end
124
134
 
135
+ def sparkline_positions
136
+ checkpoint_times.map(&:inverted_leg_position)
137
+ end
138
+
139
+ def neighbourhood(spread=3)
140
+ race_instance.performances.finishing_between(self.position.to_i - spread, self.position.to_i + spread)
141
+ end
142
+
143
+ def neighbours(spread=3)
144
+ neighbourhood(spread) - [self]
145
+ end
146
+
125
147
  def time_in_seconds
126
148
  read_attribute(:elapsed_time)
127
149
  end
@@ -143,6 +165,20 @@ class RacePerformance < ActiveRecord::Base
143
165
  cpt.first if cpt.any?
144
166
  end
145
167
 
168
+ def splits
169
+ self.checkpoint_times.each_with_object({}) do |cpt, hsh|
170
+ hsh[cpt.race_checkpoint_id] = cpt.elapsed_time
171
+ end
172
+ end
173
+
174
+ def split_times
175
+ @split_times ||= race_instance.checkpoints.map{ |cp| self.time_at(cp) }
176
+ end
177
+
178
+ def split_seconds
179
+ @split_seconds ||= split_times.map{ |st| st.time_in_seconds if st }
180
+ end
181
+
146
182
  def status
147
183
  RacePerformanceStatus.find(self.status_id)
148
184
  end
@@ -152,13 +188,46 @@ class RacePerformance < ActiveRecord::Base
152
188
  def finished?
153
189
  status == RacePerformanceStatus["Finished"]
154
190
  end
155
-
156
-
157
191
 
158
192
  def prized?
159
193
  true if prizes.any?
160
194
  end
161
-
195
+
196
+ def club_name
197
+ self.club ? self.club.name : 'unattached'
198
+ end
199
+
200
+ def as_json(options={})
201
+ json = {
202
+ :id => self.id,
203
+ :pos => self.position,
204
+ :name => self.name,
205
+ }
206
+ splits = []
207
+ baseline = case options[:vs]
208
+ when 'median'
209
+ race_instance.median_checkpoint_times
210
+ when 'leader'
211
+ race_instance.winning_performance.split_seconds
212
+ when RacePerformance
213
+ options[:vs].split_seconds.dup
214
+ else
215
+ nil
216
+ end
217
+ if baseline
218
+ splits = self.split_seconds.map{ |cpt|
219
+ base = baseline.shift # we need to step through the baseline even if this cp is missing
220
+ base - cpt if cpt && base
221
+ }
222
+ else
223
+ splits = self.split_seconds
224
+ end
225
+ intervals = race_instance.fastest_checkpoint_legs
226
+ carry = 0
227
+ json[:splits] = splits.map{|t| [carry += intervals.shift, t]}
228
+ json
229
+ end
230
+
162
231
  protected
163
232
 
164
233
  def times_from_checkpoints
@@ -171,6 +240,15 @@ protected
171
240
  position(true)
172
241
  prizes(true)
173
242
  end
243
+
244
+ def record_finish_checkpoint_time
245
+ unless self.elapsed_time.blank?
246
+ cp = self.race.checkpoints.find_by_name('Finish')
247
+ cpt = self.checkpoint_times.find_or_create_by_race_checkpoint_id(cp.id)
248
+ cpt.elapsed_time = self.elapsed_time
249
+ cpt.save if cpt.changed?
250
+ end
251
+ end
174
252
 
175
253
  end
176
254
 
@@ -12,8 +12,7 @@
12
12
  %br
13
13
  = @race.errors.full_messages
14
14
 
15
- .container_12
16
-
15
+ #form_container
17
16
  - render_region :form do |form|
18
17
  - form.edit_name do
19
18
  %p.title
@@ -14,11 +14,11 @@
14
14
 
15
15
  - content_for :breadhead do
16
16
  = link_to "Races", races_url
17
- = t('separator')
17
+ = t('race_results_extension.separator')
18
18
  = link_to @instance.race.name, race_url(@instance.race), :class => 'breadhead'
19
- = t('separator')
19
+ = t('race_results_extension.separator')
20
20
  = link_to @instance.name, race_instance_url(@instance), :class => 'breadhead'
21
- = t('separator')
21
+ = t('race_results_extension.separator')
22
22
 
23
23
  - content_for :title do
24
24
  = @instance.full_name
@@ -0,0 +1,10 @@
1
+ %thead
2
+ %tr
3
+ - if @instance.splits_available?
4
+ %th
5
+ %th.pos= link_to t('activerecord.attributes.race_performance.position'), '#'
6
+ %th.name= link_to t('activerecord.attributes.race_performance.name'), '#'
7
+ %th.club= link_to t('activerecord.attributes.race_performance.club'), '#'
8
+ %th.cat= link_to t('activerecord.attributes.race_performance.category'), '#'
9
+ %th.time= link_to t('activerecord.attributes.race_performance.time'), '#'
10
+ %th.prizes