diff --git a/app/controllers/completions_controller.rb b/app/controllers/completions_controller.rb index 86cb4c6..027716b 100644 --- a/app/controllers/completions_controller.rb +++ b/app/controllers/completions_controller.rb @@ -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 diff --git a/app/controllers/concerns/contestants_concern.rb b/app/controllers/concerns/contestants_concern.rb new file mode 100644 index 0000000..5d60eb8 --- /dev/null +++ b/app/controllers/concerns/contestants_concern.rb @@ -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 diff --git a/app/controllers/contestants_controller.rb b/app/controllers/contestants_controller.rb index 1cdd1ab..9030025 100644 --- a/app/controllers/contestants_controller.rb +++ b/app/controllers/contestants_controller.rb @@ -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 diff --git a/app/controllers/contests_controller.rb b/app/controllers/contests_controller.rb index d345990..6edf6a5 100644 --- a/app/controllers/contests_controller.rb +++ b/app/controllers/contests_controller.rb @@ -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 diff --git a/app/lib/ranking.rb b/app/lib/ranking.rb new file mode 100644 index 0000000..983c5e5 --- /dev/null +++ b/app/lib/ranking.rb @@ -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 diff --git a/app/models/contest.rb b/app/models/contest.rb index 7d38812..7cd3488 100644 --- a/app/models/contest.rb +++ b/app/models/contest.rb @@ -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 diff --git a/app/views/contests/settings_general_edit.html.slim b/app/views/contests/settings_general_edit.html.slim index 296b24e..b73bc27 100644 --- a/app/views/contests/settings_general_edit.html.slim +++ b/app/views/contests/settings_general_edit.html.slim @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 7828c94..674e4e3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/config/locales/fr.yml b/config/locales/fr.yml index bb6b5a7..373bc52 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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 diff --git a/db/migrate/20251114085123_add_ranking_mode_to_contest.rb b/db/migrate/20251114085123_add_ranking_mode_to_contest.rb new file mode 100644 index 0000000..7560274 --- /dev/null +++ b/db/migrate/20251114085123_add_ranking_mode_to_contest.rb @@ -0,0 +1,5 @@ +class AddRankingModeToContest < ActiveRecord::Migration[8.0] + def change + add_column :contests, :ranking_mode, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index d62b282..39f174e 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_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 diff --git a/spec/factories/contests.rb b/spec/factories/contests.rb index 45e5875..f67e22f 100644 --- a/spec/factories/contests.rb +++ b/spec/factories/contests.rb @@ -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