diff --git a/app/controllers/completions_controller.rb b/app/controllers/completions_controller.rb index 027716b..f28c744 100644 --- a/app/controllers/completions_controller.rb +++ b/app/controllers/completions_controller.rb @@ -26,9 +26,7 @@ class CompletionsController < ApplicationController @completion = Completion.new(completion_params) @completion.contest_id = @contest.id if @completion.save - if @completion.display_time_from_start.present? - extend_completions!(@completion.contestant) - end + extend_completions!(@completion.contestant) if @contestant && !params[:completion].key?(:message_id) redirect_to edit_contest_contestant_path(@contest, @contestant), notice: t("completions.new.notice") else diff --git a/app/controllers/concerns/completions_concern.rb b/app/controllers/concerns/completions_concern.rb index ea63f20..fb325fb 100644 --- a/app/controllers/concerns/completions_concern.rb +++ b/app/controllers/concerns/completions_concern.rb @@ -9,24 +9,38 @@ module CompletionsConcern end def display_time(time) + if time == nil + return "" + end h = time / 3600 m = (time % 3600) / 60 s = (time % 3600) % 60 if h > 0 return h.to_s + ":" + pad(m) + ":" + pad(s) - elsif m > 0 - return m.to_s + ":" + pad(s) end - s.to_s + m.to_s + ":" + pad(s) end def extend_completions!(contestant) - current_time_from_start = 0 - contestant.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 + completions = contestant.completions + puzzles = contestant.contest.puzzles + if puzzles.length > 1 + current_time_from_start = 0 + 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 - contestant.update(display_time: display_time(current_time_from_start), time_seconds: current_time_from_start) end end diff --git a/app/controllers/contests_controller.rb b/app/controllers/contests_controller.rb index 6edf6a5..1dee732 100644 --- a/app/controllers/contests_controller.rb +++ b/app/controllers/contests_controller.rb @@ -160,6 +160,7 @@ class ContestsController < ApplicationController contestant = Contestant.create(contest: @contest, name: @offline.name, offline: @offline) Completion.create(contest: @contest, contestant: contestant, + offline: @offline, puzzle: @contest.puzzles[0], completed: @offline.completed, display_time_from_start: dp, diff --git a/app/helpers/contests_helper.rb b/app/helpers/contests_helper.rb index e58ce4d..d566e6b 100644 --- a/app/helpers/contests_helper.rb +++ b/app/helpers/contests_helper.rb @@ -7,14 +7,15 @@ module ContestsHelper end def display_time(time) + if time == nil + return "" + end h = time / 3600 m = (time % 3600) / 60 s = (time % 3600) % 60 if h > 0 return h.to_s + ":" + pad(m) + ":" + pad(s) - elsif m > 0 - return m.to_s + ":" + pad(s) end - "0:" + pad(s) + m.to_s + ":" + pad(s) end end diff --git a/app/models/completion.rb b/app/models/completion.rb index 651a22a..00fe893 100644 --- a/app/models/completion.rb +++ b/app/models/completion.rb @@ -7,6 +7,7 @@ # display_relative_time :string # display_time_from_start :string # missing_pieces :integer +# projected_time :integer # remaining_pieces :integer # time_seconds :integer # created_at :datetime not null @@ -36,11 +37,12 @@ class Completion < ApplicationRecord belongs_to :puzzle belongs_to :message, optional: true - before_save :add_time_seconds, if: -> { display_time_from_start.present? } - before_save :nullify_display_time - before_save :clean_pieces + has_one :offline, dependent: :destroy - 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 :contestant_id, uniqueness: { scope: :puzzle }, 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 - 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 - arr = display_time_from_start.split(":") - if arr.size == 3 - self.time_seconds = arr[0].to_i * 3600 + arr[1].to_i * 60 + arr[2].to_i - elsif arr.size == 2 - self.time_seconds = arr[0].to_i * 60 + arr[1].to_i - elsif arr.size == 1 - self.time_seconds = arr[0].to_i + if display_time_from_start.present? + arr = display_time_from_start.split(":") + if arr.size == 3 + self.time_seconds = arr[0].to_i * 3600 + arr[1].to_i * 60 + arr[2].to_i + elsif arr.size == 2 + self.time_seconds = arr[0].to_i * 60 + arr[1].to_i + elsif arr.size == 1 + self.time_seconds = arr[0].to_i + end + else + self.time_seconds = 1 end end @@ -78,4 +77,16 @@ class Completion < ApplicationRecord self.missing_pieces = nil 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 diff --git a/app/models/contestant.rb b/app/models/contestant.rb index 89ec326..4e42d8d 100644 --- a/app/models/contestant.rb +++ b/app/models/contestant.rb @@ -2,14 +2,15 @@ # # Table name: contestants # -# id :integer not null, primary key -# display_time :string -# email :string -# name :string -# time_seconds :integer -# created_at :datetime not null -# updated_at :datetime not null -# contest_id :integer not null +# id :integer not null, primary key +# display_time :string +# email :string +# name :string +# projected_time :string +# time_seconds :integer +# created_at :datetime not null +# updated_at :datetime not null +# contest_id :integer not null # # Indexes # @@ -22,7 +23,7 @@ class Contestant < ApplicationRecord belongs_to :contest has_many :completions, dependent: :destroy - has_one :offline + has_one :offline, dependent: :destroy has_and_belongs_to_many :categories before_validation :initialize_time_seconds_if_empty diff --git a/app/models/offline.rb b/app/models/offline.rb index 9481ccb..2833989 100644 --- a/app/models/offline.rb +++ b/app/models/offline.rb @@ -12,22 +12,26 @@ # submitted :boolean # created_at :datetime not null # updated_at :datetime not null +# completion_id :integer # contest_id :integer not null # contestant_id :integer # # Indexes # +# index_offlines_on_completion_id (completion_id) # index_offlines_on_contest_id (contest_id) # index_offlines_on_contestant_id (contestant_id) # # Foreign Keys # +# completion_id (completion_id => completions.id) # contest_id (contest_id => contests.id) # contestant_id (contestant_id => contestants.id) # class Offline < ApplicationRecord belongs_to :contest belongs_to :contestant, optional: true + belongs_to :completion, optional: true has_many_attached :images diff --git a/app/views/contestants/_form.html.slim b/app/views/contestants/_form.html.slim index e1a47d4..a48ba44 100644 --- a/app/views/contestants/_form.html.slim +++ b/app/views/contestants/_form.html.slim @@ -36,6 +36,8 @@ table.table.table-striped.table-hover thead tr + th scope="col" + = t("activerecord.attributes.completion.completed") - if @contest.puzzles.size > 1 th scope="col" = t("activerecord.attributes.completion.display_time_from_start") @@ -44,6 +46,8 @@ - else th scope="col" = t("activerecord.attributes.completion.display_time") + th scope="col" + = t("activerecord.attributes.completion.projected_time") th scope="col" = t("activerecord.attributes.completion.missing_pieces") th scope="col" @@ -54,10 +58,19 @@ - @completions.each do |completion| tr scope="row" td - = completion.display_time_from_start + - if completion.completed + + + + + td + = display_time(completion.time_seconds) - if @contest.puzzles.size > 1 td = completion.display_relative_time + - else + td + = display_time(completion.projected_time) td = completion.missing_pieces td diff --git a/app/views/contests/scoreboard.html.slim b/app/views/contests/scoreboard.html.slim index 1652c40..989b17f 100644 --- a/app/views/contests/scoreboard.html.slim +++ b/app/views/contests/scoreboard.html.slim @@ -47,11 +47,11 @@ css: td = contestant.completions.where(remaining_pieces: nil).length td style="position: relative" - - 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" + - if index > 0 && contestant.time_seconds > 0 && contestant.completions.where(completed: true).size > 0 + .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) - = 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-5 - @contest.puzzles.each do |puzzle| diff --git a/config/locales/en.yml b/config/locales/en.yml index 674e4e3..e2bad72 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -50,6 +50,7 @@ en: display_relative_time: Time for this puzzle puzzle: Puzzle missing_pieces: Missing pieces + projected_time: Projected time remaining_pieces: Remaining pieces (not completed puzzle) contest: lang: Language for the public scoreboard diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 373bc52..711ec36 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -21,6 +21,7 @@ fr: display_relative_time: Temps pour ce puzzle puzzle: Puzzle missing_pieces: Pièces manquantes + projected_time: Temps projeté remaining_pieces: Pièces restantes (puzzle non fini) contest: lang: Langue pour le classement public @@ -183,7 +184,7 @@ fr: notice: Participant.e modifié.e not_finished: Non terminé offline_participation: Participation hors-ligne - start_time: Image de début + start_image: Image de début title: Participant.e team_title: Équipe finalize_import: diff --git a/db/migrate/20251114092752_add_projected_time_to_completion.rb b/db/migrate/20251114092752_add_projected_time_to_completion.rb new file mode 100644 index 0000000..de6b099 --- /dev/null +++ b/db/migrate/20251114092752_add_projected_time_to_completion.rb @@ -0,0 +1,5 @@ +class AddProjectedTimeToCompletion < ActiveRecord::Migration[8.0] + def change + add_column :completions, :projected_time, :integer + end +end diff --git a/db/migrate/20251114093213_add_completion_to_offline.rb b/db/migrate/20251114093213_add_completion_to_offline.rb new file mode 100644 index 0000000..a300e61 --- /dev/null +++ b/db/migrate/20251114093213_add_completion_to_offline.rb @@ -0,0 +1,5 @@ +class AddCompletionToOffline < ActiveRecord::Migration[8.0] + def change + add_reference :offlines, :completion, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 39f174e..0f91367 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # 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| t.string "name", 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 "missing_pieces" t.boolean "completed" + t.integer "projected_time" t.index ["contest_id"], name: "index_completions_on_contest_id" t.index ["contestant_id"], name: "index_completions_on_contestant_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.string "display_time" t.integer "time_seconds" + t.string "projected_time" t.index ["contest_id"], name: "index_contestants_on_contest_id" end @@ -141,6 +143,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_14_085123) do t.integer "missing_pieces" t.integer "remaining_pieces" 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 ["contestant_id"], name: "index_offlines_on_contestant_id" end @@ -185,6 +189,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_14_085123) do add_foreign_key "contestants", "contests" add_foreign_key "contests", "users" add_foreign_key "messages", "contests" + add_foreign_key "offlines", "completions" add_foreign_key "offlines", "contestants" add_foreign_key "offlines", "contests" add_foreign_key "puzzles", "contests" diff --git a/spec/factories/offlines.rb b/spec/factories/offlines.rb index 943a47e..0ba3f64 100644 --- a/spec/factories/offlines.rb +++ b/spec/factories/offlines.rb @@ -12,16 +12,19 @@ # submitted :boolean # created_at :datetime not null # updated_at :datetime not null +# completion_id :integer # contest_id :integer not null # contestant_id :integer # # Indexes # +# index_offlines_on_completion_id (completion_id) # index_offlines_on_contest_id (contest_id) # index_offlines_on_contestant_id (contestant_id) # # Foreign Keys # +# completion_id (completion_id => completions.id) # contest_id (contest_id => contests.id) # contestant_id (contestant_id => contestants.id) # diff --git a/spec/models/offline_spec.rb b/spec/models/offline_spec.rb index f164a5e..62cdfa3 100644 --- a/spec/models/offline_spec.rb +++ b/spec/models/offline_spec.rb @@ -12,16 +12,19 @@ # submitted :boolean # created_at :datetime not null # updated_at :datetime not null +# completion_id :integer # contest_id :integer not null # contestant_id :integer # # Indexes # +# index_offlines_on_completion_id (completion_id) # index_offlines_on_contest_id (contest_id) # index_offlines_on_contestant_id (contestant_id) # # Foreign Keys # +# completion_id (completion_id => completions.id) # contest_id (contest_id => contests.id) # contestant_id (contestant_id => contestants.id) #