radiant-race_results-extension 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg/*
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Race Results
2
2
 
3
- This extension was writting to process and display the results of fell races. It should work for any timed event, including those with intermediate checkpoints. I don't expect it to be of interest to anyone else so I haven't bothered much with documentation or convenience: if you think you'd like to use it please let me know and I'll write it up properly.
3
+ This extension was writting to process and display the results of races. It's written for fell races but it should work for any timed event, including those with intermediate checkpoints and the usual paraphernalia of clubs and categories. With a bit of minor updating it will also work for scored events, mountain marathons, relays and many other kinds of competition.
4
+
5
+ I don't expect it to be of much interest to other developers so I haven't given it much documentation: if you think you'd like to use it please let me know and I'll write it up properly.
4
6
 
5
7
  # Author
6
8
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.1
1
+ 1.3.0
@@ -1,9 +1,21 @@
1
1
  class RaceInstancesController < SiteController
2
2
  radiant_layout { |controller| Radiant::Config['races.layout'] }
3
3
  no_login_required
4
- before_filter :get_race, :only => :show
4
+ before_filter :establish_context
5
5
 
6
6
  def show
7
+
8
+ end
9
+
10
+ def splits
11
+ @checkpoints = @instance.checkpoints
12
+ @splits = @instance.assembled_checkpoint_times
13
+ end
14
+
15
+ private
16
+
17
+ def establish_context
18
+ @race = Race.find_by_slug(params[:race_slug])
7
19
  @instance = @race.instances.find_by_slug(params[:slug])
8
20
  @performances = @instance.performances
9
21
  if params[:club] && @club = RaceClub.find(params[:club])
@@ -12,13 +24,6 @@ class RaceInstancesController < SiteController
12
24
  if params[:cat] && @category = @instance.categories.find_by_name(params[:cat])
13
25
  @performances = @performances.eligible_for_category(@category)
14
26
  end
15
- expires_in 1.month, :private => false, :public => true
16
- end
17
-
18
- private
19
-
20
- def get_race
21
- @race = Race.find_by_slug(params[:race_slug])
22
27
  end
23
28
 
24
29
  end
@@ -1,17 +1,18 @@
1
1
  class RacePerformancesController < SiteController
2
2
  radiant_layout { |controller| Radiant::Config['races.layout'] }
3
3
  no_login_required
4
- before_filter :get_race, :only => :show
4
+ before_filter :establish_context, :only => :show
5
5
 
6
6
  def show
7
7
  expires_in 1.month, :private => false, :public => true
8
- @performance = @race.performances.find(params[:id])
9
8
  end
10
9
 
11
10
  private
12
11
 
13
- def get_race
14
- @race = Race.find(params[:race_id])
12
+ def establish_context
13
+ @race = Race.find_by_slug(params[:race_slug])
14
+ @instance = @race.instances.find_by_slug(params[:slug])
15
+ @performance = @instance.performances.find(params[:id])
15
16
  end
16
17
 
17
18
  end
data/app/models/race.rb CHANGED
@@ -55,11 +55,11 @@ class Race < ActiveRecord::Base
55
55
  end
56
56
 
57
57
  def checkpoint_before(cp)
58
- checkpoints.at(checkpoints.index(cp) - 1) if checkpoints.contain?(cp) and checkpoints.first != cp
58
+ checkpoints.at(checkpoints.index(cp) - 1) if checkpoints.include?(cp) and checkpoints.first != cp
59
59
  end
60
60
 
61
61
  def checkpoint_after(cp)
62
- checkpoints.at(checkpoints.index(cp) + 1) if checkpoints.contain?(cp) and checkpoints.last != cp
62
+ checkpoints.at(checkpoints.index(cp) + 1) if checkpoints.include?(cp) and checkpoints.last != cp
63
63
  end
64
64
 
65
65
 
@@ -3,33 +3,62 @@ class RaceCheckpointTime < ActiveRecord::Base
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'
6
- belongs_to :performance, :class_name => 'RacePerformance'
7
- belongs_to :checkpoint, :class_name => 'RaceCheckpoint'
8
- # delegate :race_instance, :category, :competitor, :club, :to => :performance
6
+ belongs_to :race_performance
7
+ belongs_to :race_checkpoint
8
+ delegate :race_instance, :category, :competitor, :club, :to => :race_performance
9
+ delegate :name, :location, :description, :to => :race_checkpoint
10
+ validates_presence_of :race_checkpoint, :race_performance
9
11
 
10
- # validates_presence_of :checkpoint, :performance
12
+ alias :performance :race_performance
13
+ alias :checkpoint :race_checkpoint
11
14
 
15
+ default_scope :include => [:race_performance, :race_checkpoint]
16
+ before_save :calculate_interval # position would be nice too but we may not have imported all the data at this stage
17
+
18
+ named_scope :in, lambda {|instance|
19
+ {
20
+ :joins => "INNER JOIN race_performances as performances ON race_checkpoint_times.race_performance_id = performances.id",
21
+ :conditions => ["performances.race_instance_id = ?", instance.id]
22
+ }
23
+ }
24
+
25
+ named_scope :with_context, :include => [:race_performance, :race_checkpoint]
26
+
12
27
  named_scope :at_checkpoint, lambda {|checkpoint|
13
28
  {
14
29
  :conditions => ["race_checkpoint_id = ?", checkpoint.id]
15
30
  }
16
31
  }
17
32
 
18
- named_scope :time_in, lambda {|instance|
33
+ named_scope :ahead_of, lambda {|duration|
19
34
  {
20
- :joins => "INNER JOIN race_performances as performances ON race_checkpoint_times.race_performance_id = performances.id",
21
- :conditions => ["performances.race_instance_id = ?", instance.id]
35
+ :conditions => ["race_checkpoint_times.elapsed_time IS NOT NULL AND race_checkpoint_times.elapsed_time > 0 AND race_checkpoint_times.elapsed_time < ?", duration]
22
36
  }
23
37
  }
24
-
38
+
25
39
  named_scope :quicker_than, lambda {|duration|
26
40
  {
27
- :conditions => ["elapsed_time < ?", duration]
41
+ :conditions => ["race_checkpoint_times.interval IS NOT NULL AND race_checkpoint_times.interval > 0 AND race_checkpoint_times.interval < ?", duration]
28
42
  }
29
43
  }
44
+
45
+ def to_s
46
+ time = read_attribute(:elapsed_time)
47
+ time.to_timecode if time && time != 0
48
+ end
30
49
 
31
50
  def position
32
- checkpoint.times.in(race_instance).quicker_than(duration).count + 1
51
+ faster = self.class.in(race_instance).at_checkpoint(checkpoint).ahead_of(elapsed_time.seconds)
52
+ faster.length + 1
53
+ end
54
+
55
+ def leg_position
56
+ if !previous
57
+ position
58
+ elsif interval && interval.seconds > 0
59
+ faster = self.class.in(race_instance).at_checkpoint(checkpoint).quicker_than(interval.seconds)
60
+ faster.length + 1
61
+ end
33
62
  end
34
63
 
35
64
  def elapsed_time
@@ -44,8 +73,16 @@ class RaceCheckpointTime < ActiveRecord::Base
44
73
  write_attribute(:elapsed_time, time.seconds) # numbers will pass through unchanged. strings will be timecode-parsed
45
74
  end
46
75
 
76
+ def interval
77
+ if s = read_attribute(:interval)
78
+ s.to_timecode
79
+ else
80
+ ""
81
+ end
82
+ end
83
+
47
84
  def previous
48
- performance.checkpoint_times.at_checkpoint(checkpoint.previous) if checkpoint.previous
85
+ performance.time_at(checkpoint.previous) if checkpoint.previous
49
86
  end
50
87
 
51
88
  def previous_position
@@ -57,7 +94,15 @@ class RaceCheckpointTime < ActiveRecord::Base
57
94
  end
58
95
 
59
96
  def gain
60
- sprintf('+%d', position - previous_position)
97
+ position - previous_position
98
+ end
99
+
100
+ private
101
+
102
+ def calculate_interval
103
+ if previous
104
+ write_attribute(:interval, (elapsed_time - previous.elapsed_time).seconds)
105
+ end
61
106
  end
62
107
 
63
108
  end
@@ -74,7 +74,16 @@ class RaceInstance < ActiveRecord::Base
74
74
  # we will need to accommodate races whose checkpoints are different with each instance, but for now:
75
75
  race.checkpoints
76
76
  end
77
-
77
+
78
+ def assembled_checkpoint_times
79
+ checkpoint_times = {}
80
+ RaceCheckpointTime.in(self).with_context.each do |cpt|
81
+ checkpoint_times[cpt.performance.id] ||= {}
82
+ checkpoint_times[cpt.performance.id][cpt.checkpoint.id] = cpt.elapsed_time
83
+ end
84
+ checkpoint_times
85
+ end
86
+
78
87
  def performance_by(competitor)
79
88
 
80
89
  end
@@ -115,6 +124,7 @@ protected
115
124
  def process_results_file
116
125
  if csv_data = read_results_file
117
126
  headers = csv_data.shift.map(&:to_s)
127
+ checkpoints = headers.map{|h| race.checkpoints.find_by_name(h) }.compact
118
128
  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
119
129
  RaceInstance.transaction do
120
130
  performances.destroy_all
@@ -128,8 +138,9 @@ protected
128
138
  competitor = RaceCompetitor.find_or_create_by_name_and_club(runner.delete('name'), club)
129
139
  category = RaceCategory.find_or_create_by_normalized_name(runner.delete('category'))
130
140
  competitor.update_attribute(:gender, category.gender) unless competitor.gender
131
- status = RacePerformanceStatus.from_time(runner['elapsed_time'])
141
+ status = RacePerformanceStatus.from_pos_or_time(runner['position'], runner['elapsed_time'])
132
142
  performance = self.performances.create!({
143
+ :number => runner.delete('number'),
133
144
  :position => runner.delete('position'),
134
145
  :competitor => competitor,
135
146
  :category => category,
@@ -137,11 +148,9 @@ protected
137
148
  :status_id => status.id
138
149
  })
139
150
 
140
- headers.each do |key|
141
- value = runner[normalize(key)]
142
- if value && value.looks_like_duration? && cp = race.checkpoints.find_by_name(key)
143
- performance.checkpoint_times.create(:race_checkpoint_id => cp, :elapsed_time => value)
144
- end
151
+ checkpoints.each do |cp|
152
+ value = runner[normalize(cp.name)]
153
+ cpt = performance.checkpoint_times.create!(:race_checkpoint_id => cp.id, :elapsed_time => value || 0)
145
154
  end
146
155
  end
147
156
  end
@@ -155,6 +164,7 @@ protected
155
164
  end
156
165
 
157
166
  @@field_aliases = {
167
+ 'no' => 'number',
158
168
  'cat' => 'category',
159
169
  'time' => 'elapsed_time',
160
170
  'finish' => 'elapsed_time',
@@ -6,7 +6,7 @@ class RacePerformance < ActiveRecord::Base
6
6
  belongs_to :race_instance
7
7
  belongs_to :race_competitor
8
8
  belongs_to :race_category
9
- has_many :checkpoint_times, :class_name => 'RaceCheckpointTime'
9
+ has_many :checkpoint_times, :class_name => 'RaceCheckpointTime', :dependent => :destroy
10
10
 
11
11
  delegate :name, :reader, :club, :to => :competitor
12
12
 
@@ -139,9 +139,10 @@ class RacePerformance < ActiveRecord::Base
139
139
  end
140
140
 
141
141
  def time_at(checkpoint)
142
- checkpoint_times.at_checkpoint(checkpoint).first
142
+ cpt = checkpoint_times.at_checkpoint(checkpoint)
143
+ cpt.first if cpt.any?
143
144
  end
144
-
145
+
145
146
  def status
146
147
  RacePerformanceStatus.find(self.status_id)
147
148
  end
@@ -36,8 +36,17 @@ class RacePerformanceStatus
36
36
  @@statuses.dup
37
37
  end
38
38
 
39
- def self.from_time(time=nil)
40
- time ||= ""
39
+ def self.from_pos_or_time(pos="",time="")
40
+ if self[pos]
41
+ self[pos]
42
+ elsif pos.looks_like_number?
43
+ self["Finished"]
44
+ else
45
+ self.from_time(time)
46
+ end
47
+ end
48
+
49
+ def self.from_time(time="")
41
50
  if time.seconds > 0 # String.seconds returns 0 for a string that doesn't parse. See DurationExtensions for method.
42
51
  self["Finished"]
43
52
  elsif self[time]
@@ -49,10 +58,10 @@ class RacePerformanceStatus
49
58
 
50
59
  @@statuses = [
51
60
  RacePerformanceStatus.new(:id => 0, :name => '?', :aliases => ['unknown'] ),
52
- RacePerformanceStatus.new(:id => 10, :name => 'Ret', :aliases => ['retired', 'r']),
53
- RacePerformanceStatus.new(:id => 20, :name => 'DNF', :aliases => ['did not finish', 'x']),
54
- RacePerformanceStatus.new(:id => 30, :name => 'TO', :aliases => ['timed out', 't']),
55
- RacePerformanceStatus.new(:id => 50, :name => 'Dsq', :aliases => ['disqualified', 'disq', 'd']),
61
+ RacePerformanceStatus.new(:id => 10, :name => 'Ret', :aliases => ['retired', 'r', 'ret']),
62
+ RacePerformanceStatus.new(:id => 20, :name => 'DNF', :aliases => ['did not finish', 'x', 'dnf']),
63
+ RacePerformanceStatus.new(:id => 30, :name => 'TO', :aliases => ['timed out', 't', 'to']),
64
+ RacePerformanceStatus.new(:id => 50, :name => 'Dsq', :aliases => ['disqualified', 'disq', 'd', 'dsq']),
56
65
  RacePerformanceStatus.new(:id => 100, :name => 'Finished')
57
66
  ]
58
67
  end
@@ -0,0 +1,31 @@
1
+ - splitting ||= false
2
+ - results_url = race_instance_url(@race, @instance)
3
+ - splits_url = race_splits_url(@race, @instance)
4
+
5
+ %p.download
6
+ =link_to "Download data", {:format => 'csv'}, {:class => 'csv'}
7
+
8
+ %p.subset
9
+ - if @club || @category
10
+ Showing only
11
+ - if @club
12
+ - results_url = race_club_url(@race, @instance, @club)
13
+ - splits_url = race_club_splits_url(@race, @instance, @club)
14
+ %strong
15
+ = @club.name
16
+ - if @category
17
+ - results_url = race_category_url(@race, @instance, @category)
18
+ - splits_url = race_category_splits_url(@race, @instance, @category)
19
+ %strong
20
+ = @category.name
21
+ = link_to "(Show all results)", splitting ? race_splits_url(@race, @instance) : race_instance_url(@race, @instance)
22
+
23
+ - else
24
+ Click on a club or category to see only those results.
25
+
26
+ %br
27
+ View as
28
+ - if splitting
29
+ = link_to_unless_current "simple results", results_url
30
+ - else
31
+ = link_to_unless_current "splits", splits_url
@@ -0,0 +1,2 @@
1
+ Pos, Name, Cat, Club, Time
2
+ <%= render :partial => "race_performances/performance", :collection => @performances.completed %>
@@ -2,25 +2,18 @@
2
2
 
3
3
  - content_for :results do
4
4
  - if @instance.has_results?
5
- - if @club || @category
6
- %p.subset
7
- Showing only
8
- - if @club
9
- %strong
10
- = @club.name
11
- - if @category
12
- %strong
13
- = @category.name
14
- %br
15
- = link_to "Show all results.", race_instance_url(@race, @instance)
5
+ = render :partial => 'summary'
16
6
 
17
- %table.results
7
+ %table.ranking
18
8
  %thead
19
- = render :partial => 'race_performances/headings'
9
+ %tr
10
+ - %w{Pos Name Club Cat Time}.each do |h|
11
+ %th{:class => h.downcase}
12
+ =h
13
+ %th.prizes
20
14
  %tbody
21
15
  = render :partial => 'race_performances/performance', :collection => @performances.completed
22
16
  = render :partial => 'race_performances/performance', :collection => @performances.incomplete, :locals => {:trclass => 'unfinished'}
23
-
24
17
  - else
25
18
  %p.noresults
26
19
  Not available yet.
@@ -31,8 +24,13 @@
31
24
  = link_to @instance.race.name, race_url(@instance.race), :class => 'breadhead'
32
25
 
33
26
  - content_for :title do
34
- = @instance.full_name
35
-
27
+ - if @club
28
+ = "#{@instance.full_name}: #{@club.name}"
29
+ - elsif @category
30
+ = "#{@instance.full_name}: #{@category.name}"
31
+ - else
32
+ = @instance.full_name
33
+
36
34
  - content_for :introduction do
37
35
  - if @instance.past?
38
36
  = @instance.filter.filter(@instance.report)
@@ -0,0 +1,2 @@
1
+ Pos, Name, <%= @instance.race.checkpoints.map(&:name).join(', ') %>, Finish
2
+ <%= render :partial => "race_performances/splits", :collection => @performances.completed %>
@@ -0,0 +1,61 @@
1
+ = render :partial => 'races/standard_parts'
2
+
3
+ - content_for :results do
4
+ - if @instance.has_results?
5
+ = render :partial => 'summary', :locals => {:splitting => true}
6
+ %table.splits
7
+ %thead
8
+ %tr
9
+ %th.pos
10
+ pos
11
+ %th.runner
12
+ name
13
+ - @instance.race.checkpoints.each do |cp|
14
+ %th.cpt
15
+ =cp.name
16
+ %th.time
17
+ Finish
18
+ %tbody
19
+ = render :partial => 'race_performances/splits', :collection => @performances.completed
20
+
21
+ - else
22
+ %p.noresults
23
+ Not available yet.
24
+
25
+ - content_for :breadhead do
26
+ = link_to "Races", races_url, :class => 'breadhead'
27
+ = t('separator')
28
+ = link_to @instance.race.name, race_url(@instance.race), :class => 'breadhead'
29
+ = t('separator')
30
+ splits
31
+
32
+ - content_for :title do
33
+ - if @club
34
+ = "#{@instance.full_name}: #{@club.name} splits"
35
+ - elsif @category
36
+ = "#{@instance.full_name}: #{@category.name} splits"
37
+ - else
38
+ = "#{@instance.full_name}: splits"
39
+
40
+ - content_for :introduction do
41
+ - if @instance.past?
42
+ = @instance.filter.filter(@instance.report)
43
+ - else
44
+ = @instance.filter.filter(@instance.notes)
45
+
46
+ - content_for :see_also do
47
+ .see_also
48
+ %p
49
+ - if others = @race.instances.with_results.select{|i| i != @instance}
50
+ More
51
+ = @race.name
52
+ results:
53
+ = others.map { |instance| link_to instance.name, race_instance_url(@race, instance) }.join(', ') + '.'
54
+ %br
55
+ More races:
56
+ = Race.except(@race).map {|race| link_to race.name, race_url(race) }.join(', ') + '.'
57
+
58
+
59
+
60
+ = yield :results
61
+ = yield :see_also
@@ -0,0 +1 @@
1
+ <%= performance.position %>, <%= performance.name %>, <%= performance.race_category.name %>, <%= performance.club.name %>, <%= performance.elapsed_time %>
@@ -3,11 +3,11 @@
3
3
  %tr{:class => trclass}
4
4
  %td.pos
5
5
  - if performance.finished?
6
- = performance.position
6
+ = link_to performance.position, race_performance_url(@instance.race, @instance, performance)
7
7
  - else
8
8
  = performance.status
9
9
  %td.name
10
- = performance.name
10
+ = link_to performance.name, race_performance_url(@instance.race, @instance, performance)
11
11
  %td.club
12
12
  - if performance.club
13
13
  = link_to_unless_current performance.club.name, race_club_url(@instance.race, @instance, performance.club)
@@ -0,0 +1,3 @@
1
+ <%-
2
+ performance ||= splits
3
+ %><%= performance.position %>, <%= performance.name %>, <%= @instance.race.checkpoints.collect {|cp| performance.time_at(cp) }.join(', ') %>, <%= performance.elapsed_time %>
@@ -0,0 +1,30 @@
1
+ - performance ||= splits
2
+
3
+ %tr
4
+ %td.pos
5
+ - if performance.finished?
6
+ = link_to performance.position, race_performance_url(@instance.race, @instance, performance)
7
+ - else
8
+ = performance.status
9
+ %td.runner
10
+ = link_to performance.name, race_performance_url(@instance.race, @instance, performance)
11
+ - previous = nil
12
+ - @checkpoints.each do |cp|
13
+ %td.cpt
14
+ - if t = @splits[performance.id][cp.id]
15
+ = t
16
+ - if previous
17
+ %br
18
+ %span.note
19
+ = t - previous
20
+ - previous = t
21
+ - else
22
+ - previous = nil
23
+ &mdash;
24
+ %td.time
25
+ - if performance.finished?
26
+ = performance.elapsed_time
27
+ - if previous
28
+ %br
29
+ %span.note
30
+ = performance.elapsed_time - previous
@@ -0,0 +1,105 @@
1
+ = render :partial => 'races/standard_parts'
2
+
3
+ - content_for :performance do
4
+ %table.ranking
5
+ %thead
6
+ %tr
7
+ - %w{Pos Name Club Cat Time}.each do |h|
8
+ %th{:class => h.downcase}
9
+ =h
10
+ %th.prizes
11
+ %tbody
12
+ = render :partial => 'race_performances/performance', :object => @performance
13
+
14
+ %h2 Splits
15
+
16
+ %table.splits
17
+ %thead
18
+ %tr
19
+ %th Checkpoint
20
+ %th Time
21
+ %th Interval
22
+ %th Position
23
+ %th This leg
24
+ %th Places gained
25
+ %tbody
26
+ - previous = nil
27
+ - previous_position = nil
28
+ - @race.checkpoints.each do |cp|
29
+ - cpt = @performance.time_at(cp)
30
+ - t = cpt.elapsed_time
31
+ %tr
32
+ %td
33
+ = cpt.name
34
+ - if t && t.seconds > 0
35
+ %td
36
+ = t
37
+ %td
38
+ - if previous
39
+ = t - previous
40
+ %td
41
+ = cpt.position
42
+ %td
43
+ = cpt.leg_position
44
+ %td
45
+ - if previous_position
46
+ - delta = previous_position - cpt.position
47
+ - if delta > 0
48
+ = "+#{delta}"
49
+ - else
50
+ = delta
51
+ - previous = t
52
+ - previous_position = cpt.position
53
+ - else
54
+ %td
55
+ &mdash;
56
+ - previous = nil
57
+ %tr
58
+ %td
59
+ Finish
60
+ %td
61
+ = @performance.elapsed_time
62
+ %td
63
+ - if previous
64
+ = @performance.elapsed_time - previous
65
+ %td
66
+ = @performance.position
67
+ %td
68
+ %td
69
+ - if previous_position
70
+ - delta = previous_position - @performance.position
71
+ - if delta > 0
72
+ = "+#{delta}"
73
+ - elsif delta == 0
74
+ &mdash;
75
+ - else
76
+ = delta
77
+
78
+
79
+ - content_for :breadhead do
80
+ = link_to "Races", races_url, :class => 'breadhead'
81
+ = t('separator')
82
+ = link_to @instance.race.name, race_url(@instance.race), :class => 'breadhead'
83
+ = t('separator')
84
+ = @performance.name
85
+
86
+ - content_for :title do
87
+ = "#{@instance.full_name}: #{@performance.name}"
88
+
89
+ - content_for :see_also do
90
+ .see_also
91
+ %p
92
+ - if @performance.club
93
+ = link_to "More #{@performance.club.name} results", race_club_url(@race, @instance, @performance.club)
94
+ %br
95
+ - if @performance.category
96
+ = link_to "More #{@performance.category.name} results", race_category_url(@race, @instance, @performance.category)
97
+ %br
98
+ All
99
+ = @instance.full_name
100
+ = link_to "results", race_instance_url(@race, @instance)
101
+ or
102
+ = link_to "splits", race_splits_url(@race, @instance)
103
+
104
+ = yield :performance
105
+ = yield :see_also
data/artwork/csv.psd ADDED
Binary file
data/config/routes.rb CHANGED
@@ -1,21 +1,24 @@
1
1
  ActionController::Routing::Routes.draw do |map|
2
2
  map.namespace :admin, :member => { :remove => :get } do |admin|
3
3
  admin.resources :races do |race|
4
- race.resources :race_instances, :has_many => [:race_performances]
4
+ race.resources :race_instances, :has_many => [:race_performances], :member => [:splits]
5
5
  end
6
6
  admin.resources :race_clubs
7
7
  admin.resources :race_competitors
8
8
  end
9
9
 
10
- # public interface is read-only and uses slugs for url-friendliness (and seo)
10
+ # public interface is read-only and uses slugs for url-friendliness (and seo, I suppose)
11
11
  map.races '/races', :controller => 'races', :action => 'index'
12
12
  map.race '/races/:slug', :controller => 'races', :action => 'show'
13
13
 
14
- map.race_instance '/races/:race_slug/:slug', :controller => 'race_instances', :action => 'show'
15
- map.race_club '/races/:race_slug/:slug/club/:club', :controller => 'race_instances', :action => 'show'
16
- map.race_category '/races/:race_slug/:slug/cat/:cat', :controller => 'race_instances', :action => 'show'
17
-
14
+ map.race_instance '/races/:race_slug/:slug.:format', :controller => 'race_instances', :action => 'show'
18
15
  map.race_performance '/races/:race_slug/:slug/p/:id', :controller => 'race_performances', :action => 'show'
16
+
17
+ map.race_splits '/races/:race_slug/:slug/splits.:format', :controller => 'race_instances', :action => 'splits'
18
+ map.race_club '/races/:race_slug/:slug/club/:club.:format', :controller => 'race_instances', :action => 'show'
19
+ map.race_club_splits '/races/:race_slug/:slug/splits/club/:club.:format', :controller => 'race_instances', :action => 'splits'
20
+ map.race_category '/races/:race_slug/:slug/cat/:cat.:format', :controller => 'race_instances', :action => 'show'
21
+ map.race_category_splits '/races/:race_slug/:slug/splits/cat/:cat.:format', :controller => 'race_instances', :action => 'splits'
19
22
 
20
23
  # map.resources :race_clubs
21
24
  # map.resources :race_competitors
@@ -0,0 +1,9 @@
1
+ class StoreIntervals < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :race_checkpoint_times, :interval, :integer
4
+ end
5
+
6
+ def self.down
7
+ remove_column :race_checkpoint_times, :interval
8
+ end
9
+ end
@@ -39,7 +39,7 @@ end
39
39
 
40
40
  module StringExtensions
41
41
  def duration
42
- ActiveSupport::Duration.parse(self)
42
+ ActiveSupport::Duration.parse(self).value
43
43
  end
44
44
  alias :seconds :duration
45
45
 
@@ -47,9 +47,26 @@ module StringExtensions
47
47
  delimiters = Radiant::Config['race_results.delimiters'] || ':,.'
48
48
  true if self.match(/^[\d#{Regexp.escape(delimiters)}]+$/)
49
49
  end
50
+
51
+ def -(other)
52
+ if self.looks_like_duration? && other.looks_like_duration?
53
+ (self.seconds - other.seconds).to_timecode
54
+ end
55
+ end
56
+
57
+ def looks_like_number?
58
+ Float(s) != nil rescue false
59
+ end
60
+
61
+ def ordinal
62
+ to_i.ordinal
63
+ end
50
64
  end
51
65
 
52
66
  module NumericExtensions
67
+ def ordinal
68
+ to_s + ([[nil, 'st','nd','rd'],[]][self / 10 == 1 && 1 || 0][self % 10] || 'th')
69
+ end
53
70
  def to_timecode
54
71
  seconds.timecode
55
72
  end
Binary file
@@ -0,0 +1,3 @@
1
+ (function($){$.extend({tablesorter:new
2
+ function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:true,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'/\.|\,/g',onRenderHeader:null,selectorHeaders:'thead th',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}if(table.tBodies.length==0)return;var rows=table.tBodies[0].rows;if(rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i<l;i++){var p=false;if($.metadata&&($($headers[i]).metadata()&&$($headers[i]).metadata().sorter)){p=getParserById($($headers[i]).metadata().sorter);}else if((table.config.headers[i]&&table.config.headers[i].sorter)){p=getParserById(table.config.headers[i].sorter);}if(!p){p=detectParserForColumn(table,rows,-1,i);}if(table.config.debug){parsersDebug+="column:"+i+" parser:"+p.id+"\n";}list.push(p);}}if(table.config.debug){log(parsersDebug);}return list;};function detectParserForColumn(table,rows,rowIndex,cellIndex){var l=parsers.length,node=false,nodeValue=false,keepLooking=true;while(nodeValue==''&&keepLooking){rowIndex++;if(rows[rowIndex]){node=getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex);nodeValue=trimAndGetNodeText(table.config,node);if(table.config.debug){log('Checking if value was empty on row:'+rowIndex);}}else{keepLooking=false;}}for(var i=1;i<l;i++){if(parsers[i].is(nodeValue,table,node)){return parsers[i];}}return parsers[0];}function getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex){return rows[rowIndex].cells[cellIndex];}function trimAndGetNodeText(config,node){return $.trim(getElementText(config,node));}function getParserById(name){var l=parsers.length;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==name.toLowerCase()){return parsers[i];}}return false;}function buildCache(table){if(table.config.debug){var cacheTime=new Date();}var totalRows=(table.tBodies[0]&&table.tBodies[0].rows.length)||0,totalCells=(table.tBodies[0].rows[0]&&table.tBodies[0].rows[0].cells.length)||0,parsers=table.config.parsers,cache={row:[],normalized:[]};for(var i=0;i<totalRows;++i){var c=$(table.tBodies[0].rows[i]),cols=[];if(c.hasClass(table.config.cssChildRow)){cache.row[cache.row.length-1]=cache.row[cache.row.length-1].add(c);continue;}cache.row.push(c);for(var j=0;j<totalCells;++j){cols.push(parsers[j].format(getElementText(table.config,c[0].cells[j]),table,c[0].cells[j]));}cols.push(cache.normalized.length);cache.normalized.push(cols);cols=null;};if(table.config.debug){benchmark("Building cache for "+totalRows+" rows:",cacheTime);}return cache;};function getElementText(config,node){var text="";if(!node)return"";if(!config.supportsTextContent)config.supportsTextContent=node.textContent||false;if(config.textExtraction=="simple"){if(config.supportsTextContent){text=node.textContent;}else{if(node.childNodes[0]&&node.childNodes[0].hasChildNodes()){text=node.childNodes[0].innerHTML;}else{text=node.innerHTML;}}}else{if(typeof(config.textExtraction)=="function"){text=config.textExtraction(node);}else{text=$(node).text();}}return text;}function appendToTable(table,cache){if(table.config.debug){var appendTime=new Date()}var c=cache,r=c.row,n=c.normalized,totalRows=n.length,checkCell=(n[0].length-1),tableBody=$(table.tBodies[0]),rows=[];for(var i=0;i<totalRows;i++){var pos=n[i][checkCell];rows.push(r[pos]);if(!table.config.appender){var l=r[pos].length;for(var j=0;j<l;j++){tableBody[0].appendChild(r[pos][j]);}}}if(table.config.appender){table.config.appender(table,rows);}rows=null;if(table.config.debug){benchmark("Rebuilt table:",appendTime);}applyWidget(table);setTimeout(function(){$(table).trigger("sortEnd");},0);};function buildHeaders(table){if(table.config.debug){var time=new Date();}var meta=($.metadata)?true:false;var header_index=computeTableHeaderCellIndexes(table);$tableHeaders=$(table.config.selectorHeaders,table).each(function(index){this.column=header_index[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(table.config.sortInitialOrder);this.count=this.order;if(checkHeaderMetadata(this)||checkHeaderOptions(table,index))this.sortDisabled=true;if(checkHeaderOptionsSortingLocked(table,index))this.order=this.lockedOrder=checkHeaderOptionsSortingLocked(table,index);if(!this.sortDisabled){var $th=$(this).addClass(table.config.cssHeader);if(table.config.onRenderHeader)table.config.onRenderHeader.apply($th);}table.config.headerList[index]=this;});if(table.config.debug){benchmark("Built headers:",time);log($tableHeaders);}return $tableHeaders;};function computeTableHeaderCellIndexes(t){var matrix=[];var lookup={};var thead=t.getElementsByTagName('THEAD')[0];var trs=thead.getElementsByTagName('TR');for(var i=0;i<trs.length;i++){var cells=trs[i].cells;for(var j=0;j<cells.length;j++){var c=cells[j];var rowIndex=c.parentNode.rowIndex;var cellId=rowIndex+"-"+c.cellIndex;var rowSpan=c.rowSpan||1;var colSpan=c.colSpan||1
3
+ var firstAvailCol;if(typeof(matrix[rowIndex])=="undefined"){matrix[rowIndex]=[];}for(var k=0;k<matrix[rowIndex].length+1;k++){if(typeof(matrix[rowIndex][k])=="undefined"){firstAvailCol=k;break;}}lookup[cellId]=firstAvailCol;for(var k=rowIndex;k<rowIndex+rowSpan;k++){if(typeof(matrix[k])=="undefined"){matrix[k]=[];}var matrixrow=matrix[k];for(var l=firstAvailCol;l<firstAvailCol+colSpan;l++){matrixrow[l]="x";}}}}return lookup;}function checkCellColSpan(table,rows,row){var arr=[],r=table.tHead.rows,c=r[row].cells;for(var i=0;i<c.length;i++){var cell=c[i];if(cell.colSpan>1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function checkHeaderOptionsSortingLocked(table,i){if((table.config.headers[i])&&(table.config.headers[i].lockedOrder))return table.config.headers[i].lockedOrder;return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i<l;i++){getWidgetById(c[i]).format(table);}}function getWidgetById(name){var l=widgets.length;for(var i=0;i<l;i++){if(widgets[i].id.toLowerCase()==name.toLowerCase()){return widgets[i];}}};function formatSortingOrder(v){if(typeof(v)!="Number"){return(v.toLowerCase()=="desc")?1:0;}else{return(v==1)?1:0;}}function isValueInArray(v,a){var l=a.length;for(var i=0;i<l;i++){if(a[i][0]==v){return true;}}return false;}function setHeadersCss(table,$headers,list,css){$headers.removeClass(css[0]).removeClass(css[1]);var h=[];$headers.each(function(offset){if(!this.sortDisabled){h[this.column]=$(this);}});var l=list.length;for(var i=0;i<l;i++){h[list[i][0]].addClass(css[list[i][1]]);}}function fixColumnWidth(table,$headers){var c=table.config;if(c.widthFixed){var colgroup=$('<colgroup>');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('<col>').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i<l;i++){var s=sortList[i],o=c.headerList[s[0]];o.count=s[1];o.count++;}}function multisort(table,sortList,cache){if(table.config.debug){var sortTime=new Date();}var dynamicExp="var sortWrapper = function(a,b) {",l=sortList.length;for(var i=0;i<l;i++){var c=sortList[i][0];var order=sortList[i][1];var s=(table.config.parsers[c].type=="text")?((order==0)?makeSortFunction("text","asc",c):makeSortFunction("text","desc",c)):((order==0)?makeSortFunction("numeric","asc",c):makeSortFunction("numeric","desc",c));var e="e"+i;dynamicExp+="var "+e+" = "+s;dynamicExp+="if("+e+") { return "+e+"; } ";dynamicExp+="else { ";}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;i<l;i++){dynamicExp+="}; ";}dynamicExp+="return 0; ";dynamicExp+="}; ";if(table.config.debug){benchmark("Evaling expression:"+dynamicExp,new Date());}eval(dynamicExp);cache.normalized.sort(sortWrapper);if(table.config.debug){benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime);}return cache;};function makeSortFunction(type,direction,index){var a="a["+index+"]",b="b["+index+"]";if(type=='text'&&direction=='asc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+a+" < "+b+") ? -1 : 1 )));";}else if(type=='text'&&direction=='desc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+b+" < "+a+") ? -1 : 1 )));";}else if(type=='numeric'&&direction=='asc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+a+" - "+b+"));";}else if(type=='numeric'&&direction=='desc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+b+" - "+a+"));";}};function makeSortText(i){return"((a["+i+"] < b["+i+"]) ? -1 : ((a["+i+"] > b["+i+"]) ? 1 : 0));";};function makeSortTextDesc(i){return"((b["+i+"] < a["+i+"]) ? -1 : ((b["+i+"] > a["+i+"]) ? 1 : 0));";};function makeSortNumeric(i){return"a["+i+"]-b["+i+"];";};function makeSortNumericDesc(i){return"b["+i+"]-a["+i+"];";};function sortText(a,b){if(table.config.sortLocaleCompare)return a.localeCompare(b);return((a<b)?-1:((a>b)?1:0));};function sortTextDesc(a,b){if(table.config.sortLocaleCompare)return b.localeCompare(a);return((b<a)?-1:((b>a)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$.data(this,"tablesorter",config);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){$this.trigger("sortStart");var $cell=$(this);var i=this.column;this.order=this.count++%2;if(this.lockedOrder)this.order=this.lockedOrder;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j<a.length;j++){if(a[j][0]!=i){config.sortList.push(a[j]);}}}config.sortList.push([i,this.order]);}else{if(isValueInArray(i,config.sortList)){for(var j=0;j<config.sortList.length;j++){var s=config.sortList[j],o=config.headerList[s[0]];if(s[0]==i){o.count=s[1];o.count++;s[1]=o.count%2;}}}else{config.sortList.push([i,this.order]);}};setTimeout(function(){setHeadersCss($this[0],$headers,config.sortList,sortCSS);appendToTable($this[0],multisort($this[0],config.sortList,cache));},1);return false;}}).mousedown(function(){if(config.cancelSelection){this.onselectstart=function(){return false};return false;}});$this.bind("update",function(){var me=this;setTimeout(function(){me.config.parsers=buildParserCache(me,$headers);cache=buildCache(me);},1);}).bind("updateCell",function(e,cell){var config=this.config;var pos=[(cell.parentNode.rowIndex-1),cell.cellIndex];cache.normalized[pos[0]][pos[1]]=config.parsers[pos[1]].format(getElementText(config,cell),cell);}).bind("sorton",function(e,list){$(this).trigger("sortStart");config.sortList=list;var sortList=config.sortList;updateHeaderSortCount(this,sortList);setHeadersCss(this,$headers,sortList,sortCSS);appendToTable(this,multisort(this,sortList,cache));}).bind("appendCache",function(){appendToTable(this,cache);}).bind("applyWidgetId",function(e,id){getWidgetById(id).format(this);}).bind("applyWidgets",function(){applyWidget(this);});if($.metadata&&($(this).metadata()&&$(this).metadata().sortlist)){config.sortList=$(this).metadata().sortlist;}if(config.sortList.length>0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==parser.id.toLowerCase()){a=false;}}if(a){parsers.push(parser);};};this.addWidget=function(widget){widgets.push(widget);};this.formatFloat=function(s){var i=parseFloat(s);return(isNaN(i))?0:i;};this.formatInt=function(s){var i=parseInt(s);return(isNaN(i))?0:i;};this.isDigit=function(s,config){return/^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g,'')));};this.clearTableBody=function(table){if($.browser.msie){function empty(){while(this.firstChild)this.removeChild(this.firstChild);}empty.apply(table.tBodies[0]);}else{table.tBodies[0].innerHTML="";}};}});$.fn.extend({tablesorter:$.tablesorter.construct});var ts=$.tablesorter;ts.addParser({id:"text",is:function(s){return true;},format:function(s){return $.trim(s.toLocaleLowerCase());},type:"text"});ts.addParser({id:"digit",is:function(s,table){var c=table.config;return $.tablesorter.isDigit(s,c);},format:function(s){return $.tablesorter.formatFloat(s);},type:"numeric"});ts.addParser({id:"currency",is:function(s){return/^[£$€?.]/.test(s);},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g),""));},type:"numeric"});ts.addParser({id:"ipAddress",is:function(s){return/^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);},format:function(s){var a=s.split("."),r="",l=a.length;for(var i=0;i<l;i++){var item=a[i];if(item.length==2){r+="0"+item;}else{r+=item;}}return $.tablesorter.formatFloat(r);},type:"numeric"});ts.addParser({id:"url",is:function(s){return/^(https?|ftp|file):\/\/$/.test(s);},format:function(s){return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));},type:"text"});ts.addParser({id:"isoDate",is:function(s){return/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);},format:function(s){return $.tablesorter.formatFloat((s!="")?new Date(s.replace(new RegExp(/-/g),"/")).getTime():"0");},type:"numeric"});ts.addParser({id:"percent",is:function(s){return/\%$/.test($.trim(s));},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));},type:"numeric"});ts.addParser({id:"usLongDate",is:function(s){return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));},format:function(s){return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"shortDate",is:function(s){return/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);},format:function(s,table){var c=table.config;s=s.replace(/\-/g,"/");if(c.dateFormat=="us"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$1/$2");}else if(c.dateFormat=="uk"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1");}else if(c.dateFormat=="dd/mm/yy"||c.dateFormat=="dd-mm-yy"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/,"$1/$2/$3");}return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"time",is:function(s){return/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);},format:function(s){return $.tablesorter.formatFloat(new Date("2000/01/01 "+s).getTime());},type:"numeric"});ts.addParser({id:"metadata",is:function(s){return false;},format:function(s,table,cell){var c=table.config,p=(!c.parserMetadataName)?'sortValue':c.parserMetadataName;return $(cell).metadata()[p];},type:"numeric"});ts.addWidget({id:"zebra",format:function(table){if(table.config.debug){var time=new Date();}var $tr,row=-1,odd;$("tr:visible",table.tBodies[0]).each(function(i){$tr=$(this);if(!$tr.hasClass(table.config.cssChildRow))row++;odd=(row%2==0);$tr.removeClass(table.config.widgetZebra.css[odd?0:1]).addClass(table.config.widgetZebra.css[odd?1:0])});if(table.config.debug){$.tablesorter.benchmark("Applying Zebra widget",time);}}});})(jQuery);
@@ -0,0 +1,60 @@
1
+ $page: #ffffff
2
+ $text: #4d4e53
3
+ $link: #949554
4
+ $visited: #949554
5
+ $hover: #d1005d
6
+ $dark: #4d4e53
7
+ $mid: #8c8d8e
8
+ $pale: #b3b3b3
9
+ $palebg: #f1f0ee
10
+
11
+ table.ranking
12
+ border-collapse: collapse
13
+ border: 0
14
+ margin-top: 10px
15
+ tr.prized
16
+ background-color: $palebg
17
+ font-weight: bold
18
+ tr.unfinished
19
+ color: $mid
20
+ a
21
+ color: $mid
22
+ th, td
23
+ vertical-align: top
24
+ font-size: 90%
25
+ padding: 6px 10px
26
+ text-align: left
27
+ a
28
+ color: $dark
29
+ a:hover
30
+ color: $hover
31
+ &.prizes
32
+ color: $mid
33
+ font-size: 75%
34
+
35
+ table.splits
36
+ border-collapse: collapse
37
+ border: 0
38
+ margin-top: 10px
39
+ tr.unfinished
40
+ color: $mid
41
+ a
42
+ color: $mid
43
+ th, td
44
+ vertical-align: top
45
+ font-size: 80%
46
+ padding: 3px 5px
47
+ text-align: left
48
+ a
49
+ color: $dark
50
+ a:hover
51
+ color: $hover
52
+ span.interval
53
+ color: $mid
54
+
55
+ p.download
56
+ a.csv
57
+ padding-left: 20px
58
+ background: transparent url(/images/race_results/csv.png) no-repeat 0 0
59
+ &:hover
60
+ background-position: 0 -16px
@@ -2,7 +2,7 @@
2
2
  # require_dependency 'application_controller'
3
3
 
4
4
  class RaceResultsExtension < Radiant::Extension
5
- version "1.2.1"
5
+ version "1.3.0"
6
6
  description "Makes easy the uploading, analysis and display of race results. Built for fell races but should work for most timed or score events."
7
7
  url "http://spanner.org/radiant/race_results"
8
8
 
@@ -5,18 +5,19 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{radiant-race_results-extension}
8
- s.version = "1.2.1"
8
+ s.version = "1.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["spanner"]
12
- s.date = %q{2011-02-11}
12
+ s.date = %q{2011-03-15}
13
13
  s.description = %q{Makes easy the uploading, analysis and display of race results. Built for fell races but should work for most timed or score events including those with checkpoints.}
14
14
  s.email = %q{will@spanner.org}
15
15
  s.extra_rdoc_files = [
16
16
  "README.md"
17
17
  ]
18
18
  s.files = [
19
- "README.md",
19
+ ".gitignore",
20
+ "README.md",
20
21
  "Rakefile",
21
22
  "VERSION",
22
23
  "app/controllers/admin/race_clubs_controller.rb",
@@ -61,13 +62,21 @@ Gem::Specification.new do |s|
61
62
  "app/views/admin/races/new.html.haml",
62
63
  "app/views/race_categories/show.html.haml",
63
64
  "app/views/race_clubs/show.html.haml",
65
+ "app/views/race_instances/_summary.html.haml",
64
66
  "app/views/race_instances/index.html.haml",
67
+ "app/views/race_instances/show.csv.erb",
65
68
  "app/views/race_instances/show.html.haml",
66
- "app/views/race_performances/_headings.html.haml",
69
+ "app/views/race_instances/splits.csv.erb",
70
+ "app/views/race_instances/splits.html.haml",
71
+ "app/views/race_performances/_performance.csv.erb",
67
72
  "app/views/race_performances/_performance.html.haml",
73
+ "app/views/race_performances/_splits.csv.erb",
74
+ "app/views/race_performances/_splits.html.haml",
75
+ "app/views/race_performances/show.html.haml",
68
76
  "app/views/races/_standard_parts.html.haml",
69
77
  "app/views/races/index.html.haml",
70
78
  "app/views/races/show.html.haml",
79
+ "artwork/csv.psd",
71
80
  "config/routes.rb",
72
81
  "cucumber.yml",
73
82
  "db/migrate/20091116130836_race_data.rb",
@@ -82,6 +91,7 @@ Gem::Specification.new do |s|
82
91
  "db/migrate/20100106104850_remember_calculations.rb",
83
92
  "db/migrate/20100426104801_race_attachments.rb",
84
93
  "db/migrate/20101005112007_race_category.rb",
94
+ "db/migrate/20110315114349_store_intervals.rb",
85
95
  "features/support/env.rb",
86
96
  "features/support/paths.rb",
87
97
  "lib/duration_extensions.rb",
@@ -90,8 +100,11 @@ Gem::Specification.new do |s|
90
100
  "lib/tasks/race_results_extension_tasks.rake",
91
101
  "public/images/admin/calendar_down.png",
92
102
  "public/images/admin/new-race.png",
103
+ "public/images/race_results/csv.png",
93
104
  "public/javascripts/admin/races.js",
105
+ "public/javascripts/tablesorter.js",
94
106
  "public/stylesheets/sass/admin/races.sass",
107
+ "public/stylesheets/sass/race_results.sass",
95
108
  "race_results_extension.rb",
96
109
  "radiant-race_results-extension.gemspec",
97
110
  "spec/datasets/competitors_dataset.rb",
@@ -78,10 +78,10 @@ describe RaceInstance do
78
78
  it "should import checkpoint times" do
79
79
  @perf.checkpoint_times.any?.should be_true
80
80
  Rails.logger.warn ">>>"
81
- p "checkpoint is #{@cp.inspect}"
82
- p "checkpoint times are #{@perf.checkpoint_times.inspect}"
83
- p "perf.time_at(@cp) is #{@perf.time_at(@cp).inspect}"
84
- Rails.logger.warn "<<<"
81
+ # p "checkpoint is #{@cp.inspect}"
82
+ # p "checkpoint times are #{@perf.checkpoint_times.inspect}"
83
+ # p "perf.time_at(@cp) is #{@perf.time_at(@cp).inspect}"
84
+ # Rails.logger.warn "<<<"
85
85
  @perf.time_at(@cp).elapsed_time.should == "0:36:30"
86
86
  end
87
87
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: radiant-race_results-extension
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
- - 2
9
- - 1
10
- version: 1.2.1
8
+ - 3
9
+ - 0
10
+ version: 1.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - spanner
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-02-11 00:00:00 +00:00
18
+ date: 2011-03-15 00:00:00 +00:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -57,6 +57,7 @@ extensions: []
57
57
  extra_rdoc_files:
58
58
  - README.md
59
59
  files:
60
+ - .gitignore
60
61
  - README.md
61
62
  - Rakefile
62
63
  - VERSION
@@ -102,13 +103,21 @@ files:
102
103
  - app/views/admin/races/new.html.haml
103
104
  - app/views/race_categories/show.html.haml
104
105
  - app/views/race_clubs/show.html.haml
106
+ - app/views/race_instances/_summary.html.haml
105
107
  - app/views/race_instances/index.html.haml
108
+ - app/views/race_instances/show.csv.erb
106
109
  - app/views/race_instances/show.html.haml
107
- - app/views/race_performances/_headings.html.haml
110
+ - app/views/race_instances/splits.csv.erb
111
+ - app/views/race_instances/splits.html.haml
112
+ - app/views/race_performances/_performance.csv.erb
108
113
  - app/views/race_performances/_performance.html.haml
114
+ - app/views/race_performances/_splits.csv.erb
115
+ - app/views/race_performances/_splits.html.haml
116
+ - app/views/race_performances/show.html.haml
109
117
  - app/views/races/_standard_parts.html.haml
110
118
  - app/views/races/index.html.haml
111
119
  - app/views/races/show.html.haml
120
+ - artwork/csv.psd
112
121
  - config/routes.rb
113
122
  - cucumber.yml
114
123
  - db/migrate/20091116130836_race_data.rb
@@ -123,6 +132,7 @@ files:
123
132
  - db/migrate/20100106104850_remember_calculations.rb
124
133
  - db/migrate/20100426104801_race_attachments.rb
125
134
  - db/migrate/20101005112007_race_category.rb
135
+ - db/migrate/20110315114349_store_intervals.rb
126
136
  - features/support/env.rb
127
137
  - features/support/paths.rb
128
138
  - lib/duration_extensions.rb
@@ -131,8 +141,11 @@ files:
131
141
  - lib/tasks/race_results_extension_tasks.rake
132
142
  - public/images/admin/calendar_down.png
133
143
  - public/images/admin/new-race.png
144
+ - public/images/race_results/csv.png
134
145
  - public/javascripts/admin/races.js
146
+ - public/javascripts/tablesorter.js
135
147
  - public/stylesheets/sass/admin/races.sass
148
+ - public/stylesheets/sass/race_results.sass
136
149
  - race_results_extension.rb
137
150
  - radiant-race_results-extension.gemspec
138
151
  - spec/datasets/competitors_dataset.rb
@@ -1,5 +0,0 @@
1
- %tr
2
- - %w{Pos Name Club Cat Time}.each do |h|
3
- %th{:class => h.downcase}
4
- =h
5
- %th.prizes