radiant-race_results-extension 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +8 -0
- data/Rakefile +137 -0
- data/app/controllers/admin/race_clubs_controller.rb +15 -0
- data/app/controllers/admin/race_competitors_controller.rb +15 -0
- data/app/controllers/admin/race_instances_controller.rb +17 -0
- data/app/controllers/admin/races_controller.rb +35 -0
- data/app/controllers/race_instances_controller.rb +23 -0
- data/app/controllers/race_performances_controller.rb +16 -0
- data/app/controllers/races_controller.rb +13 -0
- data/app/helpers/races_helper.rb +38 -0
- data/app/models/race.rb +64 -0
- data/app/models/race_category.rb +114 -0
- data/app/models/race_checkpoint.rb +31 -0
- data/app/models/race_checkpoint_time.rb +64 -0
- data/app/models/race_club.rb +48 -0
- data/app/models/race_club_alias.rb +9 -0
- data/app/models/race_competitor.rb +20 -0
- data/app/models/race_instance.rb +178 -0
- data/app/models/race_page.rb +69 -0
- data/app/models/race_performance.rb +175 -0
- data/app/models/race_performance_status.rb +58 -0
- data/app/models/race_record.rb +27 -0
- data/app/views/admin/race_checkpoints/_checkpoint.html.haml +19 -0
- data/app/views/admin/race_club_aliases/_race_club_alias.html.haml +10 -0
- data/app/views/admin/race_clubs/_club.html.haml +19 -0
- data/app/views/admin/race_clubs/_form.html.haml +21 -0
- data/app/views/admin/race_clubs/edit.html.haml +20 -0
- data/app/views/admin/race_clubs/index.html.haml +27 -0
- data/app/views/admin/race_competitors/_competitor.html.haml +14 -0
- data/app/views/admin/race_competitors/_form.html.haml +21 -0
- data/app/views/admin/race_competitors/edit.html.haml +20 -0
- data/app/views/admin/race_competitors/index.html.haml +27 -0
- data/app/views/admin/race_instances/_form.html.haml +98 -0
- data/app/views/admin/race_instances/edit.html.haml +17 -0
- data/app/views/admin/race_instances/new.html.haml +18 -0
- data/app/views/admin/race_records/_record.html.haml +20 -0
- data/app/views/admin/races/_form.html.haml +82 -0
- data/app/views/admin/races/_race.html.haml +36 -0
- data/app/views/admin/races/edit.html.haml +19 -0
- data/app/views/admin/races/index.html.haml +26 -0
- data/app/views/admin/races/new.html.haml +19 -0
- data/app/views/race_categories/show.html.haml +36 -0
- data/app/views/race_clubs/show.html.haml +25 -0
- data/app/views/race_instances/index.html.haml +14 -0
- data/app/views/race_instances/show.html.haml +52 -0
- data/app/views/race_performances/_headings.html.haml +4 -0
- data/app/views/race_performances/_performance.html.haml +21 -0
- data/app/views/races/index.html.haml +38 -0
- data/app/views/races/show.html.haml +81 -0
- data/cucumber.yml +1 -0
- data/db/migrate/20091116130836_race_data.rb +128 -0
- data/db/migrate/20091124095157_competitor_details.rb +9 -0
- data/db/migrate/20091126110634_club_aliases.rb +17 -0
- data/db/migrate/20091223094002_instance_details.rb +11 -0
- data/db/migrate/20091224100524_race_distance.rb +11 -0
- data/db/migrate/20091224100734_race_records.rb +19 -0
- data/db/migrate/20091224105637_category_details.rb +13 -0
- data/db/migrate/20091224115909_filters.rb +11 -0
- data/db/migrate/20091228122837_record_holder.rb +11 -0
- data/db/migrate/20100106104850_remember_calculations.rb +9 -0
- data/db/migrate/20100426104801_race_attachments.rb +11 -0
- data/features/support/env.rb +16 -0
- data/features/support/paths.rb +14 -0
- data/lib/duration_extensions.rb +61 -0
- data/lib/race_results/admin_ui.rb +76 -0
- data/lib/race_results/race_tags.rb +722 -0
- data/lib/tasks/race_results_extension_tasks.rake +28 -0
- data/public/images/admin/calendar_down.png +0 -0
- data/public/images/admin/new-race.png +0 -0
- data/public/javascripts/admin/races.js +29 -0
- data/public/stylesheets/sass/admin/races.sass +131 -0
- data/race_results_extension.rb +38 -0
- data/spec/datasets/competitors_dataset.rb +81 -0
- data/spec/datasets/race_sites_dataset.rb +8 -0
- data/spec/datasets/races_dataset.rb +179 -0
- data/spec/files/dunnerdale_2009.csv +277 -0
- data/spec/files/long_duddon_2008.csv +230 -0
- data/spec/lib/duration_spec.rb +50 -0
- data/spec/models/race_category_spec.rb +65 -0
- data/spec/models/race_club_spec.rb +32 -0
- data/spec/models/race_instance_spec.rb +104 -0
- data/spec/models/race_performance_spec.rb +7 -0
- data/spec/models/race_performance_status_spec.rb +31 -0
- data/spec/models/race_spec.rb +39 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +36 -0
- data/vendor/plugins/acts_as_list/README +23 -0
- data/vendor/plugins/acts_as_list/init.rb +3 -0
- data/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb +256 -0
- data/vendor/plugins/acts_as_list/test/list_test.rb +332 -0
- metadata +195 -0
data/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Race Results
|
|
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.
|
|
4
|
+
|
|
5
|
+
# Author
|
|
6
|
+
|
|
7
|
+
William Ross for spanner
|
|
8
|
+
will at spanner dot org, or messages via github are welcome.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'jeweler'
|
|
3
|
+
Jeweler::Tasks.new do |gem|
|
|
4
|
+
gem.name = "radiant-race_results-extension"
|
|
5
|
+
gem.summary = %Q{Race results analysis and presentation for the Radiant CMS}
|
|
6
|
+
gem.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.}
|
|
7
|
+
gem.email = "will@spanner.org"
|
|
8
|
+
gem.homepage = "http://github.com/radiant/radiant-race_results-extension"
|
|
9
|
+
gem.authors = ["spanner"]
|
|
10
|
+
gem.add_dependency "radiant", ">= 0.9.0"
|
|
11
|
+
gem.add_dependency "radiant-layouts-extension"
|
|
12
|
+
end
|
|
13
|
+
rescue LoadError
|
|
14
|
+
puts "Jeweler (or a dependency) not available. This is only required if you plan to package event_calendar as a gem."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# In rails 1.2, plugins aren't available in the path until they're loaded.
|
|
18
|
+
# Check to see if the rspec plugin is installed first and require
|
|
19
|
+
# it if it is. If not, use the gem version.
|
|
20
|
+
|
|
21
|
+
# Determine where the RSpec plugin is by loading the boot
|
|
22
|
+
unless defined? RADIANT_ROOT
|
|
23
|
+
ENV["RAILS_ENV"] = "test"
|
|
24
|
+
case
|
|
25
|
+
when ENV["RADIANT_ENV_FILE"]
|
|
26
|
+
require File.dirname(ENV["RADIANT_ENV_FILE"]) + "/boot"
|
|
27
|
+
when File.dirname(__FILE__) =~ %r{vendor/radiant/vendor/extensions}
|
|
28
|
+
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../")}/config/boot"
|
|
29
|
+
else
|
|
30
|
+
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../")}/config/boot"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
require 'rake'
|
|
35
|
+
require 'rake/rdoctask'
|
|
36
|
+
require 'rake/testtask'
|
|
37
|
+
|
|
38
|
+
rspec_base = File.expand_path(RADIANT_ROOT + '/vendor/plugins/rspec/lib')
|
|
39
|
+
$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
|
|
40
|
+
require 'spec/rake/spectask'
|
|
41
|
+
require 'cucumber'
|
|
42
|
+
require 'cucumber/rake/task'
|
|
43
|
+
|
|
44
|
+
# Cleanup the RADIANT_ROOT constant so specs will load the environment
|
|
45
|
+
Object.send(:remove_const, :RADIANT_ROOT)
|
|
46
|
+
|
|
47
|
+
extension_root = File.expand_path(File.dirname(__FILE__))
|
|
48
|
+
|
|
49
|
+
task :default => :spec
|
|
50
|
+
task :stats => "spec:statsetup"
|
|
51
|
+
|
|
52
|
+
desc "Run all specs in spec directory"
|
|
53
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
|
54
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
|
55
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
task :features => 'spec:integration'
|
|
59
|
+
|
|
60
|
+
namespace :spec do
|
|
61
|
+
desc "Run all specs in spec directory with RCov"
|
|
62
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
|
63
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
|
64
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
|
65
|
+
t.rcov = true
|
|
66
|
+
t.rcov_opts = ['--exclude', 'spec', '--rails']
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
desc "Print Specdoc for all specs"
|
|
70
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
|
71
|
+
t.spec_opts = ["--format", "specdoc", "--dry-run"]
|
|
72
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
[:models, :controllers, :views, :helpers].each do |sub|
|
|
76
|
+
desc "Run the specs under spec/#{sub}"
|
|
77
|
+
Spec::Rake::SpecTask.new(sub) do |t|
|
|
78
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
|
79
|
+
t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
desc "Run the Cucumber features"
|
|
84
|
+
Cucumber::Rake::Task.new(:integration) do |t|
|
|
85
|
+
t.fork = true
|
|
86
|
+
t.cucumber_opts = ['--format', (ENV['CUCUMBER_FORMAT'] || 'pretty')]
|
|
87
|
+
# t.feature_pattern = "#{extension_root}/features/**/*.feature"
|
|
88
|
+
t.profile = "default"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Setup specs for stats
|
|
92
|
+
task :statsetup do
|
|
93
|
+
require 'code_statistics'
|
|
94
|
+
::STATS_DIRECTORIES << %w(Model\ specs spec/models)
|
|
95
|
+
::STATS_DIRECTORIES << %w(View\ specs spec/views)
|
|
96
|
+
::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers)
|
|
97
|
+
::STATS_DIRECTORIES << %w(Helper\ specs spec/views)
|
|
98
|
+
::CodeStatistics::TEST_TYPES << "Model specs"
|
|
99
|
+
::CodeStatistics::TEST_TYPES << "View specs"
|
|
100
|
+
::CodeStatistics::TEST_TYPES << "Controller specs"
|
|
101
|
+
::CodeStatistics::TEST_TYPES << "Helper specs"
|
|
102
|
+
::STATS_DIRECTORIES.delete_if {|a| a[0] =~ /test/}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
namespace :db do
|
|
106
|
+
namespace :fixtures do
|
|
107
|
+
desc "Load fixtures (from spec/fixtures) into the current environment's database. Load specific fixtures using FIXTURES=x,y"
|
|
108
|
+
task :load => :environment do
|
|
109
|
+
require 'active_record/fixtures'
|
|
110
|
+
ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
|
|
111
|
+
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'spec', 'fixtures', '*.{yml,csv}'))).each do |fixture_file|
|
|
112
|
+
Fixtures.create_fixtures('spec/fixtures', File.basename(fixture_file, '.*'))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
desc 'Generate documentation for the race_results extension.'
|
|
120
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
121
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
122
|
+
rdoc.title = 'RaceResultsExtension'
|
|
123
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
|
124
|
+
rdoc.rdoc_files.include('README')
|
|
125
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# For extensions that are in transition
|
|
129
|
+
desc 'Test the race_results extension.'
|
|
130
|
+
Rake::TestTask.new(:test) do |t|
|
|
131
|
+
t.libs << 'lib'
|
|
132
|
+
t.pattern = 'test/**/*_test.rb'
|
|
133
|
+
t.verbose = true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Load any custom rakefiles for extension
|
|
137
|
+
Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class Admin::RaceClubsController < Admin::ResourceController
|
|
2
|
+
helper :races
|
|
3
|
+
before_filter :get_club, :only => [:edit, :update, :delete]
|
|
4
|
+
before_filter :make_club, :only => [:new, :create]
|
|
5
|
+
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def get_club
|
|
9
|
+
@race_club = RaceClub.find(params[:id])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def make_club
|
|
13
|
+
@race_club = RaceClub.new(params[:race_club])
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class Admin::RaceCompetitorsController < Admin::ResourceController
|
|
2
|
+
helper :races
|
|
3
|
+
before_filter :get_competitor, :only => [:edit, :update, :delete]
|
|
4
|
+
before_filter :make_competitor, :only => [:new, :create]
|
|
5
|
+
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def get_competitor
|
|
9
|
+
@race_competitor = RaceCompetitor.find(params[:id])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def make_competitor
|
|
13
|
+
@race_competitor = RaceCompetitor.new(params[:race_competitor])
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class Admin::RaceInstancesController < Admin::ResourceController
|
|
2
|
+
|
|
3
|
+
protected
|
|
4
|
+
|
|
5
|
+
def continue_url(options)
|
|
6
|
+
admin_races_url
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def load_model
|
|
10
|
+
@race = Race.find_by_slug(params[:race_id])
|
|
11
|
+
self.model = if params[:id]
|
|
12
|
+
@race.instances.find_by_slug(params[:id])
|
|
13
|
+
else
|
|
14
|
+
@race.instances.build
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class Admin::RacesController < Admin::ResourceController
|
|
2
|
+
helper :races
|
|
3
|
+
|
|
4
|
+
def update
|
|
5
|
+
Rails.logger.warn "updating with #{params[model_symbol].inspect}"
|
|
6
|
+
model.update_attributes!(params[model_symbol])
|
|
7
|
+
response_for :update
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def continue_url(options)
|
|
11
|
+
options[:redirect_to] || (params[:continue] ? {:action => 'edit', :id => model.slug} : {:action => "index"})
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def load_model
|
|
15
|
+
Rails.logger.warn "loading model with id #{params[:id].inspect}"
|
|
16
|
+
|
|
17
|
+
self.model = if params[:id]
|
|
18
|
+
model_class.find(params[:id])
|
|
19
|
+
else
|
|
20
|
+
model_class.new
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
def load_model
|
|
27
|
+
self.model = if params[:id]
|
|
28
|
+
model_class.find_by_slug(params[:id])
|
|
29
|
+
else
|
|
30
|
+
model_class.new
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class RaceInstancesController < ApplicationController
|
|
2
|
+
radiant_layout { |controller| controller.layout_for :races }
|
|
3
|
+
no_login_required
|
|
4
|
+
before_filter :get_race, :only => :show
|
|
5
|
+
|
|
6
|
+
def show
|
|
7
|
+
@instance = @race.instances.find_by_slug(params[:slug])
|
|
8
|
+
@performances = @instance.performances
|
|
9
|
+
if params[:club] && @club = RaceClub.find(params[:club])
|
|
10
|
+
@performances = @performances.by_members_of(@club)
|
|
11
|
+
end
|
|
12
|
+
if params[:cat] && @category = @instance.categories.find_by_name(params[:cat])
|
|
13
|
+
@performances = @performances.eligible_for_category(@category)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def get_race
|
|
20
|
+
@race = Race.find_by_slug(params[:race_slug])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class RacePerformancesController < ApplicationController
|
|
2
|
+
radiant_layout { |controller| controller.layout_for :races }
|
|
3
|
+
no_login_required
|
|
4
|
+
before_filter :get_race, :only => :show
|
|
5
|
+
|
|
6
|
+
def show
|
|
7
|
+
@performance = @race.performances.find(params[:id])
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def get_race
|
|
13
|
+
@race = Race.find(params[:race_id])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class RacesController < ApplicationController
|
|
2
|
+
radiant_layout { |controller| controller.layout_for :races }
|
|
3
|
+
no_login_required
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
@race = Race.find_by_slug(params[:slug])
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@races = Race.all.paginate(pagination_parameters)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module RacesHelper
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Support methods for nested forms lifted from http://transfs.com/devblog/2009/06/26/nested-forms-with-rails-2-3-helpers-and-javascript-tricks/
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
def link_to_new_nested_form(name, form_builder, method, options = {})
|
|
8
|
+
klass = form_builder.object.class.reflect_on_association(method).klass
|
|
9
|
+
Rails.logger.warn "!! link_to_new_nested_form: klass is #{klass.inspect}"
|
|
10
|
+
options[:object] ||= klass.new
|
|
11
|
+
options[:partial] ||= method.to_s.singularize
|
|
12
|
+
options[:form_builder_local] ||= :f
|
|
13
|
+
options[:element_id] ||= method.to_s
|
|
14
|
+
options[:position] ||= :bottom
|
|
15
|
+
link_to_function name do |page|
|
|
16
|
+
html = generate_nested_form(form_builder,
|
|
17
|
+
method,
|
|
18
|
+
:object => options[:object],
|
|
19
|
+
:partial => options[:partial],
|
|
20
|
+
:form_builder_local => options[:form_builder_local]
|
|
21
|
+
)
|
|
22
|
+
page << %{
|
|
23
|
+
$('#{options[:element_id]}').insert({ #{options[:position]}: "#{ escape_javascript html }".replace(/NEW_RECORD/g, new Date().getTime()) });
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def generate_nested_form(form_builder, method, options = {})
|
|
29
|
+
options[:object] ||= form_builder.object.class.reflect_on_association(method).klass.new
|
|
30
|
+
options[:partial] ||= method.to_s.singularize
|
|
31
|
+
options[:form_builder_local] ||= :f
|
|
32
|
+
|
|
33
|
+
form_builder.fields_for(method, options[:object], :child_index => 'NEW_RECORD') do |f|
|
|
34
|
+
render(:partial => options[:partial], :locals => { options[:form_builder_local] => f })
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
data/app/models/race.rb
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
class Race < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
has_site if respond_to? :has_site
|
|
4
|
+
belongs_to :created_by, :class_name => 'User'
|
|
5
|
+
belongs_to :updated_by, :class_name => 'User'
|
|
6
|
+
has_many :instances, :class_name => 'RaceInstance'
|
|
7
|
+
|
|
8
|
+
has_many :records, :class_name => 'RaceRecord'
|
|
9
|
+
accepts_nested_attributes_for :records, :allow_destroy => true
|
|
10
|
+
|
|
11
|
+
has_many :checkpoints, :class_name => 'RaceCheckpoint'
|
|
12
|
+
accepts_nested_attributes_for :checkpoints, :allow_destroy => true
|
|
13
|
+
|
|
14
|
+
belongs_to :picture_asset, :class_name => 'Asset'
|
|
15
|
+
accepts_nested_attributes_for :picture_asset, :allow_destroy => true
|
|
16
|
+
|
|
17
|
+
belongs_to :map_asset, :class_name => 'Asset'
|
|
18
|
+
accepts_nested_attributes_for :map_asset, :allow_destroy => true
|
|
19
|
+
|
|
20
|
+
validates_presence_of :name, :slug
|
|
21
|
+
validates_uniqueness_of :name, :slug
|
|
22
|
+
validates_length_of :slug, :maximum => 100, :message => '{{count}}-character limit'
|
|
23
|
+
validates_format_of :slug, :with => %r{^([-_.A-Za-z0-9]*|)$}, :message => 'not URL-friendly'
|
|
24
|
+
|
|
25
|
+
object_id_attr :filter, TextFilter
|
|
26
|
+
|
|
27
|
+
default_scope :order => 'name ASC'
|
|
28
|
+
|
|
29
|
+
def to_param
|
|
30
|
+
slug
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def latest
|
|
34
|
+
instances.past.first # they sort by start time desc
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def in(name)
|
|
38
|
+
instances.find_by_name(name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def next
|
|
42
|
+
instances.future.first
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def record(category_name="M")
|
|
46
|
+
category = RaceCategory.find_or_create_by_normalized_name(category_name)
|
|
47
|
+
if new_record?
|
|
48
|
+
self.records.build(:race_category => category)
|
|
49
|
+
else
|
|
50
|
+
RaceRecord.find_or_create_by_race_id_and_race_category_id(self.id, category.id)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def checkpoint_before(cp)
|
|
55
|
+
checkpoints.at(checkpoints.index(cp) - 1) if checkpoints.contain?(cp) and checkpoints.first != cp
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def checkpoint_after(cp)
|
|
59
|
+
checkpoints.at(checkpoints.index(cp) + 1) if checkpoints.contain?(cp) and checkpoints.last != cp
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
class RaceCategory < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
belongs_to :created_by, :class_name => 'User'
|
|
4
|
+
belongs_to :updated_by, :class_name => 'User'
|
|
5
|
+
validates_presence_of :name
|
|
6
|
+
|
|
7
|
+
# RaceCategory.within returns the list of categories (including this one) in which performances are eligible for prizes
|
|
8
|
+
# in this category. Eg. for MV50 the list will include MV50, MV60 and MV70.
|
|
9
|
+
|
|
10
|
+
named_scope :within, lambda {|category|
|
|
11
|
+
category = RaceCategory.find_by_name(category) unless category.is_a? RaceCategory
|
|
12
|
+
conditions = []
|
|
13
|
+
bound = {}
|
|
14
|
+
if category.age_above
|
|
15
|
+
conditions.push "race_categories.age_above >= :above"
|
|
16
|
+
bound[:above] = category.age_above
|
|
17
|
+
end
|
|
18
|
+
if category.age_below
|
|
19
|
+
conditions.push "race_categories.age_below <= :below"
|
|
20
|
+
bound[:below] = category.age_below
|
|
21
|
+
end
|
|
22
|
+
if category.gender
|
|
23
|
+
conditions.push "race_categories.gender = :gender"
|
|
24
|
+
bound[:gender] = category.gender
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
{ :conditions => [conditions.join(' AND '), bound] }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# RaceCategory.containing returns the list of categories in which a performance in this category is eligible for prizes
|
|
31
|
+
# eg. for MV50, the list will include M, MV40 and MV50.
|
|
32
|
+
# The list will always include both the passed category and any category of the right gender that has no other restrictions
|
|
33
|
+
|
|
34
|
+
named_scope :containing, lambda {|category|
|
|
35
|
+
category = RaceCategory.find_by_name(category) unless category.is_a? RaceCategory
|
|
36
|
+
conditions = []
|
|
37
|
+
bound = {}
|
|
38
|
+
if category.gender
|
|
39
|
+
conditions.push "(race_categories.gender = :gender)"
|
|
40
|
+
bound[:gender] = category.gender
|
|
41
|
+
end
|
|
42
|
+
if category.age_above
|
|
43
|
+
conditions.push "(race_categories.age_above IS NULL OR race_categories.age_above <= :above)"
|
|
44
|
+
bound[:above] = category.age_above
|
|
45
|
+
else
|
|
46
|
+
conditions.push "(race_categories.age_above IS NULL)"
|
|
47
|
+
end
|
|
48
|
+
if category.age_below
|
|
49
|
+
conditions.push "(race_categories.age_below IS NULL OR race_categories.age_below >= :below)"
|
|
50
|
+
bound[:below] = category.age_below
|
|
51
|
+
else
|
|
52
|
+
conditions.push "(race_categories.age_below IS NULL)"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
{ :conditions => [conditions.join(' AND '), bound] }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
default_scope :order => 'name ASC'
|
|
59
|
+
|
|
60
|
+
def to_param
|
|
61
|
+
name
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.find_or_create_by_normalized_name(name)
|
|
65
|
+
name = normalized_name(name)
|
|
66
|
+
unless category = find_by_name(name)
|
|
67
|
+
category = new(:name => name)
|
|
68
|
+
if matches = name.match(/^([A-Z]+)(\d*)$/)
|
|
69
|
+
if matches[2]
|
|
70
|
+
category.send(matches[1].match(/U/) ? :age_below= : :age_above=, matches[2])
|
|
71
|
+
end
|
|
72
|
+
category.gender = matches[1].match(/L/) ? "F" : "M"
|
|
73
|
+
end
|
|
74
|
+
category.save!
|
|
75
|
+
end
|
|
76
|
+
category
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
protected
|
|
80
|
+
|
|
81
|
+
def self.normalized_name(name)
|
|
82
|
+
name.gsub!(/\W*/, '')
|
|
83
|
+
return "M" if name.blank?
|
|
84
|
+
name.upcase!
|
|
85
|
+
case name
|
|
86
|
+
when /M(\d\d)/
|
|
87
|
+
"MV#{$1}"
|
|
88
|
+
when /L(\d\d)/
|
|
89
|
+
"LV#{$1}"
|
|
90
|
+
when "F"
|
|
91
|
+
"L"
|
|
92
|
+
when /F(\d\d)/
|
|
93
|
+
"LV#{$1}"
|
|
94
|
+
when /FV(\d\d)/
|
|
95
|
+
"LV#{$1}"
|
|
96
|
+
when /FU(\d\d)/
|
|
97
|
+
"LU#{$1}"
|
|
98
|
+
when "W"
|
|
99
|
+
"L"
|
|
100
|
+
when /W(\d\d)/
|
|
101
|
+
"LV#{$1}"
|
|
102
|
+
when /WV(\d\d)/
|
|
103
|
+
"LV#{$1}"
|
|
104
|
+
when /WU(\d\d)/
|
|
105
|
+
"LU#{$1}"
|
|
106
|
+
when "", nil
|
|
107
|
+
"M"
|
|
108
|
+
else
|
|
109
|
+
name
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
end
|
|
114
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class RaceCheckpoint < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
has_site if respond_to? :has_site
|
|
4
|
+
belongs_to :created_by, :class_name => 'User'
|
|
5
|
+
belongs_to :updated_by, :class_name => 'User'
|
|
6
|
+
belongs_to :race
|
|
7
|
+
acts_as_list :scope => :race_id
|
|
8
|
+
has_many :times, :class_name => 'RaceCheckpointTime'
|
|
9
|
+
|
|
10
|
+
default_scope :order => :position
|
|
11
|
+
|
|
12
|
+
named_scope :in, lambda {|race|
|
|
13
|
+
{
|
|
14
|
+
:conditions => ['race_id = ?', race.id]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def previous
|
|
19
|
+
race.checkpoint_before(self)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def next
|
|
23
|
+
race.checkpoint_after(self)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.find_by_normalized_name(name)
|
|
27
|
+
find_by_name(name.gsub(/\s+/, "_").downcase)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
class RaceCheckpointTime < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
has_site if respond_to? :has_site
|
|
4
|
+
belongs_to :created_by, :class_name => 'User'
|
|
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
|
|
9
|
+
|
|
10
|
+
# validates_presence_of :checkpoint, :performance
|
|
11
|
+
|
|
12
|
+
named_scope :time_at, lambda {|checkpoint|
|
|
13
|
+
{
|
|
14
|
+
:conditions => ["race_checkpoint_id = ?", checkpoint.id]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
named_scope :time_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 :quicker_than, lambda {|duration|
|
|
26
|
+
{
|
|
27
|
+
:conditions => ["elapsed_time < ?", duration]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def position
|
|
32
|
+
checkpoint.times.in(race_instance).quicker_than(duration).count + 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def elapsed_time
|
|
36
|
+
if s = read_attribute(:elapsed_time)
|
|
37
|
+
s.to_timecode
|
|
38
|
+
else
|
|
39
|
+
""
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def elapsed_time=(time)
|
|
44
|
+
write_attribute(:elapsed_time, time.seconds) # numbers will pass through unchanged. strings will be timecode-parsed
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def previous
|
|
48
|
+
performance.checkpoint_times.time_at(checkpoint.previous) if checkpoint.previous
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def previous_position
|
|
52
|
+
if previous
|
|
53
|
+
previous.position
|
|
54
|
+
else
|
|
55
|
+
1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def gain
|
|
60
|
+
sprintf('+%d', position - previous_position)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
class RaceClub < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
belongs_to :created_by, :class_name => 'User'
|
|
4
|
+
belongs_to :updated_by, :class_name => 'User'
|
|
5
|
+
has_many :performances, :class_name => 'RacePerformance'
|
|
6
|
+
has_many :competitors, :class_name => 'RaceCompetitor'
|
|
7
|
+
|
|
8
|
+
has_many :other_names, :class_name => 'RaceClubAlias'
|
|
9
|
+
accepts_nested_attributes_for :other_names, :allow_destroy => true
|
|
10
|
+
|
|
11
|
+
validates_presence_of :name
|
|
12
|
+
default_scope :order => 'name ASC'
|
|
13
|
+
|
|
14
|
+
named_scope :by_name_or_alias, lambda { |name|
|
|
15
|
+
{
|
|
16
|
+
:select => 'race_clubs.*',
|
|
17
|
+
:joins => "LEFT JOIN race_club_aliases AS aliases ON aliases.race_club_id = race_clubs.id",
|
|
18
|
+
:conditions => ["race_clubs.name LIKE :name OR aliases.name LIKE :name", {:name => "#{normalize_name(name)}%"}],
|
|
19
|
+
:group => "race_clubs.id"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def self.find_by_any_name(name)
|
|
24
|
+
by_name_or_alias(name).first
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.find_or_create_by_any_name(name=nil)
|
|
28
|
+
name = normalize_name(name)
|
|
29
|
+
return if name.blank?
|
|
30
|
+
return if %w{unattached ua u/a}.include? name.downcase
|
|
31
|
+
unless club = find_by_any_name(name)
|
|
32
|
+
club = new(:name => name)
|
|
33
|
+
club.save!
|
|
34
|
+
end
|
|
35
|
+
club
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#TODO make this a lot more UTF-friendly:
|
|
39
|
+
# strip control characters and excel mmska without losing accents and symbols?
|
|
40
|
+
|
|
41
|
+
def self.normalize_name(name="")
|
|
42
|
+
name.gsub!(/\B&\B/, "and")
|
|
43
|
+
name.gsub!(/\s+/, " ")
|
|
44
|
+
name.gsub!(/[^A-Za-z -,]/, "")
|
|
45
|
+
name.strip
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class RaceCompetitor < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
belongs_to :created_by, :class_name => 'User'
|
|
4
|
+
belongs_to :updated_by, :class_name => 'User'
|
|
5
|
+
belongs_to :reader
|
|
6
|
+
belongs_to :race_club#, :class_name => 'RaceClub'
|
|
7
|
+
has_many :performances, :class_name => 'RacePerformance'
|
|
8
|
+
default_scope :order => 'name ASC'
|
|
9
|
+
|
|
10
|
+
def club
|
|
11
|
+
race_club
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.find_or_create_by_name_and_club(name="", club=nil)
|
|
15
|
+
return if name.blank?
|
|
16
|
+
club_id = club.nil? ? nil : club.id
|
|
17
|
+
find_or_create_by_name_and_race_club_id(name, club_id)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|