From 709719b80147460e1304651247480a6039a52ee6 Mon Sep 17 00:00:00 2001 From: sto Date: Thu, 20 Nov 2025 12:01:30 +0100 Subject: [PATCH] QR codes generation --- Gemfile | 1 + Gemfile.lock | 6 ++++++ app/controllers/contestants_controller.rb | 10 ++++++++-- app/models/contestant.rb | 15 +++++++++++++++ app/models/offline.rb | 2 +- app/models/puzzle.rb | 2 +- app/policies/contest_policy.rb | 4 ++++ app/views/contestants/generate_qrcodes.html.slim | 11 +++++++++++ app/views/contestants/index.html.slim | 2 ++ config/environments/development.rb | 2 ++ config/environments/production.rb | 2 ++ config/locales/en.yml | 1 + config/locales/fr.yml | 1 + config/routes.rb | 1 + .../20251120100813_add_qrcode_to_contestant.rb | 5 +++++ db/schema.rb | 3 ++- 16 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 app/views/contestants/generate_qrcodes.html.slim create mode 100644 db/migrate/20251120100813_add_qrcode_to_contestant.rb diff --git a/Gemfile b/Gemfile index 4eba183..e909d95 100644 --- a/Gemfile +++ b/Gemfile @@ -46,6 +46,7 @@ gem "bootstrap", "~> 5.3.3" gem "friendly_id", "~> 5.5.0" gem "csv" gem "damerau-levenshtein" +gem "rqrcode", "~> 3.0" group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem diff --git a/Gemfile.lock b/Gemfile.lock index c883a6b..11183af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,7 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + chunky_png (1.4.0) concurrent-ruby (1.3.5) connection_pool (2.5.4) crass (1.0.6) @@ -291,6 +292,10 @@ GEM reline (0.6.2) io-console (~> 0.5) rexml (3.4.4) + rqrcode (3.1.0) + chunky_png (~> 1.0) + rqrcode_core (~> 2.0) + rqrcode_core (2.0.0) rspec-core (3.13.6) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) @@ -455,6 +460,7 @@ DEPENDENCIES puma (>= 5.0) pundit (~> 2.5) rails (~> 8.0.2) + rqrcode (~> 3.0) rspec-rails rubocop-rails-omakase selenium-webdriver diff --git a/app/controllers/contestants_controller.rb b/app/controllers/contestants_controller.rb index 6038743..4d2cb8d 100644 --- a/app/controllers/contestants_controller.rb +++ b/app/controllers/contestants_controller.rb @@ -2,7 +2,7 @@ class ContestantsController < ApplicationController include CompletionsConcern include ContestantsConcern - before_action :set_contest, only: %i[ index edit new create update destroy import upload_csv convert_csv finalize_import export ] + before_action :set_contest, only: %i[ index edit new create update destroy import upload_csv convert_csv finalize_import export generate_qrcodes ] before_action :set_contestant, only: %i[ destroy edit update] before_action :set_completions, only: %i[edit update ] skip_before_action :require_authentication, only: %i[ get_public_completion post_public_completion public_completion_updated ] @@ -42,7 +42,7 @@ class ContestantsController < ApplicationController if @contestant.update(contestant_params) update_contestant_categories - redirect_to @contest, notice: t("contestants.edit.notice") + redirect_to contest_contestants_path(@contest), notice: t("contestants.edit.notice") else render :edit, status: :unprocessable_entity end @@ -120,6 +120,12 @@ class ContestantsController < ApplicationController end end + def generate_qrcodes + authorize @contest + + @contestants = @contest.contestants.sort_by { |contestant| contestant.name } + end + def get_public_completion skip_authorization diff --git a/app/models/contestant.rb b/app/models/contestant.rb index adf9889..0a895ec 100644 --- a/app/models/contestant.rb +++ b/app/models/contestant.rb @@ -7,6 +7,7 @@ # email :string # name :string # projected_time :string +# qrcode :string # time_seconds :integer # created_at :datetime not null # updated_at :datetime not null @@ -27,6 +28,7 @@ class Contestant < ApplicationRecord has_and_belongs_to_many :categories before_validation :initialize_time_seconds_if_empty + before_commit :generate_qrcode, on: :create validates :name, presence: true validates :time_seconds, presence: true @@ -48,4 +50,17 @@ class Contestant < ApplicationRecord self.time_seconds = 0 end end + + def generate_qrcode + host = Rails.application.config.action_controller.default_url_options[:host] + qrcode = RQRCode::QRCode.new("https://#{host}/public/p/#{self.generate_token_for(:token)}") + self.qrcode = qrcode.as_svg( + color: "000", + shape_rendering: "crispEdges", + module_size: 3, + standalone: true, + use_path: true, + viewbox: true + ) + end end diff --git a/app/models/offline.rb b/app/models/offline.rb index 2833989..a69d4da 100644 --- a/app/models/offline.rb +++ b/app/models/offline.rb @@ -33,7 +33,7 @@ class Offline < ApplicationRecord belongs_to :contestant, optional: true belongs_to :completion, optional: true - has_many_attached :images + has_many_attached :images, dependent: :destroy generates_token_for :token diff --git a/app/models/puzzle.rb b/app/models/puzzle.rb index 5734d14..6a84289 100644 --- a/app/models/puzzle.rb +++ b/app/models/puzzle.rb @@ -22,7 +22,7 @@ class Puzzle < ApplicationRecord belongs_to :contest has_many :completions, dependent: :destroy - has_one_attached :image + has_one_attached :image, dependent: :destroy validates :name, presence: true validates :pieces, presence: true diff --git a/app/policies/contest_policy.rb b/app/policies/contest_policy.rb index 76761b4..dd20025 100644 --- a/app/policies/contest_policy.rb +++ b/app/policies/contest_policy.rb @@ -35,6 +35,10 @@ class ContestPolicy < ApplicationPolicy owner_or_admin end + def generate_qrcodes? + owner_or_admin + end + def settings_general_edit? edit? end diff --git a/app/views/contestants/generate_qrcodes.html.slim b/app/views/contestants/generate_qrcodes.html.slim new file mode 100644 index 0000000..e65cfb9 --- /dev/null +++ b/app/views/contestants/generate_qrcodes.html.slim @@ -0,0 +1,11 @@ +.row.mb-4 style="height: calc(100vh - 280px)" + .col.d-flex.flex-column style="height: 100%" + .d-flex.flex-column style="overflow-y: auto" + - for row in 0..((@contestants.length - 1) / 4) + .mt-4.d-flex.flex-row + - for col in 0..3 + - if row * 4 + col < @contestants.length + .d-flex.flex-column.ms-5 style="align-items: center" + = @contestants[row * 4 + col].name + .mt-1 style="width: 200px; height: 200px;" + = @contestants[row * 4 + col].qrcode.html_safe \ No newline at end of file diff --git a/app/views/contestants/index.html.slim b/app/views/contestants/index.html.slim index bfb5dab..c5d194a 100644 --- a/app/views/contestants/index.html.slim +++ b/app/views/contestants/index.html.slim @@ -6,6 +6,8 @@ | + #{t("helpers.buttons.add")} a.ms-2.btn.btn.btn-primary href=contest_import_path(@contest) style="margin-top: -3px" | #{t("helpers.buttons.import")} + a.ms-2.btn.btn.btn-primary href=contest_generate_qrcodes_path(@contest) style="margin-top: -3px" + | #{t("helpers.buttons.generate_qrcodes")} a.ms-2.btn.btn.btn-primary href="/contests/#{@contest.id}/export.csv" style="margin-top: -3px" | #{t("helpers.buttons.export")} diff --git a/config/environments/development.rb b/config/environments/development.rb index 4cc21c4..ceb5dbb 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -40,6 +40,8 @@ Rails.application.configure do # Set localhost to be used by links generated in mailer templates. config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + config.action_controller.default_url_options = { host: "localhost" } + # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log diff --git a/config/environments/production.rb b/config/environments/production.rb index bdcd01d..7f891ca 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -60,6 +60,8 @@ Rails.application.configure do # Set host to be used by links generated in mailer templates. config.action_mailer.default_url_options = { host: "example.com" } + config.action_controller.default_url_options = { host: "public-scoreboard.org" } + # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. # config.action_mailer.smtp_settings = { # user_name: Rails.application.credentials.dig(:smtp, :user_name), diff --git a/config/locales/en.yml b/config/locales/en.yml index 312f1ea..815058b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -258,6 +258,7 @@ en: edit: "Edit" end: Click here to submit your completion export: Export + generate_qrcodes: Generate QR codes import: CSV Import open: Open refresh: Refresh diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2588d83..f85c28b 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -229,6 +229,7 @@ fr: edit: "Modifier" end: Clique ici pour valider ta complétion du puzzle export: Exporter + generate_qrcodes: Générer des QR codes import: Importer un CSV open: Détails refresh: Rafraîchir diff --git a/config/routes.rb b/config/routes.rb index 64d1023..74fdf94 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,6 +26,7 @@ Rails.application.routes.draw do get "import/:id", to: "contestants#convert_csv" post "import/:id", to: "contestants#finalize_import" get "export", to: "contestants#export" + get "generate_qrcodes", to: "contestants#generate_qrcodes" end resources :passwords, param: :token resource :session diff --git a/db/migrate/20251120100813_add_qrcode_to_contestant.rb b/db/migrate/20251120100813_add_qrcode_to_contestant.rb new file mode 100644 index 0000000..18d4651 --- /dev/null +++ b/db/migrate/20251120100813_add_qrcode_to_contestant.rb @@ -0,0 +1,5 @@ +class AddQrcodeToContestant < ActiveRecord::Migration[8.0] + def change + add_column :contestants, :qrcode, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 0005932..0bcabf5 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_18_074914) do +ActiveRecord::Schema[8.0].define(version: 2025_11_20_100813) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -83,6 +83,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_18_074914) do t.string "display_time" t.integer "time_seconds" t.string "projected_time" + t.string "qrcode" t.index ["contest_id"], name: "index_contestants_on_contest_id" end