diff --git a/app/controllers/concerns/contests_concern.rb b/app/controllers/concerns/contests_concern.rb new file mode 100644 index 0000000..2634134 --- /dev/null +++ b/app/controllers/concerns/contests_concern.rb @@ -0,0 +1,7 @@ +module ContestsConcern + extend ActiveSupport::Concern + + def active_page(path) + request.path.starts_with?(path) ? "active" : "" + end +end diff --git a/app/controllers/contests_controller.rb b/app/controllers/contests_controller.rb index 5ac25e6..ccc57ff 100644 --- a/app/controllers/contests_controller.rb +++ b/app/controllers/contests_controller.rb @@ -1,5 +1,6 @@ class ContestsController < ApplicationController include CompletionsConcern + include ContestsConcern include ContestantsConcern before_action :set_contest, only: %i[ destroy show ] @@ -168,10 +169,30 @@ class ContestsController < ApplicationController end def scoreboard - @contest = Contest.find_by(slug: params[:id]) - unless @contest && @contest.public - skip_authorization - not_found and return + if active_page("/public/event") + @event = Event.find_by(slug: params[:id]) + unless @event && @event.shared_scoreboard && !@event.contests.where(public: true).empty? + skip_authorization + not_found and return + end + if params.key?(:contest_id) + begin + @contest = Contest.find(params[:contest_id]) + if @contest.event != @event + @contest = @event.contests.where(public: true).first + end + rescue + @contest = @event.contests.where(public: true).first + end + else + @contest = @event.contests.where(public: true).first + end + else + @contest = Contest.find_by(slug: params[:id]) + unless @contest && @contest.public + skip_authorization + not_found and return + end end authorize @contest diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 613e6fe..530be03 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -35,7 +35,7 @@ class EventsController < ApplicationController def show authorize @event - @contests = @event.contests + @contests = @event.contests.includes([ :contestants, :puzzles ]) @title = @event.name end @@ -65,11 +65,13 @@ class EventsController < ApplicationController redirect_to events_path, notice: t("events.destroy.notice") end + private + def set_event @event = Event.find(params[:id]) end def event_params - params.expect(event: [ :name, :shared_scoreboard ]) + params.expect(event: [ :lang, :name, :shared_scoreboard ]) end end diff --git a/app/models/event.rb b/app/models/event.rb index d162ece..020cab6 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -6,6 +6,7 @@ # lang :string default("en") # name :string # shared_scoreboard :boolean +# slug :string # created_at :datetime not null # updated_at :datetime not null # user_id :integer not null @@ -19,8 +20,13 @@ # user_id (user_id => users.id) # class Event < ApplicationRecord + extend FriendlyId + belongs_to :user has_many :contests, dependent: :destroy + friendly_id :name, use: :slugged + validates :name, presence: true + validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } } end diff --git a/app/views/events/_form.html.slim b/app/views/events/_form.html.slim index fae5cdf..a7c9e70 100644 --- a/app/views/events/_form.html.slim +++ b/app/views/events/_form.html.slim @@ -15,6 +15,13 @@ .alert.alert-primary role="alert" = t("events.form.shared_scoreboard_message") + if method == :patch + .row.mb-3 + .col + .form-floating + = form.select :lang, Languages::AVAILABLE_LANGUAGES.map { |lang| [ lang[:name], lang[:id] ] }, {}, class: "form-select" + = form.label :lang + .row.mt-4 .col - if method == :patch diff --git a/app/views/events/show.html.slim b/app/views/events/show.html.slim index 16d1c47..902ca00 100644 --- a/app/views/events/show.html.slim +++ b/app/views/events/show.html.slim @@ -8,7 +8,14 @@ a.mt-3.btn.btn-primary.mb-4 href=new_contest_path(event: @event.id) = t("events.show.new_contest") -.row.row-cols-1.row-cols-md-3.g-4 +- if @event.shared_scoreboard + a.btn.btn-success href="/public/event/#{@event.slug}" style="margin-top: -6px;" + = t("events.show.open_shared_scoreboard") +- else + a.btn.btn-success.disabled style="margin-top: -6px;" + = t("events.show.shared_scoreboard_disabled") + +.mt-3.row.row-cols-1.row-cols-md-3.g-4 - @contests.each do |contest| .col css: diff --git a/config/locales/en.yml b/config/locales/en.yml index 71865d2..7ef2c2c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -83,6 +83,7 @@ en: file: File separator: Separator event: + lang: Language for th shared scoreboard name: Name shared_scoreboard: Shared scoreboard shared_scoreboard_description: If activated, there will be a scoreboard link specific to the event, with tabs for each contest @@ -310,6 +311,8 @@ en: manage_contests: Contests in this event modify: Settings new_contest: Add a contest to this event + shared_scoreboard_disabled: The shared scoreboard is disabled + open_shared_scoreboard: Open shared scoreboard helpers: badges: registration: "registration" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 7621b8b..c84dc31 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -54,6 +54,7 @@ fr: file: Fichier separator: Délimiteur event: + lang: Langue pour le classement partagé name: Nom shared_scoreboard: Classement partagé shared_scoreboard_description: Si activé, un lien de classement partagé sera disponible pour l'événement, avec des onglets pour chacune des épreuves. @@ -281,6 +282,8 @@ fr: manage_contests: Épreuves de cet événement modify: Paramètres new_contest: Ajouter une épreuve à cet événement + shared_scoreboard_disabled: Le classement partagé n'est pas activé + open_shared_scoreboard: Ouvrir le classement partagé helpers: badges: registration: "auto-inscription" diff --git a/config/routes.rb b/config/routes.rb index d2689da..2b2b94d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,6 +55,7 @@ Rails.application.routes.draw do post "update_contestants", to: "users#update_contestants" get "public/:id", to: "contests#scoreboard" + get "public/event/:id", to: "contests#scoreboard" get "public/:id/offline", to: "contests#offline_new" post "public/:id/offline", to: "contests#offline_create" get "public/:id/offline/:token", to: "contests#offline_edit" diff --git a/db/migrate/20260730055752_add_slug_to_event.rb b/db/migrate/20260730055752_add_slug_to_event.rb new file mode 100644 index 0000000..75936d8 --- /dev/null +++ b/db/migrate/20260730055752_add_slug_to_event.rb @@ -0,0 +1,5 @@ +class AddSlugToEvent < ActiveRecord::Migration[8.0] + def change + add_column :events, :slug, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index cf58fff..432f5c0 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: 2026_07_26_114604) do +ActiveRecord::Schema[8.0].define(version: 2026_07_30_055752) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -126,6 +126,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_26_114604) do t.integer "user_id", null: false t.boolean "shared_scoreboard" t.string "lang", default: "en" + t.string "slug" t.index ["user_id"], name: "index_events_on_user_id" end diff --git a/spec/factories/events.rb b/spec/factories/events.rb index 215c10a..aac5eee 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -6,6 +6,7 @@ # lang :string default("en") # name :string # shared_scoreboard :boolean +# slug :string # created_at :datetime not null # updated_at :datetime not null # user_id :integer not null diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 95ef4b5..d25ebaf 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -6,6 +6,7 @@ # lang :string default("en") # name :string # shared_scoreboard :boolean +# slug :string # created_at :datetime not null # updated_at :datetime not null # user_id :integer not null