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.
Files changed (91) hide show
  1. data/README.md +8 -0
  2. data/Rakefile +137 -0
  3. data/app/controllers/admin/race_clubs_controller.rb +15 -0
  4. data/app/controllers/admin/race_competitors_controller.rb +15 -0
  5. data/app/controllers/admin/race_instances_controller.rb +17 -0
  6. data/app/controllers/admin/races_controller.rb +35 -0
  7. data/app/controllers/race_instances_controller.rb +23 -0
  8. data/app/controllers/race_performances_controller.rb +16 -0
  9. data/app/controllers/races_controller.rb +13 -0
  10. data/app/helpers/races_helper.rb +38 -0
  11. data/app/models/race.rb +64 -0
  12. data/app/models/race_category.rb +114 -0
  13. data/app/models/race_checkpoint.rb +31 -0
  14. data/app/models/race_checkpoint_time.rb +64 -0
  15. data/app/models/race_club.rb +48 -0
  16. data/app/models/race_club_alias.rb +9 -0
  17. data/app/models/race_competitor.rb +20 -0
  18. data/app/models/race_instance.rb +178 -0
  19. data/app/models/race_page.rb +69 -0
  20. data/app/models/race_performance.rb +175 -0
  21. data/app/models/race_performance_status.rb +58 -0
  22. data/app/models/race_record.rb +27 -0
  23. data/app/views/admin/race_checkpoints/_checkpoint.html.haml +19 -0
  24. data/app/views/admin/race_club_aliases/_race_club_alias.html.haml +10 -0
  25. data/app/views/admin/race_clubs/_club.html.haml +19 -0
  26. data/app/views/admin/race_clubs/_form.html.haml +21 -0
  27. data/app/views/admin/race_clubs/edit.html.haml +20 -0
  28. data/app/views/admin/race_clubs/index.html.haml +27 -0
  29. data/app/views/admin/race_competitors/_competitor.html.haml +14 -0
  30. data/app/views/admin/race_competitors/_form.html.haml +21 -0
  31. data/app/views/admin/race_competitors/edit.html.haml +20 -0
  32. data/app/views/admin/race_competitors/index.html.haml +27 -0
  33. data/app/views/admin/race_instances/_form.html.haml +98 -0
  34. data/app/views/admin/race_instances/edit.html.haml +17 -0
  35. data/app/views/admin/race_instances/new.html.haml +18 -0
  36. data/app/views/admin/race_records/_record.html.haml +20 -0
  37. data/app/views/admin/races/_form.html.haml +82 -0
  38. data/app/views/admin/races/_race.html.haml +36 -0
  39. data/app/views/admin/races/edit.html.haml +19 -0
  40. data/app/views/admin/races/index.html.haml +26 -0
  41. data/app/views/admin/races/new.html.haml +19 -0
  42. data/app/views/race_categories/show.html.haml +36 -0
  43. data/app/views/race_clubs/show.html.haml +25 -0
  44. data/app/views/race_instances/index.html.haml +14 -0
  45. data/app/views/race_instances/show.html.haml +52 -0
  46. data/app/views/race_performances/_headings.html.haml +4 -0
  47. data/app/views/race_performances/_performance.html.haml +21 -0
  48. data/app/views/races/index.html.haml +38 -0
  49. data/app/views/races/show.html.haml +81 -0
  50. data/cucumber.yml +1 -0
  51. data/db/migrate/20091116130836_race_data.rb +128 -0
  52. data/db/migrate/20091124095157_competitor_details.rb +9 -0
  53. data/db/migrate/20091126110634_club_aliases.rb +17 -0
  54. data/db/migrate/20091223094002_instance_details.rb +11 -0
  55. data/db/migrate/20091224100524_race_distance.rb +11 -0
  56. data/db/migrate/20091224100734_race_records.rb +19 -0
  57. data/db/migrate/20091224105637_category_details.rb +13 -0
  58. data/db/migrate/20091224115909_filters.rb +11 -0
  59. data/db/migrate/20091228122837_record_holder.rb +11 -0
  60. data/db/migrate/20100106104850_remember_calculations.rb +9 -0
  61. data/db/migrate/20100426104801_race_attachments.rb +11 -0
  62. data/features/support/env.rb +16 -0
  63. data/features/support/paths.rb +14 -0
  64. data/lib/duration_extensions.rb +61 -0
  65. data/lib/race_results/admin_ui.rb +76 -0
  66. data/lib/race_results/race_tags.rb +722 -0
  67. data/lib/tasks/race_results_extension_tasks.rake +28 -0
  68. data/public/images/admin/calendar_down.png +0 -0
  69. data/public/images/admin/new-race.png +0 -0
  70. data/public/javascripts/admin/races.js +29 -0
  71. data/public/stylesheets/sass/admin/races.sass +131 -0
  72. data/race_results_extension.rb +38 -0
  73. data/spec/datasets/competitors_dataset.rb +81 -0
  74. data/spec/datasets/race_sites_dataset.rb +8 -0
  75. data/spec/datasets/races_dataset.rb +179 -0
  76. data/spec/files/dunnerdale_2009.csv +277 -0
  77. data/spec/files/long_duddon_2008.csv +230 -0
  78. data/spec/lib/duration_spec.rb +50 -0
  79. data/spec/models/race_category_spec.rb +65 -0
  80. data/spec/models/race_club_spec.rb +32 -0
  81. data/spec/models/race_instance_spec.rb +104 -0
  82. data/spec/models/race_performance_spec.rb +7 -0
  83. data/spec/models/race_performance_status_spec.rb +31 -0
  84. data/spec/models/race_spec.rb +39 -0
  85. data/spec/spec.opts +6 -0
  86. data/spec/spec_helper.rb +36 -0
  87. data/vendor/plugins/acts_as_list/README +23 -0
  88. data/vendor/plugins/acts_as_list/init.rb +3 -0
  89. data/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb +256 -0
  90. data/vendor/plugins/acts_as_list/test/list_test.rb +332 -0
  91. 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
@@ -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,9 @@
1
+ class RaceClubAlias < ActiveRecord::Base
2
+
3
+ belongs_to :created_by, :class_name => 'User'
4
+ belongs_to :updated_by, :class_name => 'User'
5
+ belongs_to :club, :class_name => 'RaceClub'
6
+
7
+ validates_presence_of :name
8
+ end
9
+
@@ -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
+