Add ranking mode
This commit is contained in:
@@ -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
|
||||||
|
|||||||
12
app/controllers/concerns/contestants_concern.rb
Normal file
12
app/controllers/concerns/contestants_concern.rb
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
3
app/lib/ranking.rb
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
5
db/migrate/20251114085123_add_ranking_mode_to_contest.rb
Normal file
5
db/migrate/20251114085123_add_ranking_mode_to_contest.rb
Normal 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
3
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_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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user