Add judges codes
This commit is contained in:
@@ -24,7 +24,7 @@ class CompletionsController < ApplicationController
|
|||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
@completion = Completion.new(completion_params)
|
@completion = Completion.new(completion_params)
|
||||||
@completion.contest = @contet
|
@completion.contest = @contest
|
||||||
if @completion.save
|
if @completion.save
|
||||||
extend_completions!(@completion.contestant)
|
extend_completions!(@completion.contestant)
|
||||||
if @contestant && !params[:completion].key?(:message_id)
|
if @contestant && !params[:completion].key?(:message_id)
|
||||||
|
|||||||
@@ -123,12 +123,6 @@ class ContestantsController < ApplicationController
|
|||||||
def generate_qrcodes
|
def generate_qrcodes
|
||||||
authorize @contest
|
authorize @contest
|
||||||
|
|
||||||
@contest.contestants.each do |contestant|
|
|
||||||
if !contestant.qrcode.present?
|
|
||||||
contestant.public_generate_qrcode
|
|
||||||
contestant.save
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@contestants = @contest.contestants.sort_by { |contestant| contestant.name }
|
@contestants = @contest.contestants.sort_by { |contestant| contestant.name }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -140,6 +134,7 @@ class ContestantsController < ApplicationController
|
|||||||
not_found and return
|
not_found and return
|
||||||
end
|
end
|
||||||
@contest = @contestant.contest
|
@contest = @contestant.contest
|
||||||
|
I18n.locale = @contest.lang
|
||||||
@puzzles = @contest.puzzles
|
@puzzles = @contest.puzzles
|
||||||
@completion = Completion.new
|
@completion = Completion.new
|
||||||
@completion.completed = true
|
@completion.completed = true
|
||||||
@@ -156,16 +151,24 @@ class ContestantsController < ApplicationController
|
|||||||
not_found and return
|
not_found and return
|
||||||
end
|
end
|
||||||
@contest = @contestant.contest
|
@contest = @contestant.contest
|
||||||
|
I18n.locale = @contest.lang
|
||||||
|
|
||||||
@completion = Completion.new(completion_params)
|
@completion = Completion.new(completion_params)
|
||||||
@completion.contest = @contest
|
@completion.contest = @contest
|
||||||
@completion.contestant = @contestant
|
@completion.contestant = @contestant
|
||||||
|
if !@completion.code.present?
|
||||||
|
to_modify = true
|
||||||
|
@completion.code = "incorrect-xZy"
|
||||||
|
end
|
||||||
if @completion.save
|
if @completion.save
|
||||||
extend_completions!(@completion.contestant)
|
extend_completions!(@completion.contestant)
|
||||||
redirect_to "/public/p/#{params[:token]}/updated"
|
redirect_to "/public/p/#{params[:token]}/updated"
|
||||||
else
|
else
|
||||||
@puzzles = @contest.puzzles
|
@puzzles = @contest.puzzles
|
||||||
@public = true
|
@public = true
|
||||||
|
if to_modify
|
||||||
|
@completion.code = nil
|
||||||
|
end
|
||||||
render "completions/_form", locals: { completion: @completion, submit_text: t("helpers.buttons.create"), method: :post, url: "/public/p/#{params[:token]}" }, status: :unprocessable_entity
|
render "completions/_form", locals: { completion: @completion, submit_text: t("helpers.buttons.create"), method: :post, url: "/public/p/#{params[:token]}" }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -177,6 +180,7 @@ class ContestantsController < ApplicationController
|
|||||||
if !@contestant
|
if !@contestant
|
||||||
not_found and return
|
not_found and return
|
||||||
end
|
end
|
||||||
|
I18n.locale = @contestant.contest.lang
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -219,6 +223,6 @@ class ContestantsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def completion_params
|
def completion_params
|
||||||
params.expect(completion: [ :display_time_from_start, :completed, :missing_pieces, :remaining_pieces, :puzzle_id ])
|
params.expect(completion: [ :display_time_from_start, :completed, :missing_pieces, :remaining_pieces, :puzzle_id, :code ])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ class ContestsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def settings_general_params
|
def settings_general_params
|
||||||
params.expect(contest: [ :lang, :name, :duration, :public, :ranking_mode, :team, :allow_registration ])
|
params.expect(contest: [ :lang, :name, :duration, :public, :ranking_mode, :team, :allow_registration, :code ])
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_offline_params
|
def settings_offline_params
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# Table name: completions
|
# Table name: completions
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
|
# code :string
|
||||||
# completed :boolean
|
# completed :boolean
|
||||||
# display_relative_time :string
|
# display_relative_time :string
|
||||||
# display_time_from_start :string
|
# display_time_from_start :string
|
||||||
@@ -50,10 +51,11 @@ class Completion < ApplicationRecord
|
|||||||
validates :puzzle_id, uniqueness: { scope: :contestant }, if: -> { contest.puzzles.size > 1 }
|
validates :puzzle_id, uniqueness: { scope: :contestant }, if: -> { contest.puzzles.size > 1 }
|
||||||
validates :remaining_pieces, numericality: { only_integer: true }, if: -> { remaining_pieces.present? }
|
validates :remaining_pieces, numericality: { only_integer: true }, if: -> { remaining_pieces.present? }
|
||||||
validate :remaining_pieces_is_correct, if: -> { remaining_pieces.present? }
|
validate :remaining_pieces_is_correct, if: -> { remaining_pieces.present? }
|
||||||
|
validate :contest_code_is_correct, if: -> { code.present? }
|
||||||
|
|
||||||
def remaining_pieces_is_correct
|
def remaining_pieces_is_correct
|
||||||
if self.remaining_pieces > self.puzzle.pieces
|
if self.remaining_pieces > self.puzzle.pieces
|
||||||
errors.add(:remaining_pieces, "Cannot be greater than the number of pieces for this puzzle")
|
errors.add(:remaining_pieces, I18n.t("activerecord.errors.models.completion.attributes.remaining_pieces.too_large"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -92,4 +94,10 @@ class Completion < ApplicationRecord
|
|||||||
self.projected_time = assembled_time + Integer(self.remaining_pieces.to_f / pieces_per_second)
|
self.projected_time = assembled_time + Integer(self.remaining_pieces.to_f / pieces_per_second)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def contest_code_is_correct
|
||||||
|
if self.code != self.contest.code
|
||||||
|
errors.add(:code, I18n.t("activerecord.errors.models.completion.attributes.code.mismatch"))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# allow_registration :boolean default(FALSE)
|
# allow_registration :boolean default(FALSE)
|
||||||
|
# code :string
|
||||||
# duration :string
|
# duration :string
|
||||||
# duration_seconds :integer
|
# duration_seconds :integer
|
||||||
# lang :string default("en")
|
# lang :string default("en")
|
||||||
|
|||||||
@@ -43,10 +43,6 @@ class Contestant < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def public_generate_qrcode
|
|
||||||
self.generate_qrcode
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def initialize_time_seconds_if_empty
|
def initialize_time_seconds_if_empty
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
= form_with model: completion, url: url, method: method do |form|
|
- if @public && @puzzles.length == @contestant.completions.length
|
||||||
|
h4
|
||||||
|
= t("completions.form.validate_name", name: @contestant.name)
|
||||||
|
.mt-3.alert.alert-warning
|
||||||
|
= t("completions.form.all_finished", name: @contestant.name)
|
||||||
|
- else
|
||||||
|
= form_with model: completion, url: url, method: method do |form|
|
||||||
- if @message
|
- if @message
|
||||||
= form.hidden_field :message_id, value: @message.id
|
= form.hidden_field :message_id, value: @message.id
|
||||||
.row.mb-3
|
.row.mb-3
|
||||||
@@ -83,6 +89,13 @@
|
|||||||
missingPiecesEl.style.display = 'none';
|
missingPiecesEl.style.display = 'none';
|
||||||
remainingPiecesEl.style.display = 'block';
|
remainingPiecesEl.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
- if @public
|
||||||
|
.row.mb-3
|
||||||
|
.col
|
||||||
|
.form-floating
|
||||||
|
= form.text_field :code, autocomplete: "off", class: "form-control"
|
||||||
|
= form.label :code
|
||||||
|
= t("completions.form.code")
|
||||||
.row
|
.row
|
||||||
.col
|
.col
|
||||||
= form.submit submit_text, class: "btn btn-primary"
|
= form.submit submit_text, class: "btn btn-primary"
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
.row.mb-4 style="height: calc(100vh - 280px)"
|
- if @contest.code.present?
|
||||||
|
.row.mb-4 style="height: calc(100vh - 280px)"
|
||||||
|
.row
|
||||||
|
.col
|
||||||
|
.alert.alert-info
|
||||||
|
= t("contestants.generate_qrcodes.note")
|
||||||
.col.d-flex.flex-column style="height: 100%"
|
.col.d-flex.flex-column style="height: 100%"
|
||||||
.d-flex.flex-column style="overflow-y: auto"
|
.d-flex.flex-column style="overflow-y: auto"
|
||||||
- for row in 0..((@contestants.length - 1) / 4)
|
- for row in 0..((@contestants.length - 1) / 5)
|
||||||
.mt-4.d-flex.flex-row
|
.mt-4.d-flex.flex-row
|
||||||
- for col in 0..3
|
- for col in 0..4
|
||||||
- if row * 4 + col < @contestants.length
|
- if row * 5 + col < @contestants.length
|
||||||
.d-flex.flex-column.ms-5 style="align-items: center"
|
.d-flex.flex-column.ms-5 style="align-items: center"
|
||||||
= @contestants[row * 4 + col].name
|
= @contestants[row * 5 + col].name
|
||||||
- if @contestants[row * 4 + col].qrcode.present?
|
- if @contestants[row * 5 + col].qrcode.present?
|
||||||
.mt-1 style="width: 200px; height: 200px;"
|
.mt-1 style="width: 180px; height: 180px;"
|
||||||
= @contestants[row * 4 + col].qrcode.html_safe
|
= @contestants[row * 5 + col].qrcode.html_safe
|
||||||
|
- else
|
||||||
|
.row
|
||||||
|
.col
|
||||||
|
.alert.alert-warning
|
||||||
|
= t("contestants.generate_qrcodes.no_code_note")
|
||||||
@@ -20,6 +20,12 @@
|
|||||||
.form-floating
|
.form-floating
|
||||||
= form.select :ranking_mode, Ranking::AVAILABLE_RANKING_MODES.map { |mode| [ mode[:name], mode[:id] ] }, {}, class: "form-select"
|
= form.select :ranking_mode, Ranking::AVAILABLE_RANKING_MODES.map { |mode| [ mode[:name], mode[:id] ] }, {}, class: "form-select"
|
||||||
= form.label :ranking_mode
|
= form.label :ranking_mode
|
||||||
|
.row.mt-2.mb-3
|
||||||
|
.col
|
||||||
|
.form-floating
|
||||||
|
= form.text_field :code, autocomplete: "off", class: "form-control"
|
||||||
|
= form.label :code, class: "required"
|
||||||
|
.form-text = t("activerecord.attributes.contest.code_description")
|
||||||
.row.mt-4.mb-3
|
.row.mt-4.mb-3
|
||||||
.col
|
.col
|
||||||
.form-check.form-switch
|
.form-check.form-switch
|
||||||
|
|||||||
@@ -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:
|
||||||
|
code: Code for onsite judges
|
||||||
|
code_description: Only useful for onsite contests, if printing QR codes on tables
|
||||||
duration: Duration
|
duration: Duration
|
||||||
duration_description: Format h:mm or hh:mm
|
duration_description: Format h:mm or hh:mm
|
||||||
lang: Language for the public scoreboard
|
lang: Language for the public scoreboard
|
||||||
@@ -103,6 +105,8 @@ en:
|
|||||||
models:
|
models:
|
||||||
completion:
|
completion:
|
||||||
attributes:
|
attributes:
|
||||||
|
code:
|
||||||
|
mismatch: "Wrong code"
|
||||||
contestant_id:
|
contestant_id:
|
||||||
taken: "This contestant has already completed the puzzle"
|
taken: "This contestant has already completed the puzzle"
|
||||||
display_time_from_start:
|
display_time_from_start:
|
||||||
@@ -114,6 +118,7 @@ en:
|
|||||||
blank: This is required
|
blank: This is required
|
||||||
not_an_integer: This is not an integer
|
not_an_integer: This is not an integer
|
||||||
not_a_number: This is not an integer
|
not_a_number: This is not an integer
|
||||||
|
too_large: "Cannot be greater than the number of pieces for this puzzle"
|
||||||
contest:
|
contest:
|
||||||
attributes:
|
attributes:
|
||||||
duration:
|
duration:
|
||||||
@@ -171,6 +176,8 @@ en:
|
|||||||
notice: Completion updated
|
notice: Completion updated
|
||||||
title: Edit completion
|
title: Edit completion
|
||||||
form:
|
form:
|
||||||
|
all_finished: "All puzzles were already completed by %{name}"
|
||||||
|
code: Judges code
|
||||||
validate_name: "Validate a puzzle for %{name}"
|
validate_name: "Validate a puzzle for %{name}"
|
||||||
new:
|
new:
|
||||||
notice: Completion added
|
notice: Completion added
|
||||||
@@ -226,6 +233,9 @@ en:
|
|||||||
team_title: Teams
|
team_title: Teams
|
||||||
finalize_import:
|
finalize_import:
|
||||||
title: Import participants
|
title: Import participants
|
||||||
|
generate_qrcodes:
|
||||||
|
note: These QR codes allow for judges to fill in results without the need of the organizer's account, for example by printing them and placing them on participant tables
|
||||||
|
no_code_note: Those can't be used until a code for judges has been set up in the general settings
|
||||||
import:
|
import:
|
||||||
email_column: Participant email
|
email_column: Participant email
|
||||||
import_column: Import?
|
import_column: Import?
|
||||||
|
|||||||
@@ -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:
|
||||||
|
code: Code pour les organisateur.ice.s
|
||||||
|
code_description: Utile uniquement pour les concours en présentiel, si les QR codes sont imprimés et placés sur les tables
|
||||||
duration: Durée
|
duration: Durée
|
||||||
duration_description: Format h:mm ou hh:mm
|
duration_description: Format h:mm ou hh:mm
|
||||||
lang: Langue pour le classement public
|
lang: Langue pour le classement public
|
||||||
@@ -74,6 +76,8 @@ fr:
|
|||||||
models:
|
models:
|
||||||
completion:
|
completion:
|
||||||
attributes:
|
attributes:
|
||||||
|
code:
|
||||||
|
mismatch: "Code non valide"
|
||||||
contestant_id:
|
contestant_id:
|
||||||
taken: "Ce.tte participant.e a déjà complété le puzzle"
|
taken: "Ce.tte participant.e a déjà complété le puzzle"
|
||||||
display_time_from_start:
|
display_time_from_start:
|
||||||
@@ -85,6 +89,7 @@ fr:
|
|||||||
blank: Ce champ est obligatoire
|
blank: Ce champ est obligatoire
|
||||||
not_an_integer: Ce n'est pas un nombre entier
|
not_an_integer: Ce n'est pas un nombre entier
|
||||||
not_a_number: Ce n'est pas un nombre entier
|
not_a_number: Ce n'est pas un nombre entier
|
||||||
|
too_large: "Ne peut pas être plus grand que le nombre de pièces du puzzle"
|
||||||
contest:
|
contest:
|
||||||
attributes:
|
attributes:
|
||||||
duration:
|
duration:
|
||||||
@@ -142,6 +147,8 @@ fr:
|
|||||||
notice: Complétion modifiée
|
notice: Complétion modifiée
|
||||||
title: Modifier la complétion
|
title: Modifier la complétion
|
||||||
form:
|
form:
|
||||||
|
all_finished: Tous les puzzles ont déjà été complétés par %{name}
|
||||||
|
code: Code organisateur.ice
|
||||||
validate_name: "Valider un puzzle pour %{name}"
|
validate_name: "Valider un puzzle pour %{name}"
|
||||||
new:
|
new:
|
||||||
notice: Complétion ajoutée
|
notice: Complétion ajoutée
|
||||||
@@ -197,6 +204,9 @@ fr:
|
|||||||
team_title: Équipe
|
team_title: Équipe
|
||||||
finalize_import:
|
finalize_import:
|
||||||
title: Importer des participant.e.s
|
title: Importer des participant.e.s
|
||||||
|
generate_qrcodes:
|
||||||
|
note: Ces QR codes permettent, quand imprimés et placés sur les tables des participant.e.s, aux organisateur.ice.s de valider les temps de complétion des puzzles
|
||||||
|
no_code_note: Les codes ne seront générés qu'une fois un code pour les organisateur.ice.s défini dans les paramètres généraux
|
||||||
import:
|
import:
|
||||||
email_column: Email des participant.e.s
|
email_column: Email des participant.e.s
|
||||||
import_column: Importer ?
|
import_column: Importer ?
|
||||||
|
|||||||
5
db/migrate/20251120150744_add_code_to_contest.rb
Normal file
5
db/migrate/20251120150744_add_code_to_contest.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddCodeToContest < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :contests, :code, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
5
db/migrate/20251120152211_add_code_to_completion.rb
Normal file
5
db/migrate/20251120152211_add_code_to_completion.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddCodeToCompletion < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :completions, :code, :string
|
||||||
|
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_20_100813) do
|
ActiveRecord::Schema[8.0].define(version: 2025_11_20_152211) 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
|
||||||
@@ -68,6 +68,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_20_100813) do
|
|||||||
t.integer "missing_pieces"
|
t.integer "missing_pieces"
|
||||||
t.boolean "completed"
|
t.boolean "completed"
|
||||||
t.integer "projected_time"
|
t.integer "projected_time"
|
||||||
|
t.string "code"
|
||||||
t.index ["contest_id"], name: "index_completions_on_contest_id"
|
t.index ["contest_id"], name: "index_completions_on_contest_id"
|
||||||
t.index ["contestant_id"], name: "index_completions_on_contestant_id"
|
t.index ["contestant_id"], name: "index_completions_on_contestant_id"
|
||||||
t.index ["message_id"], name: "index_completions_on_message_id"
|
t.index ["message_id"], name: "index_completions_on_message_id"
|
||||||
@@ -101,6 +102,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_20_100813) do
|
|||||||
t.string "ranking_mode"
|
t.string "ranking_mode"
|
||||||
t.string "duration"
|
t.string "duration"
|
||||||
t.integer "duration_seconds"
|
t.integer "duration_seconds"
|
||||||
|
t.string "code"
|
||||||
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,7 @@
|
|||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# allow_registration :boolean default(FALSE)
|
# allow_registration :boolean default(FALSE)
|
||||||
|
# code :string
|
||||||
# duration :string
|
# duration :string
|
||||||
# duration_seconds :integer
|
# duration_seconds :integer
|
||||||
# lang :string default("en")
|
# lang :string default("en")
|
||||||
|
|||||||
Reference in New Issue
Block a user