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

View File

@@ -1,7 +1,8 @@
class ContestsController < ApplicationController
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 :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 ]
@@ -60,7 +61,7 @@ class ContestsController < ApplicationController
def create
authorize :contest
@contest = Contest.new(contest_params)
@contest = Contest.new(new_contest_params)
@contest.user_id = current_user.id
if @contest.save
redirect_to "/contests/#{@contest.id}/settings/general", notice: t("contests.new.notice")
@@ -69,16 +70,6 @@ class ContestsController < ApplicationController
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
authorize @contest
@@ -97,12 +88,7 @@ class ContestsController < ApplicationController
I18n.locale = @contest.lang
@title = I18n.t("contests.scoreboard.title", name: @contest.name)
@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
] }
@contestants = ranked_contestants(@contest)
filter_contestants_per_category
if params.key?(:hide_offline) && params[:hide_offline] == "true"
@contestants = @contestants.select { |contestant| !contestant.offline.present? }
@@ -212,12 +198,12 @@ class ContestsController < ApplicationController
@contest = Contest.find(params[:contest_id])
end
def contest_params
params.expect(contest: [ :lang, :name, :offline_form, :public, :team, :allow_registration ])
def new_contest_params
params.expect(contest: [ :name ])
end
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
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
# offline_form :boolean default(FALSE)
# public :boolean default(FALSE)
# ranking_mode :string
# slug :string
# team :boolean default(FALSE)
# created_at :datetime not null
@@ -38,6 +39,7 @@ class Contest < ApplicationRecord
validates :name, presence: true
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
end

View File

@@ -14,6 +14,11 @@
.form-check.form-switch
= form.check_box :public, class: "form-check-input"
= 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
.col
.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_warning: Only for single-puzzle contests
public: Enable the public scoreboard
ranking_mode: Ranking mode (public scoreboard & CSV exports)
team: Team contest
team_description: For UI display purposes mainly
allow_registration: Allow registration
@@ -259,6 +260,10 @@ en:
field: Field
none: No field selected
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:
index:
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_warning: Activable uniquement pour les concours avec un seul puzzle
public: Activer le classement public
ranking_mode: Mode de classement (classement public & exports CSV)
team: Concours par équipes
team_description: Principalement pour des raisons d'affichage
allow_registration: Autoriser l'inscription via l'interface
@@ -230,6 +231,10 @@ fr:
field: Champ
none: Aucun champ sélectionné
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:
index:
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.
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|
t.string "name", 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.boolean "public", default: false
t.boolean "offline_form", default: false
t.string "ranking_mode"
t.index ["slug"], name: "index_contests_on_slug", unique: true
t.index ["user_id"], name: "index_contests_on_user_id"
end

View File

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