Implement offline POST method for starting an offline participation
This commit is contained in:
		| @@ -30,8 +30,12 @@ class ApplicationController < ActionController::Base | |||||||
|   def user_not_authorized(exception) |   def user_not_authorized(exception) | ||||||
|     policy_name = exception.policy.class.to_s.underscore |     policy_name = exception.policy.class.to_s.underscore | ||||||
|  |  | ||||||
|     flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default |     if current_user | ||||||
|     redirect_back_or_to(root_path) |       flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default | ||||||
|  |       redirect_back_or_to(root_path) | ||||||
|  |     else | ||||||
|  |       not_found | ||||||
|  |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def not_found |   def not_found | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| class ContestsController < ApplicationController | class ContestsController < ApplicationController | ||||||
|   before_action :set_contest, only: %i[ destroy edit show update ] |   before_action :set_contest, only: %i[ destroy edit show update ] | ||||||
|   skip_before_action :require_authentication, only: %i[ scoreboard offline_new ] |   before_action :offline_setup, only: %i[ offline_new offline_create ] | ||||||
|  |   skip_before_action :require_authentication, only: %i[ scoreboard offline_new offline_create ] | ||||||
|  |  | ||||||
|   def index |   def index | ||||||
|     authorize :contest |     authorize :contest | ||||||
| @@ -100,22 +101,30 @@ class ContestsController < ApplicationController | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def offline_new |   def offline_new | ||||||
|     @contest = Contest.find_by(slug: params[:id]) |     authorize @contest | ||||||
|     unless @contest && @contest.offline_form |  | ||||||
|       skip_authorization |  | ||||||
|       not_found and return |  | ||||||
|     end |  | ||||||
|     authorize :contest |  | ||||||
|  |  | ||||||
|     I18n.locale = @contest.lang |  | ||||||
|  |  | ||||||
|     @title = I18n.t("contests.scoreboard.title", name: @contest.name) |  | ||||||
|  |  | ||||||
|     @offline = Offline.new |     @offline = Offline.new | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def offline_create | ||||||
|  |     authorize @contest | ||||||
|  |     @offline = Offline.new(offline_start_params) | ||||||
|  |     @offline.contest = @contest | ||||||
|  |     @offline.start_time = Time.now() | ||||||
|  |     if @offline.save | ||||||
|  |       redirect_to "/public/#{@contest.friendly_id}/offline/test" | ||||||
|  |     else | ||||||
|  |       render :offline_new, status: :unprocessable_entity | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|   private |   private | ||||||
|  |  | ||||||
|  |   def offline_setup | ||||||
|  |     @contest = Contest.find_by(slug: params[:id]) | ||||||
|  |     I18n.locale = @contest.lang | ||||||
|  |     @title = I18n.t("contests.scoreboard.title", name: @contest.name) | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def set_badges |   def set_badges | ||||||
|     @badges = [] |     @badges = [] | ||||||
|     @badges.push(t("helpers.badges.team")) if @contest.team |     @badges.push(t("helpers.badges.team")) if @contest.team | ||||||
| @@ -139,4 +148,8 @@ class ContestsController < ApplicationController | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def offline_start_params | ||||||
|  |     params.expect(offline: [ :name, :start_image ]) | ||||||
|  |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| # | # | ||||||
| #  id         :integer          not null, primary key | #  id         :integer          not null, primary key | ||||||
| #  end_time   :datetime | #  end_time   :datetime | ||||||
|  | #  name       :string           not null | ||||||
| #  start_time :datetime         not null | #  start_time :datetime         not null | ||||||
| #  created_at :datetime         not null | #  created_at :datetime         not null | ||||||
| #  updated_at :datetime         not null | #  updated_at :datetime         not null | ||||||
| @@ -23,5 +24,17 @@ class Offline < ApplicationRecord | |||||||
|   has_one_attached :start_image |   has_one_attached :start_image | ||||||
|   has_one_attached :end_image |   has_one_attached :end_image | ||||||
|  |  | ||||||
|  |   validates :name, presence: true | ||||||
|   validates :start_time, presence: true |   validates :start_time, presence: true | ||||||
|  |  | ||||||
|  |   validate :start_image_is_present | ||||||
|  |  | ||||||
|  |   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")) | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -47,24 +47,28 @@ class ContestPolicy < ApplicationPolicy | |||||||
|     record.user.id == user.id || user.admin? |     record.user.id == user.id || user.admin? | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def offline? | ||||||
|  |     record.offline_form | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def offline_new? |   def offline_new? | ||||||
|     true |     offline? | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def offline_create? |   def offline_create? | ||||||
|     true |     offline? | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def offline_edit? |   def offline_edit? | ||||||
|     true |     offline? | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def offline_update? |   def offline_update? | ||||||
|     true |     offline? | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def scoreboard? |   def scoreboard? | ||||||
|     true |     record.public | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def upload_csv? |   def upload_csv? | ||||||
|   | |||||||
| @@ -1,8 +1,13 @@ | |||||||
| = form_with model: @offline, url: "/public/#{@contest.id}/offline" do |form| | = form_with model: @offline, url: "/public/#{@contest.friendly_id}/offline" do |form| | ||||||
|  |   .row.mb-3 | ||||||
|  |     .col | ||||||
|  |       .form-floating | ||||||
|  |         = form.text_field :name, autocomplete: "off", class: "form-control" | ||||||
|  |         = form.label :name, class: "required" | ||||||
|   .row.mb-3 |   .row.mb-3 | ||||||
|     .col |     .col | ||||||
|       .form-text.mb-1 |       .form-text.mb-1 | ||||||
|         = t("puzzles.image_select") |         = t("offlines.start_image_select") | ||||||
|       = form.file_field :start_image, accept: "image/*", class: "form-control" |       = form.file_field :start_image, accept: "image/*", class: "form-control" | ||||||
|       .form-text.error-message style="display: none;" id="image-error-message" |       .form-text.error-message style="display: none;" id="image-error-message" | ||||||
|         = t("puzzles.form.file_too_big") |         = t("puzzles.form.file_too_big") | ||||||
| @@ -20,6 +25,6 @@ | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         setMaxUploadSize(); |         setMaxUploadSize(); | ||||||
|   .row.mt-3 |   .row.mt-4 | ||||||
|     .col |     .col | ||||||
|       = form.submit t("helpers.buttons.add"), class: "btn btn-primary" |       = form.submit t("helpers.buttons.start"), class: "btn btn-primary" | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| ActionView::Base.field_error_proc = proc do |html_tag, instance| | ActionView::Base.field_error_proc = proc do |html_tag, instance| | ||||||
|   if html_tag.include? "<label" |   if (html_tag.include? "<label") || (html_tag.include? "<input accept=\"image") | ||||||
|     appended_html = "" |     appended_html = "" | ||||||
|     if instance.error_message.is_a?(Array) |     if instance.error_message.is_a?(Array) | ||||||
|       appended_html = "<div class='error-message form-text'>#{instance.error_message.map(&:capitalize).uniq.join(", ")}</div>" |       appended_html = "<div class='error-message form-text'>#{instance.error_message.map(&:capitalize).uniq.join(", ")}</div>" | ||||||
|   | |||||||
| @@ -74,6 +74,8 @@ en: | |||||||
|         processed: Processed? |         processed: Processed? | ||||||
|         text: Content |         text: Content | ||||||
|         time: Time |         time: Time | ||||||
|  |       offline: | ||||||
|  |         name: Your name | ||||||
|       puzzle: |       puzzle: | ||||||
|         brand: Brand |         brand: Brand | ||||||
|         image: Image |         image: Image | ||||||
| @@ -112,6 +114,12 @@ en: | |||||||
|               blank: "No file selected" |               blank: "No file selected" | ||||||
|               empty: "This file is empty" |               empty: "This file is empty" | ||||||
|               not_a_csv_file: "it must be a CSV file" |               not_a_csv_file: "it must be a CSV file" | ||||||
|  |         offline: | ||||||
|  |           attributes: | ||||||
|  |             name: | ||||||
|  |               blank: Please enter a name for your participation | ||||||
|  |             start_image: | ||||||
|  |               blank: Please upload an image | ||||||
|         puzzle: |         puzzle: | ||||||
|           attributes: |           attributes: | ||||||
|             name: |             name: | ||||||
| @@ -216,6 +224,7 @@ en: | |||||||
|       refresh: Refresh |       refresh: Refresh | ||||||
|       sign_in: Sign in |       sign_in: Sign in | ||||||
|       save: Save |       save: Save | ||||||
|  |       start: Click here to start your participation | ||||||
|     field: Field |     field: Field | ||||||
|     none: No field selected |     none: No field selected | ||||||
|     rank: Rank |     rank: Rank | ||||||
| @@ -232,6 +241,8 @@ en: | |||||||
|     home: My contests |     home: My contests | ||||||
|     settings: Settings |     settings: Settings | ||||||
|     log_out: Log out |     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 | ||||||
|   puzzles: |   puzzles: | ||||||
|     destroy: |     destroy: | ||||||
|       notice: Puzzle deleted |       notice: Puzzle deleted | ||||||
|   | |||||||
| @@ -45,6 +45,8 @@ fr: | |||||||
|         processed: Traité ? |         processed: Traité ? | ||||||
|         text: Contenu |         text: Contenu | ||||||
|         time: Temps |         time: Temps | ||||||
|  |       offline: | ||||||
|  |         name: Ton nom ou pseudo | ||||||
|       puzzle: |       puzzle: | ||||||
|         brand: Marque |         brand: Marque | ||||||
|         image: Image |         image: Image | ||||||
| @@ -83,6 +85,12 @@ fr: | |||||||
|               blank: "Aucun fichier sélectionné" |               blank: "Aucun fichier sélectionné" | ||||||
|               empty: "Ce fichier est vide" |               empty: "Ce fichier est vide" | ||||||
|               not_a_csv_file: "Le fichier doit être au format CSV" |               not_a_csv_file: "Le fichier doit être au format CSV" | ||||||
|  |         offline: | ||||||
|  |           attributes: | ||||||
|  |             name: | ||||||
|  |               blank: Tu dois entrer un nom pour pouvoir participer | ||||||
|  |             start_image: | ||||||
|  |               blank: Tu dois inclure cette image pour pouvoir participer | ||||||
|         puzzle: |         puzzle: | ||||||
|           attributes: |           attributes: | ||||||
|             name: |             name: | ||||||
| @@ -187,6 +195,7 @@ fr: | |||||||
|       refresh: Rafraîchir |       refresh: Rafraîchir | ||||||
|       sign_in: Se connecter |       sign_in: Se connecter | ||||||
|       save: Modifier |       save: Modifier | ||||||
|  |       start: Clique ici pour démarrer ta participation | ||||||
|     field: Champ |     field: Champ | ||||||
|     none: Aucun champ sélectionné |     none: Aucun champ sélectionné | ||||||
|     rank: Rang |     rank: Rang | ||||||
| @@ -203,6 +212,8 @@ fr: | |||||||
|     home: Mes concours |     home: Mes concours | ||||||
|     settings: Paramètres |     settings: Paramètres | ||||||
|     log_out: Déconnexion |     log_out: Déconnexion | ||||||
|  |   offlines: | ||||||
|  |     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: |   puzzles: | ||||||
|     destroy: |     destroy: | ||||||
|       notice: Puzzle supprimé |       notice: Puzzle supprimé | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								db/migrate/20251030094151_add_name_to_offline.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20251030094151_add_name_to_offline.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | class AddNameToOffline < ActiveRecord::Migration[8.0] | ||||||
|  |   def change | ||||||
|  |     add_column :offlines, :name, :string, null: false | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										3
									
								
								db/schema.rb
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								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_10_30_092221) do | ActiveRecord::Schema[8.0].define(version: 2025_10_30_094151) 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 | ||||||
| @@ -132,6 +132,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_30_092221) do | |||||||
|     t.integer "contest_id", null: false |     t.integer "contest_id", null: false | ||||||
|     t.datetime "start_time", null: false |     t.datetime "start_time", null: false | ||||||
|     t.datetime "end_time" |     t.datetime "end_time" | ||||||
|  |     t.string "name", null: false | ||||||
|     t.index ["contest_id"], name: "index_offlines_on_contest_id" |     t.index ["contest_id"], name: "index_offlines_on_contest_id" | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| # | # | ||||||
| #  id         :integer          not null, primary key | #  id         :integer          not null, primary key | ||||||
| #  end_time   :datetime | #  end_time   :datetime | ||||||
|  | #  name       :string           not null | ||||||
| #  start_time :datetime         not null | #  start_time :datetime         not null | ||||||
| #  created_at :datetime         not null | #  created_at :datetime         not null | ||||||
| #  updated_at :datetime         not null | #  updated_at :datetime         not null | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| # | # | ||||||
| #  id         :integer          not null, primary key | #  id         :integer          not null, primary key | ||||||
| #  end_time   :datetime | #  end_time   :datetime | ||||||
|  | #  name       :string           not null | ||||||
| #  start_time :datetime         not null | #  start_time :datetime         not null | ||||||
| #  created_at :datetime         not null | #  created_at :datetime         not null | ||||||
| #  updated_at :datetime         not null | #  updated_at :datetime         not null | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user