From 5dd8e97c6b71d022b563d6b9cd98e376293c5ea2 Mon Sep 17 00:00:00 2001 From: sto Date: Tue, 11 Aug 2026 09:26:42 +0200 Subject: [PATCH] Rework of completion forms --- app/controllers/completions_controller.rb | 10 +- .../concerns/completions_concern.rb | 21 ++ app/controllers/contestants_controller.rb | 7 +- app/views/completions/_form.html.slim | 208 ++++++++++++------ config/locales/en.yml | 7 + config/locales/fr.yml | 7 + 6 files changed, 193 insertions(+), 67 deletions(-) diff --git a/app/controllers/completions_controller.rb b/app/controllers/completions_controller.rb index 936f287..f2ed13b 100644 --- a/app/controllers/completions_controller.rb +++ b/app/controllers/completions_controller.rb @@ -14,7 +14,6 @@ class CompletionsController < ApplicationController authorize @contest @completion = Completion.new - @completion.completed = true if params[:contestant_id] @completion.contestant_id = params[:contestant_id] end @@ -25,6 +24,8 @@ class CompletionsController < ApplicationController @completion = Completion.new(completion_params) @completion.contest = @contest + process_completion + if @completion.save extend_completions!(@completion.contestant) if @contestant && !params[:completion].key?(:message_id) @@ -44,7 +45,10 @@ class CompletionsController < ApplicationController authorize @contest @completion.contestant_id = params[:contestant_id] if params[:contestant_id] - if @completion.update(completion_params) + @completion.update(completion_params) + process_completion + + if @completion.update({}) extend_completions!(@completion.contestant) if @contestant redirect_to edit_contest_contestant_path(@contest, @contestant), notice: t("completions.edit.notice") @@ -59,7 +63,9 @@ class CompletionsController < ApplicationController def destroy authorize @contest + contestant = @completion.contestant @completion.destroy + extend_completions!(contestant) if params[:contestant_id] redirect_to contest_contestant_path(@contest, params[:contestant_id]), notice: t("completions.destroy.notice") else diff --git a/app/controllers/concerns/completions_concern.rb b/app/controllers/concerns/completions_concern.rb index fb325fb..3ea6b85 100644 --- a/app/controllers/concerns/completions_concern.rb +++ b/app/controllers/concerns/completions_concern.rb @@ -41,6 +41,27 @@ module CompletionsConcern else contestant.update(display_time: display_time(completions[0].time_seconds), time_seconds: completions[0].time_seconds) end + else + contestant.update(display_time: nil, time_seconds: nil) + end + end + + def process_completion + if params.key?(:completed_radio) && params[:completed_radio] == "yes" + @completion.completed = true + end + if params.key?(:completed_radio) && params[:completed_radio] == "no" + @completion.completed = false + end + if params.key?(:pieces_radio) + @pieces_radio = params[:pieces_radio] + end + if @pieces_radio == "assembled" + begin + puzzle = Puzzle.find(@completion.puzzle_id) + @completion.remaining_pieces = puzzle.pieces - @completion.remaining_pieces + rescue + end end end end diff --git a/app/controllers/contestants_controller.rb b/app/controllers/contestants_controller.rb index 857e564..304144e 100644 --- a/app/controllers/contestants_controller.rb +++ b/app/controllers/contestants_controller.rb @@ -221,10 +221,9 @@ class ContestantsController < ApplicationController @contest = @contestant.contest @puzzles = @contest.puzzles.where(hidden: false).or(@contest.puzzles.where(hidden: nil)).order(:id) @completion = Completion.new - @completion.completed = true @completion.code = @contest.code - render "completions/_form", locals: { completion: @completion, submit_text: t("helpers.buttons.create"), method: :post, url: "/public/p/#{params[:contestant_id]}" } + render "completions/_form", locals: { completion: @completion, submit_text: t("helpers.buttons.validate"), method: :post, url: "/public/p/#{params[:contestant_id]}" } end def post_public_completion @@ -245,12 +244,14 @@ class ContestantsController < ApplicationController @completion.contest = @contest @completion.contestant = @contestant + process_completion + if @completion.save extend_completions!(@completion.contestant) redirect_to "/public/p/#{params[:contestant_id]}/updated" else @puzzles = @contest.puzzles - render "completions/_form", locals: { completion: @completion, submit_text: t("helpers.buttons.create"), method: :post, url: "/public/p/#{params[:contestant_id]}" }, status: :unprocessable_entity + render "completions/_form", locals: { completion: @completion, submit_text: t("helpers.buttons.validate"), method: :post, url: "/public/p/#{params[:contestant_id]}" }, status: :unprocessable_entity end end diff --git a/app/views/completions/_form.html.slim b/app/views/completions/_form.html.slim index a73dfdc..37a5fac 100644 --- a/app/views/completions/_form.html.slim +++ b/app/views/completions/_form.html.slim @@ -1,39 +1,51 @@ - if @public && @puzzles.length == @contestant.completions.length + / Case when all puzzles are already solved by the contestant. 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 a message is present, for online contests, display it. - if @message = form.hidden_field :message_id, value: @message.id - .row.mb-3 + .row .col h4 = t("messages.singular").capitalize - .alert.alert-secondary + .alert.alert-secondary.mt-3 b = @message.author br = @message.text - .row.mb-2 + + / Section title. + .row.mt-3 .col h4 - if @public = t("completions.form.validate_name", name: @contestant.name) - else = t("completions.singular").capitalize + + / If no contestant is selected, display a select to choose one. - if @contestants.present? - .row.mb-3 + .row.mt-3 .col .form-floating = form.select :contestant_id, @contestants.map { |contestant| [contestant.form_name, contestant.id] }, {}, class: "form-select" = form.label :contestant_id + + / If a closest contestant was computed, pre-select it. - if @closest_contestant javascript: el = document.querySelector('select[name="completion[contestant_id]"]'); el.value = "#{@closest_contestant.id}" + + / If the contest has more than one puzzle to solve, display a select to choose one. - if @puzzles.size > 1 - .row.mb-3 + .row.mt-3 .col .form-floating = form.select :puzzle_id, @puzzles.map { |puzzle| ["#{puzzle.name} - #{puzzle.brand}", puzzle.id] }, {}, class: "form-select" @@ -42,91 +54,163 @@ = form.hidden_field :puzzle_id, value: @puzzles.first.id - else = form.hidden_field :puzzle_id - .row.mb-3 + + / Radio button for whether the puzzle is completed or not. + .row.mt-3 .col - .form-check.form-switch - = form.check_box :completed, class: "form-check-input" - = form.label :completed + .form-text style="font-size: 18px;" + = t("completions.form.completed_question") + .form-text.mb-2 + i + = t("completions.form.completed_note") + + - if @completion.completed + input.btn-check type="radio" name="completed_radio" id="completed_radio_yes" value="yes" checked="" + - else + input.btn-check type="radio" name="completed_radio" id="completed_radio_yes" value="yes" + label.btn.btn-secondary for="completed_radio_yes" = t("completions.form.completed_yes") + - if @completion.completed == false + input.btn-check type="radio" name="completed_radio" id="completed_radio_no" value="no" checked="" + - else + input.btn-check type="radio" name="completed_radio" id="completed_radio_no" value="no" + label.ms-2.btn.btn-secondary for="completed_radio_no" = t("completions.form.completed_no") + javascript: savetime = ""; ispatch = #{method == :patch}; - completedEl = document.getElementById('completion_completed'); - completedEl.addEventListener('change', (e) => { + + // Actions for radio buttons. + completed_radio_yes_el = document.getElementById('completed_radio_yes'); + completed_radio_yes_el?.addEventListener('click', () => { const timeEl = document.getElementById('time'); const missingPiecesEl = document.getElementById('missing_pieces'); const remainingPiecesEl = document.getElementById('remaining_pieces'); const refreshEl = document.getElementById('refresh'); - if (e.target.checked) { - if (ispatch) timeEl.value = '#{@completion.display_time_from_start}'; - else timeEl.value = savetime; - missingPiecesEl.style.display = 'block'; - remainingPiecesEl.style.display = 'none'; - if (refreshEl) refreshEl.style.display = ''; - } else { - savetime = timeEl.value; - timeEl.value = '#{display_time(@contest.duration_seconds)}'; - missingPiecesEl.style.display = 'none'; - remainingPiecesEl.style.display = 'block'; - if (refreshEl) refreshEl.style.display = 'none'; - } - }) - .row.mb-3 + const submitEl = document.getElementById('submit'); + if (ispatch) timeEl.value = '#{@completion.display_time_from_start}'; + else timeEl.value = savetime; + timeEl.parentElement.parentElement.parentElement.style.display = ''; + missingPiecesEl.style.display = 'block'; + remainingPiecesEl.style.display = 'none'; + submitEl.style.display = ''; + if (refreshEl) refreshEl.style.display = ''; + }); + completed_radio_no_el = document.getElementById('completed_radio_no'); + completed_radio_no_el?.addEventListener('click', () => { + const timeEl = document.getElementById('time'); + const missingPiecesEl = document.getElementById('missing_pieces'); + const remainingPiecesEl = document.getElementById('remaining_pieces'); + const refreshEl = document.getElementById('refresh'); + const submitEl = document.getElementById('submit'); + savetime = timeEl.value; + timeEl.value = '#{display_time(@contest.duration_seconds)}'; + timeEl.parentElement.parentElement.parentElement.style.display = ''; + missingPiecesEl.style.display = 'none'; + remainingPiecesEl.style.display = 'block'; + submitEl.style.display = 'none'; + if (refreshEl) refreshEl.style.display = 'none'; + }); + + / Text field for recording the completion time. + .row.mt-4 style="display: #{@completion.completed != nil ? "" : "none"};" .col .form-floating = form.text_field :display_time_from_start, autocomplete: "off", class: "form-control", id: "time" = form.label :display_time_from_start, class: "required" - - unless method == :patch + - unless method == :patch || @contest.start_time == nil .col.mt-2 - a.btn.btn-primary id="refresh" + a.btn.btn-primary id="refresh" style="display: #{@completion.completed ? "" : "none"}" = t("helpers.buttons.refresh") .form-text = t("activerecord.attributes.completion.display_time_from_start_description") - - if @contest.start_time.present? && method != :patch - javascript: - startTime = #{@contest.start_time.present? ? @contest.start_time.to_i : "null"}; - pauseTime = #{@contest.pause_time.present? ? @contest.pause_time.to_i : "null"}; - function updateTime() { - const timeEl = document.getElementById('time'); - if (timeEl) { - if (startTime) { - let s = Math.floor((Date.now() - 1000 * startTime) / 1000); - if (pauseTime) s = Math.floor(pauseTime - startTime); - let ss = s % 60; - let mm = Math.floor(s / 60) % 60; - let hh = Math.floor(s / 3600); - timeEl.value = `${hh}:${mm < 10 ? `0${mm}` : mm}:${ss < 10 ? `0${ss}` : ss}`; - } + + / Refresh logic. + - if @contest.start_time.present? && method != :patch + javascript: + startTime = #{@contest.start_time.present? ? @contest.start_time.to_i : "null"}; + pauseTime = #{@contest.pause_time.present? ? @contest.pause_time.to_i : "null"}; + function updateTime() { + const timeEl = document.getElementById('time'); + if (timeEl) { + if (startTime) { + let s = Math.floor((Date.now() - 1000 * startTime) / 1000); + if (pauseTime) s = Math.floor(pauseTime - startTime); + let ss = s % 60; + let mm = Math.floor(s / 60) % 60; + let hh = Math.floor(s / 3600); + timeEl.value = `${hh}:${mm < 10 ? `0${mm}` : mm}:${ss < 10 ? `0${ss}` : ss}`; + savetime = timeEl.value; } } - refreshButton = document.getElementById("refresh"); - refreshButton?.addEventListener('click', updateTime); + } + refreshButton = document.getElementById("refresh"); + refreshButton?.addEventListener('click', updateTime); + - if @completion.display_time_from_start == nil + javascript: updateTime(); - .row.mb-3 id="missing_pieces" + + / Text field for recording the missing pieces, if any. + .row.mt-3 id="missing_pieces" style="display: #{@completion.completed ? "" : "none"};" .col .form-floating = form.text_field :missing_pieces, autocomplete: "off", class: "form-control" = form.label :missing_pieces - .row.mb-3 id="remaining_pieces" style="display: none;" + + / Text field for recording the remaining pieces. + .row.mt-3 id="remaining_pieces" style="display: #{@completion.completed == false ? "" : "none"};" .col - .form-floating + .form-text.mb-2 style="font-size: 18px;" + = t("completions.form.remaining_pieces_question") + + / Radio buttons for choice of whether counting assembled or remaining pieces + - if @pieces_radio == "assembled" + input.btn-check type="radio" name="pieces_radio" id="pieces_radio_assembled" value="assembled" checked="" + - else + input.btn-check type="radio" name="pieces_radio" id="pieces_radio_assembled" value="assembled" + label.btn.btn-secondary for="pieces_radio_assembled" = t("completions.form.pieces_radio_assembled") + - if @pieces_radio == "remaining" + input.btn-check type="radio" name="pieces_radio" id="pieces_radio_remaining" value="remaining" checked="" + - else + input.btn-check type="radio" name="pieces_radio" id="pieces_radio_remaining" value="remaining" + label.ms-2.btn.btn-secondary for="pieces_radio_remaining" = t("completions.form.pieces_radio_remaining") + + / Remaining_pieces_field + .form-floating.mt-3 id="remaining_pieces_field" style="display: #{@pieces_radio != nil ? "" : "none"};" = form.text_field :remaining_pieces, autocomplete: "off", class: "form-control" - = form.label :remaining_pieces - javascript: - completedEl = document.getElementById('completion_completed'); - missingPiecesEl = document.getElementById('missing_pieces'); - remainingPiecesEl = document.getElementById('remaining_pieces'); - if (completedEl.checked) { - missingPiecesEl.style.display = 'block'; - remainingPiecesEl.style.display = 'none'; - } else { - missingPiecesEl.style.display = 'none'; - remainingPiecesEl.style.display = 'block'; - } + - if @pieces_radio == "assembled" + = form.label :remaining_pieces + = t("completions.form.pieces_radio_assembled") + - else + = form.label :remaining_pieces + = t("completions.form.pieces_radio_remaining") + + javascript: + pieces_assembled_label = '#{t("completions.form.pieces_radio_assembled")}'; + pieces_remaining_label = '#{t("completions.form.pieces_radio_remaining")}'; + pieces_radio_assembled_el = document.getElementById('pieces_radio_assembled'); + pieces_radio_assembled_el?.addEventListener('click', () => { + pieces_remaining_el = document.getElementById('remaining_pieces_field'); + pieces_remaining_el.style.display = ''; + pieces_remaining_el.getElementsByTagName('label')[0].innerHTML = pieces_assembled_label; + document.getElementById('submit').style.display = ''; + }); + pieces_radio_remaining_el = document.getElementById('pieces_radio_remaining'); + pieces_radio_remaining_el?.addEventListener('click', () => { + pieces_remaining_el = document.getElementById('remaining_pieces_field'); + pieces_remaining_el.style.display = ''; + pieces_remaining_el.getElementsByTagName('label')[0].innerHTML = pieces_remaining_label; + document.getElementById('submit').style.display = ''; + }); + + / Hidden field for the organizer code, to ensure only organizer can use this form. + - if @public - .row.mb-3 + .row .col .form-floating = form.hidden_field :code, autocomplete: "off", class: "form-control" - .row + + / Submit button. + .row.mt-3 id="submit" style="display: #{@completion.completed != nil ? "" : "none"};" .col = form.submit submit_text, class: "btn btn-primary" \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 8d497fe..2154d84 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -196,6 +196,13 @@ en: form: all_finished: "All puzzles were already completed by %{name}" code: Judges code + completed_no: No + completed_note: "If only a few pieces are missing and can't be found, answer: yes" + completed_question: Is the puzzle completed? + completed_yes: Yes + pieces_radio_assembled: Assembled pieces + pieces_radio_remaining: Remaining pieces + remaining_pieces_question: Did you count the assembled pieces, or the remaining pieces? validate_name: "Validate a puzzle for %{name}" new: notice: Completion added diff --git a/config/locales/fr.yml b/config/locales/fr.yml index dee2c32..ad749a3 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -167,6 +167,13 @@ fr: form: all_finished: Tous les puzzles ont déjà été complétés par %{name} code: Code organisateur.ice + completed_no: Non + completed_note: "S'il manque juste quelques pièces qui sont introuvables, répondre : Oui" + completed_question: Est-ce que le puzzle est terminé/validé ? + completed_yes: Oui + pieces_radio_assembled: Pièces assemblées + pieces_radio_remaining: Pièces restantes + remaining_pieces_question: As-tu compté le nombre de pièces assemblées, ou le nombre de pièces restantes ? validate_name: "Valider un puzzle pour %{name}" new: notice: Complétion ajoutée