Add contest duration & complete ranking mode implementation
This commit is contained in:
@@ -2,11 +2,15 @@ module ContestantsConcern
|
|||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
def ranked_contestants(contest)
|
def ranked_contestants(contest)
|
||||||
contest.contestants.sort_by { |contestant| [
|
if contest.ranking_mode == "actual"
|
||||||
-contestant.completions.where(remaining_pieces: nil).size,
|
contest.contestants.sort_by { |contestant| [
|
||||||
(contestant.completions.where(remaining_pieces: nil).size == @contest.puzzles.length ? 1 : 0) * contestant.time_seconds,
|
-contestant.completions.where(remaining_pieces: nil).size,
|
||||||
contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? contestant.completions[-1].remaining_pieces : 1000000,
|
(contestant.completions.where(remaining_pieces: nil).size == @contest.puzzles.length ? 1 : 0) * contestant.time_seconds,
|
||||||
contestant.time_seconds
|
contestant.completions.size > 0 && contestant.completions[-1].remaining_pieces ? contestant.completions[-1].remaining_pieces : 1000000,
|
||||||
] }
|
contestant.time_seconds
|
||||||
|
] }
|
||||||
|
elsif contest.ranking_mode == "theorical"
|
||||||
|
contest.contestants.sort_by { |contestant| contestant.completions.map { |completion| completion.projected_time }.sum }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ class ContestsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def settings_general_params
|
def settings_general_params
|
||||||
params.expect(contest: [ :lang, :name, :public, :ranking_mode, :team, :allow_registration ])
|
params.expect(contest: [ :lang, :name, :duration, :public, :ranking_mode, :team, :allow_registration ])
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_offline_params
|
def settings_offline_params
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
# puzzle_id (puzzle_id => puzzles.id)
|
# puzzle_id (puzzle_id => puzzles.id)
|
||||||
#
|
#
|
||||||
class Completion < ApplicationRecord
|
class Completion < ApplicationRecord
|
||||||
|
include ContestsHelper
|
||||||
|
|
||||||
belongs_to :contest
|
belongs_to :contest
|
||||||
belongs_to :contestant
|
belongs_to :contestant
|
||||||
belongs_to :puzzle
|
belongs_to :puzzle
|
||||||
@@ -42,7 +44,7 @@ class Completion < ApplicationRecord
|
|||||||
before_save :clean_pieces
|
before_save :clean_pieces
|
||||||
before_save :compute_projected_time
|
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 :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 || offline.present? }
|
||||||
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 }
|
||||||
@@ -66,7 +68,8 @@ class Completion < ApplicationRecord
|
|||||||
self.time_seconds = arr[0].to_i
|
self.time_seconds = arr[0].to_i
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
self.time_seconds = 1
|
self.time_seconds = self.contest.duration_seconds
|
||||||
|
self.display_time_from_start = display_time(self.time_seconds)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -75,6 +78,9 @@ class Completion < ApplicationRecord
|
|||||||
self.remaining_pieces = nil
|
self.remaining_pieces = nil
|
||||||
else
|
else
|
||||||
self.missing_pieces = nil
|
self.missing_pieces = nil
|
||||||
|
if !self.offline.present?
|
||||||
|
self.display_time_from_start = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -82,7 +88,7 @@ class Completion < ApplicationRecord
|
|||||||
add_time_seconds
|
add_time_seconds
|
||||||
if self.completed
|
if self.completed
|
||||||
self.projected_time = self.time_seconds
|
self.projected_time = self.time_seconds
|
||||||
elsif self.offline.present?
|
else
|
||||||
assembled_time = self.time_seconds
|
assembled_time = self.time_seconds
|
||||||
assembled_pieces = self.puzzle.pieces - self.remaining_pieces
|
assembled_pieces = self.puzzle.pieces - self.remaining_pieces
|
||||||
pieces_per_second = assembled_pieces.to_f / assembled_time.to_f
|
pieces_per_second = assembled_pieces.to_f / assembled_time.to_f
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# allow_registration :boolean default(FALSE)
|
# allow_registration :boolean default(FALSE)
|
||||||
|
# duration :string
|
||||||
|
# duration_seconds :integer
|
||||||
# lang :string default("en")
|
# lang :string default("en")
|
||||||
# name :string
|
# name :string
|
||||||
# offline_form :boolean default(FALSE)
|
# offline_form :boolean default(FALSE)
|
||||||
@@ -37,9 +39,19 @@ class Contest < ApplicationRecord
|
|||||||
|
|
||||||
friendly_id :name, use: :slugged
|
friendly_id :name, use: :slugged
|
||||||
|
|
||||||
|
before_save :add_duration_seconds
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
|
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
|
||||||
validates :ranking_mode, inclusion: { in: Ranking::AVAILABLE_RANKING_MODES.map { |lang| lang[:id] } }
|
validates :ranking_mode, inclusion: { in: Ranking::AVAILABLE_RANKING_MODES.map { |lang| lang[:id] } }
|
||||||
|
validates :duration, format: { with: /\A(\d\d:\d\d|\d:\d\d)\z/ }
|
||||||
|
|
||||||
generates_token_for :token
|
generates_token_for :token
|
||||||
|
|
||||||
|
def add_duration_seconds
|
||||||
|
arr = self.duration.split(":")
|
||||||
|
if arr.size == 2
|
||||||
|
self.duration_seconds = arr[0].to_i * 3600 + arr[1].to_i * 60
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ css:
|
|||||||
= 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(completed: true).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 100px; font-size: 14px; color: grey"
|
.relative-time style="position:absolute; margin: 1px 0 0 112px; 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.display_time
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
.form-floating
|
.form-floating
|
||||||
= form.text_field :name, autocomplete: "off", class: "form-control"
|
= form.text_field :name, autocomplete: "off", class: "form-control"
|
||||||
= form.label :name, class: "required"
|
= form.label :name, class: "required"
|
||||||
|
.row.mb-3
|
||||||
|
.col
|
||||||
|
.form-floating
|
||||||
|
= form.text_field :duration, autocomplete: "off", class: "form-control"
|
||||||
|
= form.label :duration, class: "required"
|
||||||
|
.form-text = t("activerecord.attributes.contest.duration_description")
|
||||||
.row.mb-3
|
.row.mb-3
|
||||||
.col
|
.col
|
||||||
.form-floating
|
.form-floating
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ en:
|
|||||||
projected_time: Projected time
|
projected_time: Projected time
|
||||||
remaining_pieces: Remaining pieces (not completed puzzle)
|
remaining_pieces: Remaining pieces (not completed puzzle)
|
||||||
contest:
|
contest:
|
||||||
|
duration: Duration
|
||||||
|
duration_description: Format h:mm or hh:mm
|
||||||
lang: Language for the public scoreboard
|
lang: Language for the public scoreboard
|
||||||
name: Name
|
name: Name
|
||||||
offline_form: Enable the offline participation form
|
offline_form: Enable the offline participation form
|
||||||
@@ -114,6 +116,8 @@ en:
|
|||||||
not_a_number: This is not an integer
|
not_a_number: This is not an integer
|
||||||
contest:
|
contest:
|
||||||
attributes:
|
attributes:
|
||||||
|
duration:
|
||||||
|
invalid: Invalid duration
|
||||||
name:
|
name:
|
||||||
blank: The contest name cannot be empty
|
blank: The contest name cannot be empty
|
||||||
contestant:
|
contestant:
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ fr:
|
|||||||
projected_time: Temps projeté
|
projected_time: Temps projeté
|
||||||
remaining_pieces: Pièces restantes (puzzle non fini)
|
remaining_pieces: Pièces restantes (puzzle non fini)
|
||||||
contest:
|
contest:
|
||||||
|
duration: Durée
|
||||||
|
duration_description: Format h:mm ou hh:mm
|
||||||
lang: Langue pour le classement public
|
lang: Langue pour le classement public
|
||||||
name: Nom
|
name: Nom
|
||||||
offline_form: Activer le formulaire de participation hors-ligne
|
offline_form: Activer le formulaire de participation hors-ligne
|
||||||
@@ -85,6 +87,8 @@ fr:
|
|||||||
not_a_number: Ce n'est pas un nombre entier
|
not_a_number: Ce n'est pas un nombre entier
|
||||||
contest:
|
contest:
|
||||||
attributes:
|
attributes:
|
||||||
|
duration:
|
||||||
|
invalid: Durée invalide
|
||||||
name:
|
name:
|
||||||
blank: Le nom du concours ne peut pas être vide
|
blank: Le nom du concours ne peut pas être vide
|
||||||
contestant:
|
contestant:
|
||||||
|
|||||||
5
db/migrate/20251118074900_add_duration_to_contest.rb
Normal file
5
db/migrate/20251118074900_add_duration_to_contest.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddDurationToContest < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :contests, :duration, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddDurationSecondsToContest < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :contests, :duration_seconds, :integer
|
||||||
|
end
|
||||||
|
end
|
||||||
4
db/schema.rb
generated
4
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_11_14_093213) do
|
ActiveRecord::Schema[8.0].define(version: 2025_11_18_074914) 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
|
||||||
@@ -98,6 +98,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_14_093213) do
|
|||||||
t.boolean "public", default: false
|
t.boolean "public", default: false
|
||||||
t.boolean "offline_form", default: false
|
t.boolean "offline_form", default: false
|
||||||
t.string "ranking_mode"
|
t.string "ranking_mode"
|
||||||
|
t.string "duration"
|
||||||
|
t.integer "duration_seconds"
|
||||||
t.index ["slug"], name: "index_contests_on_slug", unique: true
|
t.index ["slug"], name: "index_contests_on_slug", unique: true
|
||||||
t.index ["user_id"], name: "index_contests_on_user_id"
|
t.index ["user_id"], name: "index_contests_on_user_id"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# allow_registration :boolean default(FALSE)
|
# allow_registration :boolean default(FALSE)
|
||||||
|
# duration :string
|
||||||
|
# duration_seconds :integer
|
||||||
# lang :string default("en")
|
# lang :string default("en")
|
||||||
# name :string
|
# name :string
|
||||||
# offline_form :boolean default(FALSE)
|
# offline_form :boolean default(FALSE)
|
||||||
|
|||||||
Reference in New Issue
Block a user