Add ranking mode
Some checks failed
CI / scan_ruby (push) Successful in 20s
CI / scan_js (push) Successful in 13s
CI / lint (push) Successful in 12s
CI / test (push) Failing after 35s

This commit is contained in:
sto
2025-11-14 10:19:08 +01:00
parent cdf87e48f2
commit f91145637f
12 changed files with 51 additions and 38 deletions

View File

@@ -54,10 +54,6 @@ class CompletionsController < ApplicationController
redirect_to @contest, notice: t("completions.edit.notice") redirect_to @contest, notice: t("completions.edit.notice")
end end
else else
if @contestant
@action_name = t("helpers.buttons.back_to_contestant")
@action_path = edit_contest_contestant_path(@contest, @contestant)
end
render :edit, status: :unprocessable_entity render :edit, status: :unprocessable_entity
end end
end end

View File

@@ -0,0 +1,12 @@
module ContestantsConcern
extend ActiveSupport::Concern
def ranked_contestants(contest)
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
] }
end
end

View File

@@ -1,4 +1,6 @@
class ContestantsController < ApplicationController class ContestantsController < ApplicationController
include ContestantsConcern
before_action :set_contest before_action :set_contest
before_action :set_contestant, only: %i[ destroy edit update] before_action :set_contestant, only: %i[ destroy edit update]
before_action :set_completions, only: %i[edit update ] before_action :set_completions, only: %i[edit update ]
@@ -6,12 +8,7 @@ class ContestantsController < ApplicationController
def index def index
authorize @contest authorize @contest
@contestants = @contest.contestants.sort_by { |contestant| [ @contestants = @contest.contestants.sort_by { |contestant| contestant.name }
-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
end end
@@ -111,12 +108,7 @@ class ContestantsController < ApplicationController
def export def export
authorize @contest authorize @contest
@contestants = @contest.contestants.sort_by { |contestant| [ @contestants = ranked_contestants(@contest)
-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
] }
respond_to do |format| respond_to do |format|
format.csv do format.csv do

View File

@@ -1,7 +1,8 @@
class ContestsController < ApplicationController class ContestsController < ApplicationController
include CompletionsConcern include CompletionsConcern
include ContestantsConcern
before_action :set_contest, only: %i[ destroy show update ] before_action :set_contest, only: %i[ destroy show ]
before_action :set_settings_contest, only: %i[ settings_general_edit settings_general_update settings_offline_edit settings_offline_update settings_categories_edit ] before_action :set_settings_contest, only: %i[ settings_general_edit settings_general_update settings_offline_edit settings_offline_update settings_categories_edit ]
before_action :offline_setup, only: %i[ offline_new offline_create offline_edit offline_update offline_completed ] before_action :offline_setup, only: %i[ offline_new offline_create offline_edit offline_update offline_completed ]
skip_before_action :require_authentication, only: %i[ scoreboard offline_new offline_create offline_edit offline_update offline_completed ] skip_before_action :require_authentication, only: %i[ scoreboard offline_new offline_create offline_edit offline_update offline_completed ]
@@ -60,7 +61,7 @@ class ContestsController < ApplicationController
def create def create
authorize :contest authorize :contest
@contest = Contest.new(contest_params) @contest = Contest.new(new_contest_params)
@contest.user_id = current_user.id @contest.user_id = current_user.id
if @contest.save if @contest.save
redirect_to "/contests/#{@contest.id}/settings/general", notice: t("contests.new.notice") redirect_to "/contests/#{@contest.id}/settings/general", notice: t("contests.new.notice")
@@ -69,16 +70,6 @@ class ContestsController < ApplicationController
end end
end end
def update
authorize @contest
if @contest.update(contest_params)
redirect_to @contest, notice: t("contests.edit.notice")
else
render :edit, status: :unprocessable_entity
end
end
def destroy def destroy
authorize @contest authorize @contest
@@ -97,12 +88,7 @@ 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| [ @contestants = ranked_contestants(@contest)
-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
if params.key?(:hide_offline) && params[:hide_offline] == "true" if params.key?(:hide_offline) && params[:hide_offline] == "true"
@contestants = @contestants.select { |contestant| !contestant.offline.present? } @contestants = @contestants.select { |contestant| !contestant.offline.present? }
@@ -212,12 +198,12 @@ class ContestsController < ApplicationController
@contest = Contest.find(params[:contest_id]) @contest = Contest.find(params[:contest_id])
end end
def contest_params def new_contest_params
params.expect(contest: [ :lang, :name, :offline_form, :public, :team, :allow_registration ]) params.expect(contest: [ :name ])
end end
def settings_general_params def settings_general_params
params.expect(contest: [ :lang, :name, :public, :team, :allow_registration ]) params.expect(contest: [ :lang, :name, :public, :ranking_mode, :team, :allow_registration ])
end end
def settings_offline_params def settings_offline_params

3
app/lib/ranking.rb Normal file
View File

@@ -0,0 +1,3 @@
module Ranking
AVAILABLE_RANKING_MODES = [ { id: "actual", name: I18n.t("lib.ranking.actual") }, { id: "theorical", name: I18n.t("lib.ranking.theorical") } ]
end

View File

@@ -8,6 +8,7 @@
# name :string # name :string
# offline_form :boolean default(FALSE) # offline_form :boolean default(FALSE)
# public :boolean default(FALSE) # public :boolean default(FALSE)
# ranking_mode :string
# slug :string # slug :string
# team :boolean default(FALSE) # team :boolean default(FALSE)
# created_at :datetime not null # created_at :datetime not null
@@ -38,6 +39,7 @@ class Contest < ApplicationRecord
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] } }
generates_token_for :token generates_token_for :token
end end

View File

@@ -14,6 +14,11 @@
.form-check.form-switch .form-check.form-switch
= form.check_box :public, class: "form-check-input" = form.check_box :public, class: "form-check-input"
= form.label :public = form.label :public
.row.mb-3
.col
.form-floating
= form.select :ranking_mode, Ranking::AVAILABLE_RANKING_MODES.map { |mode| [ mode[:name], mode[:id] ] }, {}, class: "form-select"
= form.label :ranking_mode
.row.mb-3 .row.mb-3
.col .col
.form-check.form-switch .form-check.form-switch

View File

@@ -58,6 +58,7 @@ en:
offline_form_description: Offline participants will have to fill the form by providing an image taken of the puzzle before starting solving it, and validate their finish time with an upload of an image of the completed puzzle offline_form_description: Offline participants will have to fill the form by providing an image taken of the puzzle before starting solving it, and validate their finish time with an upload of an image of the completed puzzle
offline_form_warning: Only for single-puzzle contests offline_form_warning: Only for single-puzzle contests
public: Enable the public scoreboard public: Enable the public scoreboard
ranking_mode: Ranking mode (public scoreboard & CSV exports)
team: Team contest team: Team contest
team_description: For UI display purposes mainly team_description: For UI display purposes mainly
allow_registration: Allow registration allow_registration: Allow registration
@@ -259,6 +260,10 @@ en:
field: Field field: Field
none: No field selected none: No field selected
rank: Rank rank: Rank
lib:
ranking:
actual: First by time if completed, then by number of pieces assembled
theorical: By time only (projected time calculated with the ppm count)
messages: messages:
index: index:
no_messages: No messages received yet no_messages: No messages received yet

View File

@@ -29,6 +29,7 @@ fr:
offline_form_description: Les participant.e.s hors-ligne pourront participer en prenant une photo du puzzle avant de le commencer, puis valider leur temps avec une photo du puzzle une fois complété offline_form_description: Les participant.e.s hors-ligne pourront participer en prenant une photo du puzzle avant de le commencer, puis valider leur temps avec une photo du puzzle une fois complété
offline_form_warning: Activable uniquement pour les concours avec un seul puzzle offline_form_warning: Activable uniquement pour les concours avec un seul puzzle
public: Activer le classement public public: Activer le classement public
ranking_mode: Mode de classement (classement public & exports CSV)
team: Concours par équipes team: Concours par équipes
team_description: Principalement pour des raisons d'affichage team_description: Principalement pour des raisons d'affichage
allow_registration: Autoriser l'inscription via l'interface allow_registration: Autoriser l'inscription via l'interface
@@ -230,6 +231,10 @@ fr:
field: Champ field: Champ
none: Aucun champ sélectionné none: Aucun champ sélectionné
rank: Rang rank: Rang
lib:
ranking:
actual: Par temps d'abord, puis par nombre de pièces assemblées
theorical: Par temps uniquement (temps projeté calculé à partir de la vitesse d'assemblage)
messages: messages:
index: index:
no_messages: Pas de messages reçus pour le moment no_messages: Pas de messages reçus pour le moment

View File

@@ -0,0 +1,5 @@
class AddRankingModeToContest < ActiveRecord::Migration[8.0]
def change
add_column :contests, :ranking_mode, :string
end
end

3
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_10_110151) do ActiveRecord::Schema[8.0].define(version: 2025_11_14_085123) 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
@@ -95,6 +95,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_10_110151) do
t.string "lang", default: "en" t.string "lang", default: "en"
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.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

View File

@@ -8,6 +8,7 @@
# name :string # name :string
# offline_form :boolean default(FALSE) # offline_form :boolean default(FALSE)
# public :boolean default(FALSE) # public :boolean default(FALSE)
# ranking_mode :string
# slug :string # slug :string
# team :boolean default(FALSE) # team :boolean default(FALSE)
# created_at :datetime not null # created_at :datetime not null