Support remaining pieces in completions and scoreboards
This commit is contained in:
		| @@ -109,6 +109,6 @@ class CompletionsController < ApplicationController | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def completion_params |   def completion_params | ||||||
|     params.expect(completion: [ :display_time_from_start, :contestant_id, :message_id, :puzzle_id ]) |     params.expect(completion: [ :display_time_from_start, :remaining_pieces, :contestant_id, :message_id, :puzzle_id ]) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -15,7 +15,12 @@ class ContestsController < ApplicationController | |||||||
|     @title = I18n.t("contests.show.title", name: @contest.name) |     @title = I18n.t("contests.show.title", name: @contest.name) | ||||||
|     @action_name = t("helpers.buttons.edit") |     @action_name = t("helpers.buttons.edit") | ||||||
|     @action_path = edit_contest_path(@contest) |     @action_path = edit_contest_path(@contest) | ||||||
|     @contestants = @contest.contestants.sort_by { |contestant| [ -contestant.completions.size, contestant.time_seconds ] } |     @contestants = @contest.contestants.sort_by { |contestant| [ | ||||||
|  |       -contestant.completions.where(remaining_pieces: nil).size, | ||||||
|  |       (contestant.completions.where(remaining_pieces: nil).size == @contest.puzzles.length ? 1 : 0) * contestant.time_seconds, | ||||||
|  |       contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? contestant.completions[-1].remaining_pieces : 1000000, | ||||||
|  |       contestant.time_seconds | ||||||
|  |     ] } | ||||||
|     filter_contestants_per_category |     filter_contestants_per_category | ||||||
|     @puzzles = @contest.puzzles.order(:id) |     @puzzles = @contest.puzzles.order(:id) | ||||||
|     @messages = @contest.messages.order(:time_seconds) |     @messages = @contest.messages.order(:time_seconds) | ||||||
| @@ -77,7 +82,12 @@ class ContestsController < ApplicationController | |||||||
|     I18n.locale = @contest.lang |     I18n.locale = @contest.lang | ||||||
|  |  | ||||||
|     @title = I18n.t("contests.scoreboard.title", name: @contest.name) |     @title = I18n.t("contests.scoreboard.title", name: @contest.name) | ||||||
|     @contestants = @contest.contestants.sort_by { |contestant| [ -contestant.completions.size, contestant.time_seconds ] } |     @contestants = @contest.contestants.sort_by { |contestant| [ | ||||||
|  |       -contestant.completions.where(remaining_pieces: nil).size, | ||||||
|  |       (contestant.completions.where(remaining_pieces: nil).size == @contest.puzzles.length ? 1 : 0) * contestant.time_seconds, | ||||||
|  |       contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? contestant.completions[-1].remaining_pieces : 1000000, | ||||||
|  |       contestant.time_seconds | ||||||
|  |     ] } | ||||||
|     filter_contestants_per_category |     filter_contestants_per_category | ||||||
|     @puzzles = @contest.puzzles.order(:id) |     @puzzles = @contest.puzzles.order(:id) | ||||||
|     @action_name = t("helpers.buttons.refresh") |     @action_name = t("helpers.buttons.refresh") | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
| #  id                      :integer          not null, primary key | #  id                      :integer          not null, primary key | ||||||
| #  display_relative_time   :string | #  display_relative_time   :string | ||||||
| #  display_time_from_start :string | #  display_time_from_start :string | ||||||
|  | #  remaining_pieces        :integer | ||||||
| #  time_seconds            :integer | #  time_seconds            :integer | ||||||
| #  created_at              :datetime         not null | #  created_at              :datetime         not null | ||||||
| #  updated_at              :datetime         not null | #  updated_at              :datetime         not null | ||||||
| @@ -34,10 +35,25 @@ class Completion < ApplicationRecord | |||||||
|   belongs_to :message, optional: true |   belongs_to :message, optional: true | ||||||
|  |  | ||||||
|   before_save :add_time_seconds |   before_save :add_time_seconds | ||||||
|  |   before_save :nullify_display_time | ||||||
|  |  | ||||||
|   validates :display_time_from_start, presence: true, format: { with: /\A(((\d\d|\d):\d\d|\d\d|\d):\d\d|\d\d|\d)\z/ } |   validates :display_time_from_start, presence: true, format: { with: /\A(((\d\d|\d):\d\d|\d\d|\d):\d\d|\d\d|\d)\z/ }, if: -> { remaining_pieces == nil } | ||||||
|   validates :contestant_id, uniqueness: { scope: :puzzle }, if: -> { contest.puzzles.size == 1 } |   validates :contestant_id, uniqueness: { scope: :puzzle }, if: -> { contest.puzzles.size == 1 } | ||||||
|   validates :puzzle_id, uniqueness: { scope: :contestant }, if: -> { contest.puzzles.size > 1 } |   validates :puzzle_id, uniqueness: { scope: :contestant }, if: -> { contest.puzzles.size > 1 } | ||||||
|  |   validate :remaining_pieces_is_correct | ||||||
|  |  | ||||||
|  |   def remaining_pieces_is_correct | ||||||
|  |     if self.remaining_pieces && self.remaining_pieces > self.puzzle.pieces | ||||||
|  |       errors.add(:remaining_pieces, "Cannot be greater than the number of pieces for this puzzle") | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def nullify_display_time | ||||||
|  |     if self.remaining_pieces | ||||||
|  |       self.display_time_from_start = nil | ||||||
|  |       self.display_relative_time = nil | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def add_time_seconds |   def add_time_seconds | ||||||
|     arr = display_time_from_start.split(":") |     arr = display_time_from_start.split(":") | ||||||
|   | |||||||
| @@ -12,11 +12,6 @@ | |||||||
|   .row |   .row | ||||||
|     .col |     .col | ||||||
|       h4 = t("completions.singular").capitalize |       h4 = t("completions.singular").capitalize | ||||||
|   .row.mb-3 |  | ||||||
|     .col |  | ||||||
|       .form-floating |  | ||||||
|         = form.text_field :display_time_from_start, autocomplete: "off", class: "form-control" |  | ||||||
|         = form.label :display_time_from_start, class: "required" |  | ||||||
|   .row.mb-3 |   .row.mb-3 | ||||||
|     .col |     .col | ||||||
|       .form-floating |       .form-floating | ||||||
| @@ -36,6 +31,18 @@ | |||||||
|     = form.hidden_field :puzzle_id, value: @puzzles.first.id |     = form.hidden_field :puzzle_id, value: @puzzles.first.id | ||||||
|   - else |   - else | ||||||
|     = form.hidden_field :puzzle_id |     = form.hidden_field :puzzle_id | ||||||
|  |   .row.mb-3 | ||||||
|  |     .col | ||||||
|  |       .form-floating | ||||||
|  |         = form.text_field :display_time_from_start, autocomplete: "off", class: "form-control" | ||||||
|  |         = form.label :display_time_from_start, class: "required" | ||||||
|  |   .row.mb-3 | ||||||
|  |     .col | ||||||
|  |       .form-floating | ||||||
|  |         = form.text_field :remaining_pieces, autocomplete: "off", class: "form-control" | ||||||
|  |         = form.label :remaining_pieces | ||||||
|  |       .form-text | ||||||
|  |         = t("activerecord.attributes.completion.remaining_pieces_description") | ||||||
|   .row |   .row | ||||||
|     .col |     .col | ||||||
|       = form.submit submit_text, class: "btn btn-primary" |       = form.submit submit_text, class: "btn btn-primary" | ||||||
| @@ -44,6 +44,8 @@ | |||||||
|         - else |         - else | ||||||
|           th scope="col" |           th scope="col" | ||||||
|             = t("activerecord.attributes.completion.display_time") |             = t("activerecord.attributes.completion.display_time") | ||||||
|  |         th scope="col" | ||||||
|  |           = t("activerecord.attributes.completion.remaining_pieces") | ||||||
|         th scope="col" |         th scope="col" | ||||||
|           = t("activerecord.attributes.completion.puzzle") |           = t("activerecord.attributes.completion.puzzle") | ||||||
|     tbody |     tbody | ||||||
| @@ -54,6 +56,8 @@ | |||||||
|           - if @contest.puzzles.size > 1 |           - if @contest.puzzles.size > 1 | ||||||
|             td |             td | ||||||
|               = completion.display_relative_time |               = completion.display_relative_time | ||||||
|  |           td | ||||||
|  |             = completion.remaining_pieces | ||||||
|           td |           td | ||||||
|             - if !completion.puzzle.brand.blank? |             - if !completion.puzzle.brand.blank? | ||||||
|               | #{completion.puzzle.name} - #{completion.puzzle.brand} |               | #{completion.puzzle.name} - #{completion.puzzle.brand} | ||||||
|   | |||||||
| @@ -42,13 +42,13 @@ css: | |||||||
|                   = contestant.name |                   = contestant.name | ||||||
|                 - if @contest.puzzles.size > 1 |                 - if @contest.puzzles.size > 1 | ||||||
|                   td |                   td | ||||||
|                     = contestant.completions.length |                     = contestant.completions.where(remaining_pieces: nil).length | ||||||
|                 td style="position: relative" |                 td style="position: relative" | ||||||
|                   - if index > 0 && contestant.time_seconds > 0 |                   - if index > 0 && contestant.time_seconds > 0 && contestant.completions.where(remaining_pieces: nil).size > 0 | ||||||
|                     .relative-time style="position:absolute; margin: 1px 0 0 64px; font-size: 14px; color: grey" |                     .relative-time style="position:absolute; margin: 1px 0 0 64px; font-size: 14px; color: grey" | ||||||
|                       |> + |                       |> + | ||||||
|                       = display_time(contestant.time_seconds - @contestants[index - 1].time_seconds) |                       = display_time(contestant.time_seconds - @contestants[index - 1].time_seconds) | ||||||
|                   = contestant.display_time |                   = contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? "#{contestant.completions.map{|completion| completion.puzzle.pieces}.sum - contestant.completions[-1].remaining_pieces}p" : contestant.display_time | ||||||
|     .col-1 |     .col-1 | ||||||
|     .col-5 |     .col-5 | ||||||
|       - @contest.puzzles.each do |puzzle| |       - @contest.puzzles.each do |puzzle| | ||||||
| @@ -89,6 +89,6 @@ css: | |||||||
|                 = contestant.name |                 = contestant.name | ||||||
|               - if @contest.puzzles.size > 1 |               - if @contest.puzzles.size > 1 | ||||||
|                 td |                 td | ||||||
|                   = contestant.completions.length |                   = contestant.completions.where(remaining_pieces: nil).length | ||||||
|               td |               td | ||||||
|                 = contestant.display_time |                 = contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? "#{contestant.completions.map{|completion| completion.puzzle.pieces}.sum - contestant.completions[-1].remaining_pieces}p" : contestant.display_time | ||||||
| @@ -85,9 +85,9 @@ javascript: | |||||||
|               td |               td | ||||||
|                 = contestant.name |                 = contestant.name | ||||||
|               td |               td | ||||||
|                 = contestant.completions.length |                 = contestant.completions.where(remaining_pieces: nil).length | ||||||
|               td |               td | ||||||
|                 = contestant.display_time |                 = contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? "#{contestant.completions.map{|completion| completion.puzzle.pieces}.sum - contestant.completions[-1].remaining_pieces}p" : contestant.display_time | ||||||
|               td |               td | ||||||
|                 a.btn.btn-sm.btn-secondary href=edit_contest_contestant_path(@contest, contestant) |                 a.btn.btn-sm.btn-secondary href=edit_contest_contestant_path(@contest, contestant) | ||||||
|                   = t("helpers.buttons.open") |                   = t("helpers.buttons.open") | ||||||
|   | |||||||
| @@ -48,6 +48,8 @@ en: | |||||||
|         display_time_from_start: Time since start |         display_time_from_start: Time since start | ||||||
|         display_relative_time: Time for this puzzle |         display_relative_time: Time for this puzzle | ||||||
|         puzzle: Puzzle |         puzzle: Puzzle | ||||||
|  |         remaining_pieces: Remaining pieces | ||||||
|  |         remaining_pieces_description: When this field is filled, the above time will not be taken into account | ||||||
|       contest: |       contest: | ||||||
|         lang: Language for the public scoreboard |         lang: Language for the public scoreboard | ||||||
|         name: Name |         name: Name | ||||||
|   | |||||||
| @@ -19,6 +19,8 @@ fr: | |||||||
|         display_time_from_start: Temps depuis le début |         display_time_from_start: Temps depuis le début | ||||||
|         display_relative_time: Temps pour ce puzzle |         display_relative_time: Temps pour ce puzzle | ||||||
|         puzzle: Puzzle |         puzzle: Puzzle | ||||||
|  |         remaining_pieces: Nombre de pièces restantes | ||||||
|  |         remaining_pieces_description: Si ce champ est rempli, le temps ci-dessus ne sera pas pris en compte | ||||||
|       contest: |       contest: | ||||||
|         lang: Langue pour le classement public |         lang: Langue pour le classement public | ||||||
|         name: Nom |         name: Nom | ||||||
|   | |||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | class AddRemainingPiecesToCompletion < ActiveRecord::Migration[8.0] | ||||||
|  |   def change | ||||||
|  |     add_column :completions, :remaining_pieces, :integer | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										3
									
								
								db/schema.rb
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								db/schema.rb
									
									
									
										generated
									
									
									
								
							| @@ -10,7 +10,7 @@ | |||||||
| # | # | ||||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||||
|  |  | ||||||
| ActiveRecord::Schema[8.0].define(version: 2025_07_14_115208) do | ActiveRecord::Schema[8.0].define(version: 2025_10_28_131431) do | ||||||
|   create_table "active_storage_attachments", force: :cascade do |t| |   create_table "active_storage_attachments", force: :cascade do |t| | ||||||
|     t.string "name", null: false |     t.string "name", null: false | ||||||
|     t.string "record_type", null: false |     t.string "record_type", null: false | ||||||
| @@ -64,6 +64,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_14_115208) do | |||||||
|     t.string "display_time_from_start" |     t.string "display_time_from_start" | ||||||
|     t.string "display_relative_time" |     t.string "display_relative_time" | ||||||
|     t.integer "message_id" |     t.integer "message_id" | ||||||
|  |     t.integer "remaining_pieces" | ||||||
|     t.index ["contest_id"], name: "index_completions_on_contest_id" |     t.index ["contest_id"], name: "index_completions_on_contest_id" | ||||||
|     t.index ["contestant_id"], name: "index_completions_on_contestant_id" |     t.index ["contestant_id"], name: "index_completions_on_contestant_id" | ||||||
|     t.index ["message_id"], name: "index_completions_on_message_id" |     t.index ["message_id"], name: "index_completions_on_message_id" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user