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 +1 -0
- data/README.md +3 -1
- data/VERSION +1 -1
- data/app/controllers/race_instances_controller.rb +13 -8
- data/app/controllers/race_performances_controller.rb +5 -4
- data/app/models/race.rb +2 -2
- data/app/models/race_checkpoint_time.rb +57 -12
- data/app/models/race_instance.rb +17 -7
- data/app/models/race_performance.rb +4 -3
- data/app/models/race_performance_status.rb +15 -6
- data/app/views/race_instances/_summary.html.haml +31 -0
- data/app/views/race_instances/show.csv.erb +2 -0
- data/app/views/race_instances/show.html.haml +14 -16
- data/app/views/race_instances/splits.csv.erb +2 -0
- data/app/views/race_instances/splits.html.haml +61 -0
- data/app/views/race_performances/_performance.csv.erb +1 -0
- data/app/views/race_performances/_performance.html.haml +2 -2
- data/app/views/race_performances/_splits.csv.erb +3 -0
- data/app/views/race_performances/_splits.html.haml +30 -0
- data/app/views/race_performances/show.html.haml +105 -0
- data/artwork/csv.psd +0 -0
- data/config/routes.rb +9 -6
- data/db/migrate/20110315114349_store_intervals.rb +9 -0
- data/lib/duration_extensions.rb +18 -1
- data/public/images/race_results/csv.png +0 -0
- data/public/javascripts/tablesorter.js +3 -0
- data/public/stylesheets/sass/race_results.sass +60 -0
- data/race_results_extension.rb +1 -1
- data/radiant-race_results-extension.gemspec +17 -4
- data/spec/models/race_instance_spec.rb +4 -4
- metadata +19 -6
- data/app/views/race_performances/_headings.html.haml +0 -5
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
|
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.
|
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 :
|
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 :
|
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
|
14
|
-
@race = Race.
|
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.
|
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.
|
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 :
|
7
|
-
belongs_to :
|
8
|
-
|
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
|
-
|
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 :
|
33
|
+
named_scope :ahead_of, lambda {|duration|
|
19
34
|
{
|
20
|
-
:
|
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 => ["
|
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
|
-
|
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.
|
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
|
-
|
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
|
data/app/models/race_instance.rb
CHANGED
@@ -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.
|
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
|
-
|
141
|
-
value = runner[normalize(
|
142
|
-
|
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)
|
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.
|
40
|
-
|
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
|
@@ -2,25 +2,18 @@
|
|
2
2
|
|
3
3
|
- content_for :results do
|
4
4
|
- if @instance.has_results?
|
5
|
-
|
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.
|
7
|
+
%table.ranking
|
18
8
|
%thead
|
19
|
-
|
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
|
-
|
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,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,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
|
+
—
|
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
|
+
—
|
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
|
+
—
|
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
|
data/lib/duration_extensions.rb
CHANGED
@@ -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
|
data/race_results_extension.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
# require_dependency 'application_controller'
|
3
3
|
|
4
4
|
class RaceResultsExtension < Radiant::Extension
|
5
|
-
version "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.
|
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-
|
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
|
-
"
|
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/
|
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:
|
4
|
+
hash: 27
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 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-
|
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/
|
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
|