Implement projected time
Some checks failed
CI / scan_ruby (push) Successful in 16s
CI / scan_js (push) Successful in 13s
CI / lint (push) Successful in 13s
CI / test (push) Failing after 35s

This commit is contained in:
sto
2025-11-14 12:13:09 +01:00
parent f91145637f
commit b88460ae71
16 changed files with 114 additions and 48 deletions

View File

@@ -26,9 +26,7 @@ class CompletionsController < ApplicationController
@completion = Completion.new(completion_params) @completion = Completion.new(completion_params)
@completion.contest_id = @contest.id @completion.contest_id = @contest.id
if @completion.save if @completion.save
if @completion.display_time_from_start.present? extend_completions!(@completion.contestant)
extend_completions!(@completion.contestant)
end
if @contestant && !params[:completion].key?(:message_id) if @contestant && !params[:completion].key?(:message_id)
redirect_to edit_contest_contestant_path(@contest, @contestant), notice: t("completions.new.notice") redirect_to edit_contest_contestant_path(@contest, @contestant), notice: t("completions.new.notice")
else else

View File

@@ -9,24 +9,38 @@ module CompletionsConcern
end end
def display_time(time) def display_time(time)
if time == nil
return ""
end
h = time / 3600 h = time / 3600
m = (time % 3600) / 60 m = (time % 3600) / 60
s = (time % 3600) % 60 s = (time % 3600) % 60
if h > 0 if h > 0
return h.to_s + ":" + pad(m) + ":" + pad(s) return h.to_s + ":" + pad(m) + ":" + pad(s)
elsif m > 0
return m.to_s + ":" + pad(s)
end end
s.to_s m.to_s + ":" + pad(s)
end end
def extend_completions!(contestant) def extend_completions!(contestant)
current_time_from_start = 0 completions = contestant.completions
contestant.completions.order(:time_seconds).each do |completion| puzzles = contestant.contest.puzzles
completion.update(display_time_from_start: display_time(completion.time_seconds), if puzzles.length > 1
display_relative_time: display_time(completion.time_seconds - current_time_from_start)) current_time_from_start = 0
current_time_from_start = completion.time_seconds completions.order(:time_seconds).each do |completion|
completion.update(display_time_from_start: display_time(completion.time_seconds),
display_relative_time: display_time(completion.time_seconds - current_time_from_start))
current_time_from_start = completion.time_seconds
end
contestant.update(display_time: display_time(current_time_from_start), time_seconds: current_time_from_start)
elsif puzzles.length == 1 && completions.length >= 1
if completions[0].remaining_pieces != nil
contestant.update(
display_time: "#{display_time(completions[0].time_seconds)} - #{puzzles[0].pieces - completions[0].remaining_pieces}p",
time_seconds: completions[0].projected_time
)
else
contestant.update(display_time: display_time(completions[0].time_seconds), time_seconds: completions[0].time_seconds)
end
end end
contestant.update(display_time: display_time(current_time_from_start), time_seconds: current_time_from_start)
end end
end end

View File

@@ -160,6 +160,7 @@ class ContestsController < ApplicationController
contestant = Contestant.create(contest: @contest, name: @offline.name, offline: @offline) contestant = Contestant.create(contest: @contest, name: @offline.name, offline: @offline)
Completion.create(contest: @contest, Completion.create(contest: @contest,
contestant: contestant, contestant: contestant,
offline: @offline,
puzzle: @contest.puzzles[0], puzzle: @contest.puzzles[0],
completed: @offline.completed, completed: @offline.completed,
display_time_from_start: dp, display_time_from_start: dp,

View File

@@ -7,14 +7,15 @@ module ContestsHelper
end end
def display_time(time) def display_time(time)
if time == nil
return ""
end
h = time / 3600 h = time / 3600
m = (time % 3600) / 60 m = (time % 3600) / 60
s = (time % 3600) % 60 s = (time % 3600) % 60
if h > 0 if h > 0
return h.to_s + ":" + pad(m) + ":" + pad(s) return h.to_s + ":" + pad(m) + ":" + pad(s)
elsif m > 0
return m.to_s + ":" + pad(s)
end end
"0:" + pad(s) m.to_s + ":" + pad(s)
end end
end end

View File

@@ -7,6 +7,7 @@
# display_relative_time :string # display_relative_time :string
# display_time_from_start :string # display_time_from_start :string
# missing_pieces :integer # missing_pieces :integer
# projected_time :integer
# remaining_pieces :integer # remaining_pieces :integer
# time_seconds :integer # time_seconds :integer
# created_at :datetime not null # created_at :datetime not null
@@ -36,11 +37,12 @@ class Completion < ApplicationRecord
belongs_to :puzzle belongs_to :puzzle
belongs_to :message, optional: true belongs_to :message, optional: true
before_save :add_time_seconds, if: -> { display_time_from_start.present? } has_one :offline, dependent: :destroy
before_save :nullify_display_time
before_save :clean_pieces
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: -> { completed } before_save :clean_pieces
before_save :compute_projected_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 :remaining_pieces, presence: true, if: -> { !completed } validates :remaining_pieces, presence: true, if: -> { !completed }
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 }
@@ -53,21 +55,18 @@ class Completion < ApplicationRecord
end end
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(":") if display_time_from_start.present?
if arr.size == 3 arr = display_time_from_start.split(":")
self.time_seconds = arr[0].to_i * 3600 + arr[1].to_i * 60 + arr[2].to_i if arr.size == 3
elsif arr.size == 2 self.time_seconds = arr[0].to_i * 3600 + arr[1].to_i * 60 + arr[2].to_i
self.time_seconds = arr[0].to_i * 60 + arr[1].to_i elsif arr.size == 2
elsif arr.size == 1 self.time_seconds = arr[0].to_i * 60 + arr[1].to_i
self.time_seconds = arr[0].to_i elsif arr.size == 1
self.time_seconds = arr[0].to_i
end
else
self.time_seconds = 1
end end
end end
@@ -78,4 +77,16 @@ class Completion < ApplicationRecord
self.missing_pieces = nil self.missing_pieces = nil
end end
end end
def compute_projected_time
add_time_seconds
if self.completed
self.projected_time = self.time_seconds
elsif self.offline.present?
assembled_time = self.time_seconds
assembled_pieces = self.puzzle.pieces - self.remaining_pieces
pieces_per_second = assembled_pieces.to_f / assembled_time.to_f
self.projected_time = assembled_time + Integer(self.remaining_pieces.to_f / pieces_per_second)
end
end
end end

View File

@@ -2,14 +2,15 @@
# #
# Table name: contestants # Table name: contestants
# #
# id :integer not null, primary key # id :integer not null, primary key
# display_time :string # display_time :string
# email :string # email :string
# name :string # name :string
# time_seconds :integer # projected_time :string
# created_at :datetime not null # time_seconds :integer
# updated_at :datetime not null # created_at :datetime not null
# contest_id :integer not null # updated_at :datetime not null
# contest_id :integer not null
# #
# Indexes # Indexes
# #
@@ -22,7 +23,7 @@
class Contestant < ApplicationRecord class Contestant < ApplicationRecord
belongs_to :contest belongs_to :contest
has_many :completions, dependent: :destroy has_many :completions, dependent: :destroy
has_one :offline has_one :offline, dependent: :destroy
has_and_belongs_to_many :categories has_and_belongs_to_many :categories
before_validation :initialize_time_seconds_if_empty before_validation :initialize_time_seconds_if_empty

View File

@@ -12,22 +12,26 @@
# submitted :boolean # submitted :boolean
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# completion_id :integer
# contest_id :integer not null # contest_id :integer not null
# contestant_id :integer # contestant_id :integer
# #
# Indexes # Indexes
# #
# index_offlines_on_completion_id (completion_id)
# index_offlines_on_contest_id (contest_id) # index_offlines_on_contest_id (contest_id)
# index_offlines_on_contestant_id (contestant_id) # index_offlines_on_contestant_id (contestant_id)
# #
# Foreign Keys # Foreign Keys
# #
# completion_id (completion_id => completions.id)
# contest_id (contest_id => contests.id) # contest_id (contest_id => contests.id)
# contestant_id (contestant_id => contestants.id) # contestant_id (contestant_id => contestants.id)
# #
class Offline < ApplicationRecord class Offline < ApplicationRecord
belongs_to :contest belongs_to :contest
belongs_to :contestant, optional: true belongs_to :contestant, optional: true
belongs_to :completion, optional: true
has_many_attached :images has_many_attached :images

View File

@@ -36,6 +36,8 @@
table.table.table-striped.table-hover table.table.table-striped.table-hover
thead thead
tr tr
th scope="col"
= t("activerecord.attributes.completion.completed")
- if @contest.puzzles.size > 1 - if @contest.puzzles.size > 1
th scope="col" th scope="col"
= t("activerecord.attributes.completion.display_time_from_start") = t("activerecord.attributes.completion.display_time_from_start")
@@ -44,6 +46,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.projected_time")
th scope="col" th scope="col"
= t("activerecord.attributes.completion.missing_pieces") = t("activerecord.attributes.completion.missing_pieces")
th scope="col" th scope="col"
@@ -54,10 +58,19 @@
- @completions.each do |completion| - @completions.each do |completion|
tr scope="row" tr scope="row"
td td
= completion.display_time_from_start - if completion.completed
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-square" viewBox="0 0 16 16">
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
<path d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"/>
</svg>
td
= display_time(completion.time_seconds)
- if @contest.puzzles.size > 1 - if @contest.puzzles.size > 1
td td
= completion.display_relative_time = completion.display_relative_time
- else
td
= display_time(completion.projected_time)
td td
= completion.missing_pieces = completion.missing_pieces
td td

View File

@@ -47,11 +47,11 @@ css:
td td
= contestant.completions.where(remaining_pieces: nil).length = contestant.completions.where(remaining_pieces: nil).length
td style="position: relative" td style="position: relative"
- if index > 0 && contestant.time_seconds > 0 && contestant.completions.where(remaining_pieces: nil).size > 0 - if index > 0 && contestant.time_seconds > 0 && contestant.completions.where(completed: true).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 100px; 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.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 = contestant.display_time
.col-1 .col-1
.col-5 .col-5
- @contest.puzzles.each do |puzzle| - @contest.puzzles.each do |puzzle|

View File

@@ -50,6 +50,7 @@ en:
display_relative_time: Time for this puzzle display_relative_time: Time for this puzzle
puzzle: Puzzle puzzle: Puzzle
missing_pieces: Missing pieces missing_pieces: Missing pieces
projected_time: Projected time
remaining_pieces: Remaining pieces (not completed puzzle) remaining_pieces: Remaining pieces (not completed puzzle)
contest: contest:
lang: Language for the public scoreboard lang: Language for the public scoreboard

View File

@@ -21,6 +21,7 @@ fr:
display_relative_time: Temps pour ce puzzle display_relative_time: Temps pour ce puzzle
puzzle: Puzzle puzzle: Puzzle
missing_pieces: Pièces manquantes missing_pieces: Pièces manquantes
projected_time: Temps projeté
remaining_pieces: Pièces restantes (puzzle non fini) remaining_pieces: Pièces restantes (puzzle non fini)
contest: contest:
lang: Langue pour le classement public lang: Langue pour le classement public
@@ -183,7 +184,7 @@ fr:
notice: Participant.e modifié.e notice: Participant.e modifié.e
not_finished: Non terminé not_finished: Non terminé
offline_participation: Participation hors-ligne offline_participation: Participation hors-ligne
start_time: Image de début start_image: Image de début
title: Participant.e title: Participant.e
team_title: Équipe team_title: Équipe
finalize_import: finalize_import:

View File

@@ -0,0 +1,5 @@
class AddProjectedTimeToCompletion < ActiveRecord::Migration[8.0]
def change
add_column :completions, :projected_time, :integer
end
end

View File

@@ -0,0 +1,5 @@
class AddCompletionToOffline < ActiveRecord::Migration[8.0]
def change
add_reference :offlines, :completion, foreign_key: true
end
end

7
db/schema.rb generated
View File

@@ -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_11_14_085123) do ActiveRecord::Schema[8.0].define(version: 2025_11_14_093213) 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
@@ -67,6 +67,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_14_085123) do
t.integer "remaining_pieces" t.integer "remaining_pieces"
t.integer "missing_pieces" t.integer "missing_pieces"
t.boolean "completed" t.boolean "completed"
t.integer "projected_time"
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"
@@ -81,6 +82,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_14_085123) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "display_time" t.string "display_time"
t.integer "time_seconds" t.integer "time_seconds"
t.string "projected_time"
t.index ["contest_id"], name: "index_contestants_on_contest_id" t.index ["contest_id"], name: "index_contestants_on_contest_id"
end end
@@ -141,6 +143,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_14_085123) do
t.integer "missing_pieces" t.integer "missing_pieces"
t.integer "remaining_pieces" t.integer "remaining_pieces"
t.boolean "submitted" t.boolean "submitted"
t.integer "completion_id"
t.index ["completion_id"], name: "index_offlines_on_completion_id"
t.index ["contest_id"], name: "index_offlines_on_contest_id" t.index ["contest_id"], name: "index_offlines_on_contest_id"
t.index ["contestant_id"], name: "index_offlines_on_contestant_id" t.index ["contestant_id"], name: "index_offlines_on_contestant_id"
end end
@@ -185,6 +189,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_14_085123) do
add_foreign_key "contestants", "contests" add_foreign_key "contestants", "contests"
add_foreign_key "contests", "users" add_foreign_key "contests", "users"
add_foreign_key "messages", "contests" add_foreign_key "messages", "contests"
add_foreign_key "offlines", "completions"
add_foreign_key "offlines", "contestants" add_foreign_key "offlines", "contestants"
add_foreign_key "offlines", "contests" add_foreign_key "offlines", "contests"
add_foreign_key "puzzles", "contests" add_foreign_key "puzzles", "contests"

View File

@@ -12,16 +12,19 @@
# submitted :boolean # submitted :boolean
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# completion_id :integer
# contest_id :integer not null # contest_id :integer not null
# contestant_id :integer # contestant_id :integer
# #
# Indexes # Indexes
# #
# index_offlines_on_completion_id (completion_id)
# index_offlines_on_contest_id (contest_id) # index_offlines_on_contest_id (contest_id)
# index_offlines_on_contestant_id (contestant_id) # index_offlines_on_contestant_id (contestant_id)
# #
# Foreign Keys # Foreign Keys
# #
# completion_id (completion_id => completions.id)
# contest_id (contest_id => contests.id) # contest_id (contest_id => contests.id)
# contestant_id (contestant_id => contestants.id) # contestant_id (contestant_id => contestants.id)
# #

View File

@@ -12,16 +12,19 @@
# submitted :boolean # submitted :boolean
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# completion_id :integer
# contest_id :integer not null # contest_id :integer not null
# contestant_id :integer # contestant_id :integer
# #
# Indexes # Indexes
# #
# index_offlines_on_completion_id (completion_id)
# index_offlines_on_contest_id (contest_id) # index_offlines_on_contest_id (contest_id)
# index_offlines_on_contestant_id (contestant_id) # index_offlines_on_contestant_id (contestant_id)
# #
# Foreign Keys # Foreign Keys
# #
# completion_id (completion_id => completions.id)
# contest_id (contest_id => contests.id) # contest_id (contest_id => contests.id)
# contestant_id (contestant_id => contestants.id) # contestant_id (contestant_id => contestants.id)
# #