diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3dacbd13926..4de021227b5 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -49,8 +49,9 @@ Metrics/BlockLength: Exclude: - 'config/environments/production.rb' - 'config/initializers/doorkeeper.rb' + - 'test/presenters/signup_count_presenter_test.rb' -# Offense count: 5 +# Offense count: 7 # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ClassLength: Exclude: @@ -59,6 +60,26 @@ Metrics/ClassLength: - 'app/graphql/types/ability_type.rb' - 'app/graphql/types/mutation_type.rb' - 'app/graphql/types/query_type.rb' + - 'app/presenters/signup_count_presenter.rb' + - 'test/presenters/signup_count_presenter_test.rb' + +# Offense count: 1 +# Configuration parameters: Max. +Metrics/ParameterLists: + Exclude: + - 'app/presenters/signup_count_presenter.rb' + +# Offense count: 1 +# Configuration parameters: ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. +# ForbiddenPrefixes: is_, has_, have_ +Naming/PredicateMethod: + Exclude: + - 'app/presenters/signup_count_presenter.rb' + +# Offense count: 1 +Naming/PredicatePrefix: + Exclude: + - 'app/presenters/signup_count_presenter.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). diff --git a/app/presenters/signup_count_presenter.rb b/app/presenters/signup_count_presenter.rb index 720004cfc69..4395f8f225d 100644 --- a/app/presenters/signup_count_presenter.rb +++ b/app/presenters/signup_count_presenter.rb @@ -3,7 +3,7 @@ class SignupCountPresenter include SortBuckets include ActionView::Helpers::TextHelper - DIMENSIONS = %i[state bucket_key requested_bucket_key counted team_member] + DIMENSIONS = %i[state bucket_key requested_bucket_key counted team_member].freeze class SignupCountRow attr_reader :count, :state, :bucket_key, :requested_bucket_key, :counted, :team_member @@ -62,7 +62,7 @@ def next_dimension def final? return @final unless @final.nil? - @final = dimension == DIMENSIONS[DIMENSIONS.size - 1] + @final = dimension == DIMENSIONS[-1] end def count(**filters) @@ -112,9 +112,7 @@ def self.signup_count_data_for_runs(runs) def self.for_runs(runs) data_by_run_id = signup_count_data_for_runs(runs) - runs.each_with_object({}) do |run, hash| - hash[run.id] = new(run, count_data: data_by_run_id[run.id] || SignupCountData.new(DIMENSIONS[0], [])) - end + runs.to_h { |run| [run.id, new(run, count_data: data_by_run_id[run.id] || SignupCountData.new(DIMENSIONS[0], []))] } end def initialize(run, count_data: nil) @@ -143,22 +141,10 @@ def bucket_descriptions_text(state) end def bucket_descriptions(state) - counted_key = counted_key_for_state(state) - if buckets.size == 1 - [count_data.count(bucket_key: buckets.first.key, counted: counted_key).to_s] + single_bucket_descriptions(state) else - bucket_texts = - buckets.map do |bucket| - bucket_counted_key = counted_key - bucket_counted_key = :not_counted if bucket.not_counted? - "#{bucket.name}: #{count_data.count(bucket_key: bucket.key, counted: bucket_counted_key)}" - end - - no_pref_count = count_data.count(requested_bucket_key: nil, counted: false) - bucket_texts << "No preference: #{no_pref_count}" if state == "waitlisted" && no_pref_count.positive? - - bucket_texts + multi_bucket_descriptions(state) end end @@ -210,6 +196,31 @@ def buckets private + def single_bucket_descriptions(state) + if state == "waitlisted" + [count_data.count(state: state).to_s] + else + [count_data.count(state: state, bucket_key: buckets.first.key, counted: counted_key_for_state(state)).to_s] + end + end + + def multi_bucket_descriptions(state) + counted_key = counted_key_for_state(state) + texts = buckets.map { |bucket| per_bucket_description(bucket, state, counted_key) } + no_pref_count = count_data.count(state: state, requested_bucket_key: nil, counted: false) + texts << "No preference: #{no_pref_count}" if state == "waitlisted" && no_pref_count.positive? + texts + end + + def per_bucket_description(bucket, state, counted_key) + if state == "waitlisted" + "#{bucket.name}: #{count_data.count(state: state, requested_bucket_key: bucket.key, counted: false)}" + else + bucket_counted_key = bucket.not_counted? ? :not_counted : counted_key + "#{bucket.name}: #{count_data.count(state: state, bucket_key: bucket.key, counted: bucket_counted_key)}" + end + end + def registration_policy @registration_policy ||= run.registration_policy end diff --git a/test/presenters/signup_count_presenter_test.rb b/test/presenters/signup_count_presenter_test.rb new file mode 100644 index 00000000000..4567152ddf4 --- /dev/null +++ b/test/presenters/signup_count_presenter_test.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true +require "test_helper" + +class SignupCountPresenterTest < ActiveSupport::TestCase + let(:convention) { create(:convention) } + + describe "with a single-bucket limited event" do + let(:registration_policy) do + RegistrationPolicy.new(buckets: [{ key: "attendees", name: "Attendees", slots_limited: true, total_slots: 10 }]) + end + let(:event) { create(:event, convention:, registration_policy:) } + let(:the_run) { create(:run, event:) } + let(:presenter) { SignupCountPresenter.new(the_run) } + + describe "#confirmed_count" do + it "counts confirmed signups" do + create(:signup, run: the_run) + create(:signup, run: the_run) + assert_equal 2, presenter.confirmed_count + end + + it "does not count withdrawn signups" do + create(:signup, run: the_run) + create(:signup, run: the_run, state: "withdrawn", bucket_key: nil, counted: false) + assert_equal 1, presenter.confirmed_count + end + end + + describe "#waitlist_count" do + it "counts waitlisted signups" do + create(:signup, run: the_run, state: "waitlisted", counted: false, requested_bucket_key: "attendees") + create(:signup, run: the_run, state: "waitlisted", counted: false, requested_bucket_key: "attendees") + assert_equal 2, presenter.waitlist_count + end + + it "does not count withdrawn signups" do + create(:signup, run: the_run, state: "waitlisted", counted: false, requested_bucket_key: "attendees") + create(:signup, run: the_run, state: "withdrawn", counted: false, requested_bucket_key: "attendees") + assert_equal 1, presenter.waitlist_count + end + end + + describe "#has_waitlist?" do + it "returns true when there are waitlisted signups" do + create(:signup, run: the_run, state: "waitlisted", counted: false, requested_bucket_key: "attendees") + assert presenter.has_waitlist? + end + + it "returns false when there are no waitlisted signups" do + create(:signup, run: the_run) + assert_not presenter.has_waitlist? + end + end + + describe "#signups_description" do + it "reports confirmed count" do + create(:signup, run: the_run) + create(:signup, run: the_run) + assert_equal "Signed up: 2", presenter.signups_description + end + + it "does not count withdrawn signups as confirmed" do + create(:signup, run: the_run) + create(:signup, run: the_run, state: "withdrawn", bucket_key: nil, counted: false) + assert_equal "Signed up: 1", presenter.signups_description + end + + it "reports waitlisted count when the waitlist is non-empty" do + create(:signup, run: the_run) + create(:signup, run: the_run, state: "waitlisted", counted: false, requested_bucket_key: "attendees") + create(:signup, run: the_run, state: "waitlisted", counted: false, requested_bucket_key: "attendees") + assert_equal "Signed up: 1\nWaitlisted: 2", presenter.signups_description + end + end + end + + describe "with a multi-bucket event" do + let(:registration_policy) do + RegistrationPolicy.new( + buckets: [ + { key: "dogs", name: "Dogs", slots_limited: true, total_slots: 5 }, + { key: "cats", name: "Cats", slots_limited: true, total_slots: 5 }, + { key: "flex", name: "Flex", slots_limited: true, total_slots: 5, anything: true } + ] + ) + end + let(:event) { create(:event, convention:, registration_policy:) } + let(:the_run) { create(:run, event:) } + let(:presenter) { SignupCountPresenter.new(the_run) } + + describe "#signups_description" do + it "shows per-bucket confirmed counts" do + create(:signup, run: the_run, bucket_key: "dogs", requested_bucket_key: "dogs") + create(:signup, run: the_run, bucket_key: "dogs", requested_bucket_key: "dogs") + create(:signup, run: the_run, bucket_key: "cats", requested_bucket_key: "cats") + description = presenter.signups_description + assert_includes description, "Dogs: 2" + assert_includes description, "Cats: 1" + end + + it "does not count withdrawn signups in per-bucket confirmed counts" do + create(:signup, run: the_run, bucket_key: "dogs", requested_bucket_key: "dogs") + create(:signup, run: the_run, state: "withdrawn", bucket_key: nil, counted: false, requested_bucket_key: "dogs") + assert_includes presenter.signups_description, "Dogs: 1" + end + + it "shows waitlisted count broken down by requested bucket" do + create(:signup, run: the_run, state: "waitlisted", counted: false, requested_bucket_key: "dogs") + create(:signup, run: the_run, state: "waitlisted", counted: false, requested_bucket_key: "dogs") + create(:signup, run: the_run, state: "waitlisted", counted: false, requested_bucket_key: "cats") + description = presenter.signups_description + assert_includes description, "Dogs: 2" + assert_includes description, "Cats: 1" + end + + it "does not count withdrawn signups in the no-preference waitlist count" do + # 3 genuine no-preference waitlisted signups + 3.times { create(:signup, run: the_run, state: "waitlisted", counted: false, requested_bucket_key: nil) } + # 1 withdrawn signup whose requested_bucket_key is nil (e.g. was confirmed via the flex bucket) + create(:signup, run: the_run, state: "withdrawn", counted: false, requested_bucket_key: nil) + assert_includes presenter.signups_description, "No preference: 3" + end + end + end +end