diff --git a/app/models/course.rb b/app/models/course.rb index 2e55a93c9..db856ad61 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -68,6 +68,11 @@ def visible_events events.visible end + # @return [Event::ActiveRecord_AssociationRelation] + def projectable_events + events.used_for_projections + end + # @return [String, nil] def home_time_zone events.latest&.home_time_zone diff --git a/app/models/event.rb b/app/models/event.rb index 2d227a563..94d15af81 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -43,6 +43,7 @@ class Event < ApplicationRecord after_touch :notify_event_update, if: :topic_resource_key? scope :name_search, ->(search_param) { where("events.name ILIKE ?", "%#{search_param}%") } + scope :used_for_projections, -> { where(use_for_projections: true) } scope :select_with_params, lambda { |search_param| search(search_param) .left_joins(:efforts).left_joins(:event_group) diff --git a/app/models/projection.rb b/app/models/projection.rb index 892661a80..e71c4ecf8 100644 --- a/app/models/projection.rb +++ b/app/models/projection.rb @@ -39,11 +39,10 @@ def self.sql(split_time:, starting_time_point:, subject_time_points:, ignore_tim relevant_event_ids as ( select events.id as event_id from events - join event_groups on event_groups.id = events.event_group_id join courses on courses.id = events.course_id join splits on splits.course_id = courses.id where splits.id = #{starting_split_id} - and (event_groups.concealed is false or event_groups.concealed is null) + and events.use_for_projections is true and (#{ignore_timestamp} is null or events.scheduled_start_time <= #{ignore_timestamp}) order by scheduled_start_time desc limit #{EVENT_LOOKBACK_COUNT} diff --git a/app/parameters/event_parameters.rb b/app/parameters/event_parameters.rb index 4420dea5e..c6d1bb088 100644 --- a/app/parameters/event_parameters.rb +++ b/app/parameters/event_parameters.rb @@ -14,6 +14,7 @@ def self.permitted :results_template_id, :notice_text, :lottery_id, + :use_for_projections, ] end diff --git a/app/view_models/plan_display.rb b/app/view_models/plan_display.rb index 16819fa8b..3c959c41d 100644 --- a/app/view_models/plan_display.rb +++ b/app/view_models/plan_display.rb @@ -3,18 +3,26 @@ class PlanDisplay < EffortWithLapSplitRows include CourseAnalysisMethods include TimeFormats + attr_reader :course, :error_messages delegate :name, :organization, :simple?, to: :course delegate :multiple_laps?, to: :event, allow_nil: true - def initialize(args) + def initialize(args) # rubocop:disable Lint/MissingSuper @course = args[:course] @params = args[:params] @error_messages = [] validate_setup end + # Overrides CourseAnalysisMethods#event: planning anchors on the same + # events the projection engine draws data from, so a course whose only + # events are concealed seed events can still plan + def event + @event ||= course.projectable_events.latest + end + def effort @effort ||= event.efforts.new end @@ -103,7 +111,7 @@ def lap_splits end def validate_setup - error_messages << "No events have been held on this course." if course.visible_events.empty? + error_messages << "No events on this course are available for planning." if course.projectable_events.empty? AssignSegmentTimes.perform(ordered_split_times) if error_messages.empty? rescue ArgumentError => e error_messages << e.message diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb index 8dd328faa..1aacc8fac 100644 --- a/app/views/events/_form.html.erb +++ b/app/views/events/_form.html.erb @@ -197,6 +197,17 @@ <%= form.text_field :beacon_url, class: "form-control", placeholder: "example.com/tracking/my-event" %> + +
+
+ <%= form.label :use_for_projections, "Use this event's times for projections", class: "me-2" %> + + <%= form.check_box :use_for_projections %> +
+

diff --git a/spec/models/projection_spec.rb b/spec/models/projection_spec.rb index cf9bf4986..de1f00b88 100644 --- a/spec/models/projection_spec.rb +++ b/spec/models/projection_spec.rb @@ -85,6 +85,22 @@ end end + context "when an event on the course is flagged not to be used for projections" do + before { events(:hardrock_2014).update_column(:use_for_projections, false) } + + it "does not draw from that event's efforts" do + expect(subject.first.effort_years).to eq([2016]) + end + end + + context "when an event in a concealed event group is flagged for projections" do + before { event_groups(:hardrock_2014).update_column(:concealed, true) } + + it "draws from that event's efforts" do + expect(subject.first.effort_years).to eq([2014, 2016]) + end + end + context "when given a starting split time" do let(:split_time) { effort.ordered_split_times.first } it "returns an empty array" do diff --git a/spec/system/plan_effort_flow_spec.rb b/spec/system/plan_effort_flow_spec.rb index 4b404848b..dc4ce4fe1 100644 --- a/spec/system/plan_effort_flow_spec.rb +++ b/spec/system/plan_effort_flow_spec.rb @@ -50,11 +50,11 @@ visit_page verify_content_present(course) - expect(page).to have_content("No events have been held on this course.") + expect(page).to have_content("No events on this course are available for planning.") end end - scenario "Inputmask works", js: true do + scenario "Inputmask works", :js do visit_page expected_values = { diff --git a/spec/view_models/plan_display_spec.rb b/spec/view_models/plan_display_spec.rb new file mode 100644 index 000000000..6b9cd2da3 --- /dev/null +++ b/spec/view_models/plan_display_spec.rb @@ -0,0 +1,32 @@ +require "rails_helper" + +RSpec.describe PlanDisplay do + subject { described_class.new(course: course, params: ActionController::Parameters.new(params)) } + + let(:course) { courses(:hardrock_ccw) } + let(:params) { { expected_time: "38:00" } } + + context "when the course has visible events with relevant data" do + it "returns no errors and builds projected split times" do + expect(subject.error_messages).to be_empty + expect(subject.ordered_split_times).to be_present + end + end + + context "when the course's only event is in a concealed event group but flagged for projections" do + before { event_groups(:hardrock_2015).update_column(:concealed, true) } + + it "returns no errors and builds projected split times" do + expect(subject.error_messages).to be_empty + expect(subject.ordered_split_times).to be_present + end + end + + context "when no events on the course are flagged for projections" do + before { course.events.each { |event| event.update_column(:use_for_projections, false) } } + + it "returns an error message" do + expect(subject.error_messages).to include("No events on this course are available for planning.") + end + end +end