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) | ||||
|     policy_name = exception.policy.class.to_s.underscore | ||||
|  | ||||
|     if current_user | ||||
|       flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default | ||||
|       redirect_back_or_to(root_path) | ||||
|     else | ||||
|       not_found | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def not_found | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| class ContestsController < ApplicationController | ||||
|   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 | ||||
|     authorize :contest | ||||
| @@ -100,22 +101,30 @@ class ContestsController < ApplicationController | ||||
|   end | ||||
|  | ||||
|   def offline_new | ||||
|     @contest = Contest.find_by(slug: params[:id]) | ||||
|     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) | ||||
|  | ||||
|     authorize @contest | ||||
|     @offline = Offline.new | ||||
|   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 | ||||
|  | ||||
|   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 | ||||
|     @badges = [] | ||||
|     @badges.push(t("helpers.badges.team")) if @contest.team | ||||
| @@ -139,4 +148,8 @@ class ContestsController < ApplicationController | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def offline_start_params | ||||
|     params.expect(offline: [ :name, :start_image ]) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| # | ||||
| #  id         :integer          not null, primary key | ||||
| #  end_time   :datetime | ||||
| #  name       :string           not null | ||||
| #  start_time :datetime         not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| @@ -23,5 +24,17 @@ class Offline < ApplicationRecord | ||||
|   has_one_attached :start_image | ||||
|   has_one_attached :end_image | ||||
|  | ||||
|   validates :name, 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 | ||||
|   | ||||
| @@ -47,24 +47,28 @@ class ContestPolicy < ApplicationPolicy | ||||
|     record.user.id == user.id || user.admin? | ||||
|   end | ||||
|  | ||||
|   def offline? | ||||
|     record.offline_form | ||||
|   end | ||||
|  | ||||
|   def offline_new? | ||||
|     true | ||||
|     offline? | ||||
|   end | ||||
|  | ||||
|   def offline_create? | ||||
|     true | ||||
|     offline? | ||||
|   end | ||||
|  | ||||
|   def offline_edit? | ||||
|     true | ||||
|     offline? | ||||
|   end | ||||
|  | ||||
|   def offline_update? | ||||
|     true | ||||
|     offline? | ||||
|   end | ||||
|  | ||||
|   def scoreboard? | ||||
|     true | ||||
|     record.public | ||||
|   end | ||||
|  | ||||
|   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 | ||||
|     .col | ||||
|       .form-text.mb-1 | ||||
|         = t("puzzles.image_select") | ||||
|         = t("offlines.start_image_select") | ||||
|       = form.file_field :start_image, accept: "image/*", class: "form-control" | ||||
|       .form-text.error-message style="display: none;" id="image-error-message" | ||||
|         = t("puzzles.form.file_too_big") | ||||
| @@ -20,6 +25,6 @@ | ||||
|         } | ||||
|  | ||||
|         setMaxUploadSize(); | ||||
|   .row.mt-3 | ||||
|   .row.mt-4 | ||||
|     .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| | ||||
|   if html_tag.include? "<label" | ||||
|   if (html_tag.include? "<label") || (html_tag.include? "<input accept=\"image") | ||||
|     appended_html = "" | ||||
|     if instance.error_message.is_a?(Array) | ||||
|       appended_html = "<div class='error-message form-text'>#{instance.error_message.map(&:capitalize).uniq.join(", ")}</div>" | ||||
|   | ||||
| @@ -74,6 +74,8 @@ en: | ||||
|         processed: Processed? | ||||
|         text: Content | ||||
|         time: Time | ||||
|       offline: | ||||
|         name: Your name | ||||
|       puzzle: | ||||
|         brand: Brand | ||||
|         image: Image | ||||
| @@ -112,6 +114,12 @@ en: | ||||
|               blank: "No file selected" | ||||
|               empty: "This file is empty" | ||||
|               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: | ||||
|           attributes: | ||||
|             name: | ||||
| @@ -216,6 +224,7 @@ en: | ||||
|       refresh: Refresh | ||||
|       sign_in: Sign in | ||||
|       save: Save | ||||
|       start: Click here to start your participation | ||||
|     field: Field | ||||
|     none: No field selected | ||||
|     rank: Rank | ||||
| @@ -232,6 +241,8 @@ en: | ||||
|     home: My contests | ||||
|     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 | ||||
|   puzzles: | ||||
|     destroy: | ||||
|       notice: Puzzle deleted | ||||
|   | ||||
| @@ -45,6 +45,8 @@ fr: | ||||
|         processed: Traité ? | ||||
|         text: Contenu | ||||
|         time: Temps | ||||
|       offline: | ||||
|         name: Ton nom ou pseudo | ||||
|       puzzle: | ||||
|         brand: Marque | ||||
|         image: Image | ||||
| @@ -83,6 +85,12 @@ fr: | ||||
|               blank: "Aucun fichier sélectionné" | ||||
|               empty: "Ce fichier est vide" | ||||
|               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: | ||||
|           attributes: | ||||
|             name: | ||||
| @@ -187,6 +195,7 @@ fr: | ||||
|       refresh: Rafraîchir | ||||
|       sign_in: Se connecter | ||||
|       save: Modifier | ||||
|       start: Clique ici pour démarrer ta participation | ||||
|     field: Champ | ||||
|     none: Aucun champ sélectionné | ||||
|     rank: Rang | ||||
| @@ -203,6 +212,8 @@ fr: | ||||
|     home: Mes concours | ||||
|     settings: Paramètres | ||||
|     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: | ||||
|     destroy: | ||||
|       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. | ||||
|  | ||||
| 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| | ||||
|     t.string "name", 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.datetime "start_time", null: false | ||||
|     t.datetime "end_time" | ||||
|     t.string "name", null: false | ||||
|     t.index ["contest_id"], name: "index_offlines_on_contest_id" | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| # | ||||
| #  id         :integer          not null, primary key | ||||
| #  end_time   :datetime | ||||
| #  name       :string           not null | ||||
| #  start_time :datetime         not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| # | ||||
| #  id         :integer          not null, primary key | ||||
| #  end_time   :datetime | ||||
| #  name       :string           not null | ||||
| #  start_time :datetime         not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
|   | ||||
		Reference in New Issue
	
	Block a user