From ff5f387a87035be083b1c0eb4e69ecdc9a329b53 Mon Sep 17 00:00:00 2001 From: sto Date: Fri, 31 Oct 2025 11:55:28 +0100 Subject: [PATCH] Offline participation: implement completed puzzle methods --- app/controllers/contests_controller.rb | 47 +++++++++++++++++-- app/models/offline.rb | 20 +++++--- app/policies/contest_policy.rb | 4 ++ .../contests/offline_completed.html.slim | 1 + app/views/contests/offline_edit.html.slim | 26 ++++++++++ app/views/contests/offline_new.html.slim | 2 +- config/locales/en.yml | 6 ++- config/locales/fr.yml | 4 ++ config/routes.rb | 5 +- ...20251031095604_add_completed_to_offline.rb | 5 ++ db/schema.rb | 3 +- spec/factories/offlines.rb | 3 +- spec/models/offline_spec.rb | 1 + 13 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 app/views/contests/offline_completed.html.slim create mode 100644 app/views/contests/offline_edit.html.slim create mode 100644 db/migrate/20251031095604_add_completed_to_offline.rb diff --git a/app/controllers/contests_controller.rb b/app/controllers/contests_controller.rb index 17ecc01..6287eeb 100644 --- a/app/controllers/contests_controller.rb +++ b/app/controllers/contests_controller.rb @@ -1,7 +1,7 @@ class ContestsController < ApplicationController before_action :set_contest, only: %i[ destroy edit show update ] - before_action :offline_setup, only: %i[ offline_new offline_create ] - skip_before_action :require_authentication, only: %i[ scoreboard offline_new offline_create ] + 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 ] def index authorize :contest @@ -111,12 +111,47 @@ class ContestsController < ApplicationController @offline.contest = @contest @offline.start_time = Time.now() if @offline.save - redirect_to "/public/#{@contest.friendly_id}/offline/test" + redirect_to "/public/#{@contest.friendly_id}/offline/#{@offline.generate_token_for(:token)}" else render :offline_new, status: :unprocessable_entity end end + def offline_edit + authorize @contest + + @offline = Offline.find_by_token_for(:token, params[:token]) + if !@offline + not_found and return + end + end + + def offline_update + authorize @contest + + @offline = Offline.find_by_token_for(:token, params[:token]) + if !@offline + not_found and return + end + + @offline.completed = true + @offline.images.attach(params[:offline][:end_image]) + if @offline.save + redirect_to "/public/#{@contest.friendly_id}/offline/#{@offline.generate_token_for(:token)}/completed" + else + render :offline_edit, status: :unprocessable_entity + end + end + + def offline_completed + authorize @contest + + @offline = Offline.find_by_token_for(:token, params[:token]) + if !@offline + not_found and return + end + end + private def offline_setup @@ -150,6 +185,10 @@ class ContestsController < ApplicationController end def offline_start_params - params.expect(offline: [ :name, :start_image ]) + params.expect(offline: [ :name, :images ]) + end + + def offline_end_params + params.expect(offline: [ :completed, :end_image ]) end end diff --git a/app/models/offline.rb b/app/models/offline.rb index f140626..1baed99 100644 --- a/app/models/offline.rb +++ b/app/models/offline.rb @@ -3,6 +3,7 @@ # Table name: offlines # # id :integer not null, primary key +# completed :boolean # end_time :datetime # name :string not null # start_time :datetime not null @@ -21,20 +22,25 @@ class Offline < ApplicationRecord belongs_to :contest - has_one_attached :start_image - has_one_attached :end_image + has_many_attached :images + + generates_token_for :token validates :name, presence: true validates :start_time, presence: true + validate :end_image_is_present validate :start_image_is_present + def end_image_is_present + if self.completed && self.images.length < 2 + errors.add(:end_image, I18n.t("activerecord.errors.models.offline.attributes.end_image.blank")) + end + end + def start_image_is_present - logger = Logger.new(STDOUT) - logger.info "TESTddfzefzef" - logger.info self.start_image.attached? - if !self.start_image.attached? - errors.add(:start_image, I18n.t("activerecord.errors.models.offline.attributes.start_image.blank")) + if !self.images.attached? + errors.add(:images, I18n.t("activerecord.errors.models.offline.attributes.start_image.blank")) end end end diff --git a/app/policies/contest_policy.rb b/app/policies/contest_policy.rb index c435e3a..62a0f4a 100644 --- a/app/policies/contest_policy.rb +++ b/app/policies/contest_policy.rb @@ -67,6 +67,10 @@ class ContestPolicy < ApplicationPolicy offline? end + def offline_completed? + offline? + end + def scoreboard? record.public end diff --git a/app/views/contests/offline_completed.html.slim b/app/views/contests/offline_completed.html.slim new file mode 100644 index 0000000..269bb6d --- /dev/null +++ b/app/views/contests/offline_completed.html.slim @@ -0,0 +1 @@ +| TODO \ No newline at end of file diff --git a/app/views/contests/offline_edit.html.slim b/app/views/contests/offline_edit.html.slim new file mode 100644 index 0000000..d49a7b3 --- /dev/null +++ b/app/views/contests/offline_edit.html.slim @@ -0,0 +1,26 @@ += form_with model: @offline, url: "/public/#{@contest.friendly_id}/offline/#{@offline.generate_token_for(:token)}" do |form| + = form.hidden_field :completed + .row.mb-3 + .col + .form-text.mb-1 + = t("offlines.end_image_select") + = form.file_field :end_image, accept: "image/*", class: "form-control" + .form-text.error-message style="display: none;" id="image-error-message" + = t("puzzles.form.file_too_big") + javascript: + function setMaxUploadSize() { + const el = document.querySelector('input[type="file"]'); + el.onchange = function() { + if(this.files[0].size > 2 * 1024 * 1024) { + document.getElementById('image-error-message').style.display = 'block'; + this.value = ""; + } else { + document.getElementById('image-error-message').style.display = 'none'; + } + }; + } + + setMaxUploadSize(); + .row.mt-4 + .col + = form.submit t("helpers.buttons.end"), class: "btn btn-primary" \ No newline at end of file diff --git a/app/views/contests/offline_new.html.slim b/app/views/contests/offline_new.html.slim index bcf422f..4240014 100644 --- a/app/views/contests/offline_new.html.slim +++ b/app/views/contests/offline_new.html.slim @@ -8,7 +8,7 @@ .col .form-text.mb-1 = t("offlines.start_image_select") - = form.file_field :start_image, accept: "image/*", class: "form-control" + = form.file_field :images, accept: "image/*", class: "form-control" .form-text.error-message style="display: none;" id="image-error-message" = t("puzzles.form.file_too_big") javascript: diff --git a/config/locales/en.yml b/config/locales/en.yml index aed4394..c7c302a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -116,6 +116,8 @@ en: not_a_csv_file: "it must be a CSV file" offline: attributes: + end_image: + blank: Please upload an image name: blank: Please enter a name for your participation start_image: @@ -218,6 +220,7 @@ en: create: "Create" delete: "Delete" edit: "Edit" + end: Click here to submit your completion export: Export import: CSV Import open: Open @@ -242,7 +245,8 @@ en: settings: Settings log_out: Log out offlines: - start_image_select: Enter your photo of the puzzle with the provided code written on a paper before starting it + end_image_select: Take a photo of your completed puzzle + start_image_select: Take a photo of the puzzle with the provided code written on a paper before starting it puzzles: destroy: notice: Puzzle deleted diff --git a/config/locales/fr.yml b/config/locales/fr.yml index d21a2b2..ae3b9da 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -87,6 +87,8 @@ fr: not_a_csv_file: "Le fichier doit être au format CSV" offline: attributes: + end_image: + blank: Tu dois inclure cette image pour pouvoir valider ton puzzle complété name: blank: Tu dois entrer un nom pour pouvoir participer start_image: @@ -189,6 +191,7 @@ fr: create: "Créer" delete: "Supprimer" edit: "Modifier" + end: Clique ici pour valider ta complétion du puzzle export: Exporter import: Importer un CSV open: Détails @@ -213,6 +216,7 @@ fr: settings: Paramètres log_out: Déconnexion offlines: + end_image_select: Prends une photo du puzzle une fois complété start_image_select: Prends une photo du puzzle avant de le commencer, avec le code donné par l'organisateur.ice écrit sur du papier puzzles: destroy: diff --git a/config/routes.rb b/config/routes.rb index 54b74f0..e05898b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,6 +34,7 @@ Rails.application.routes.draw do get "public/:id", to: "contests#scoreboard" get "public/:id/offline", to: "contests#offline_new" post "public/:id/offline", to: "contests#offline_create" - get "public/:id/offline/:offline_id", to: "contests#offline_edit" - post "public/:id/offline/:offline_id", to: "contests#offline_update" + get "public/:id/offline/:token", to: "contests#offline_edit" + patch "public/:id/offline/:token", to: "contests#offline_update" + get "public/:id/offline/:token/completed", to: "contests#offline_completed" end diff --git a/db/migrate/20251031095604_add_completed_to_offline.rb b/db/migrate/20251031095604_add_completed_to_offline.rb new file mode 100644 index 0000000..69ec6c4 --- /dev/null +++ b/db/migrate/20251031095604_add_completed_to_offline.rb @@ -0,0 +1,5 @@ +class AddCompletedToOffline < ActiveRecord::Migration[8.0] + def change + add_column :offlines, :completed, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 40d6dbb..1479ca8 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_10_30_094151) do +ActiveRecord::Schema[8.0].define(version: 2025_10_31_095604) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -133,6 +133,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_30_094151) do t.datetime "start_time", null: false t.datetime "end_time" t.string "name", null: false + t.boolean "completed" t.index ["contest_id"], name: "index_offlines_on_contest_id" end diff --git a/spec/factories/offlines.rb b/spec/factories/offlines.rb index baabcfb..5cf6d4a 100644 --- a/spec/factories/offlines.rb +++ b/spec/factories/offlines.rb @@ -3,6 +3,7 @@ # Table name: offlines # # id :integer not null, primary key +# completed :boolean # end_time :datetime # name :string not null # start_time :datetime not null @@ -20,6 +21,6 @@ # FactoryBot.define do factory :offline do - + name { "test" } end end diff --git a/spec/models/offline_spec.rb b/spec/models/offline_spec.rb index 853bb59..89e1d42 100644 --- a/spec/models/offline_spec.rb +++ b/spec/models/offline_spec.rb @@ -3,6 +3,7 @@ # Table name: offlines # # id :integer not null, primary key +# completed :boolean # end_time :datetime # name :string not null # start_time :datetime not null