From 9b10cc12d2c73e32c8f22fda1c0cb9e54ccd6f17 Mon Sep 17 00:00:00 2001 From: Malaber Date: Mon, 4 May 2026 14:43:20 +0200 Subject: [PATCH 1/2] feat(tournaments): add metadata fields Add typed tournament metadata to API payloads and follower sync. --- app/controllers/tournaments_controller.rb | 1 + app/models/tournament.rb | 18 +++++ .../simple_tournament_serializer.rb | 10 ++- app/services/tournament_snapshot_builder.rb | 2 + app/services/tournament_snapshot_importer.rb | 6 ++ app/services/tournament_sync_schema.rb | 6 ++ ...60504100000_add_metadata_to_tournaments.rb | 14 ++++ db/schema.rb | 8 +- .../tournaments_controller_spec.rb | 79 +++++++++++++++++++ spec/e2e/http/api_surface_spec.rb | 30 +++++++ spec/e2e/http/tournament_follow_sync_spec.rb | 30 +++++++ spec/e2e/http/tournament_sync_test_spec.rb | 14 +++- .../tournament_snapshot_importer_spec.rb | 18 +++++ 13 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20260504100000_add_metadata_to_tournaments.rb diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index f878c77..5d05a5f 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -285,6 +285,7 @@ class TournamentsController < ApplicationController params.slice( :name, :description, :public, :teams, :group_stage, :playoff_teams_amount, :instant_finalists_amount, :intermediate_round_participants_amount, + :location, :starts_at, :ends_at, :organizer_name, :contact_email, :website_url, :read_only_mode, :sync_target_url, :sync_auth_token ).permit! end diff --git a/app/models/tournament.rb b/app/models/tournament.rb index 4063d27..2b2f2d8 100644 --- a/app/models/tournament.rb +++ b/app/models/tournament.rb @@ -6,6 +6,7 @@ require 'openssl' class Tournament < ApplicationRecord TIMER_MODES = %w[countdown countup].freeze TIMER_REASONS = %w[tournament_start playoff_start match_end custom].freeze + METADATA_STRING_ATTRIBUTES = %i[location organizer_name contact_email website_url].freeze belongs_to :user belongs_to :owner, class_name: 'User', foreign_key: :user_id @@ -20,14 +21,18 @@ class Tournament < ApplicationRecord validates :timer_mode, inclusion: { in: TIMER_MODES }, allow_nil: true validates :timer_reason, inclusion: { in: TIMER_REASONS }, allow_nil: true validates :sync_target_url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true + validates :website_url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true + validates :contact_email, format: URI::MailTo::EMAIL_REGEXP, allow_blank: true validate :sync_configuration_is_complete validate :playoff_teams_amount_is_positive_power_of_two validate :timer_reason_text_matches_reason + validate :ends_at_is_after_starts_at after_initialize :generate_code after_create_commit :ensure_default_beamer! before_validation :normalize_timer_reason + before_validation :normalize_metadata before_validation :protect_follower_sync_token before_validation :clear_follower_sync_token_when_disabling_read_only_mode after_commit :broadcast_timer_state_change, if: :saved_change_to_timer_state? @@ -154,6 +159,19 @@ class Tournament < ApplicationRecord end end + def normalize_metadata + METADATA_STRING_ATTRIBUTES.each do |attribute| + self[attribute] = self[attribute]&.strip&.presence + end + end + + def ends_at_is_after_starts_at + return if starts_at.blank? || ends_at.blank? + return if ends_at > starts_at + + errors.add(:ends_at, 'must be after starts_at') + end + def saved_change_to_timer_state? saved_change_to_timestamp? || saved_change_to_timer_mode? || diff --git a/app/serializers/simple_tournament_serializer.rb b/app/serializers/simple_tournament_serializer.rb index 0d88639..6bb26e4 100644 --- a/app/serializers/simple_tournament_serializer.rb +++ b/app/serializers/simple_tournament_serializer.rb @@ -1,5 +1,13 @@ # frozen_string_literal: true class SimpleTournamentSerializer < ApplicationSerializer - attributes :name, :code, :public + attributes :name, :code, :public, :location, :organizer_name, :contact_email, :website_url + + attribute :starts_at do + object.starts_at&.iso8601 + end + + attribute :ends_at do + object.ends_at&.iso8601 + end end diff --git a/app/services/tournament_snapshot_builder.rb b/app/services/tournament_snapshot_builder.rb index 0975a78..148c76e 100644 --- a/app/services/tournament_snapshot_builder.rb +++ b/app/services/tournament_snapshot_builder.rb @@ -22,6 +22,8 @@ class TournamentSnapshotBuilder def build payload = tournament.attributes.symbolize_keys.slice(*ROOT_ATTRIBUTES) + payload[:starts_at] = tournament.starts_at&.iso8601 + payload[:ends_at] = tournament.ends_at&.iso8601 payload[:timestamp] = tournament.timestamp&.iso8601 payload[:timer_mode] = tournament.timer_mode payload[:timer_reason] = tournament.timer_reason diff --git a/app/services/tournament_snapshot_importer.rb b/app/services/tournament_snapshot_importer.rb index c2868da..0b443f3 100644 --- a/app/services/tournament_snapshot_importer.rb +++ b/app/services/tournament_snapshot_importer.rb @@ -6,6 +6,10 @@ class TournamentSnapshotImporter code description public + location + organizer_name + contact_email + website_url playoff_teams_amount instant_finalists_amount intermediate_round_participants_amount @@ -67,6 +71,8 @@ class TournamentSnapshotImporter def update_tournament! root_attrs = snapshot.slice(*ROOT_ATTRIBUTES) root_attrs[:code] = tournament.code if code_taken_by_another_tournament?(root_attrs[:code]) + root_attrs[:starts_at] = parse_time(snapshot[:starts_at]) + root_attrs[:ends_at] = parse_time(snapshot[:ends_at]) root_attrs[:timestamp] = parse_time(snapshot[:timestamp]) root_attrs[:timer_mode] = snapshot[:timer_mode] root_attrs[:timer_reason] = snapshot[:timer_reason] diff --git a/app/services/tournament_sync_schema.rb b/app/services/tournament_sync_schema.rb index ee0a9ac..7c9f436 100644 --- a/app/services/tournament_sync_schema.rb +++ b/app/services/tournament_sync_schema.rb @@ -9,6 +9,12 @@ class TournamentSyncSchema code description public + location + starts_at + ends_at + organizer_name + contact_email + website_url playoff_teams_amount instant_finalists_amount intermediate_round_participants_amount diff --git a/db/migrate/20260504100000_add_metadata_to_tournaments.rb b/db/migrate/20260504100000_add_metadata_to_tournaments.rb new file mode 100644 index 0000000..5d10c4e --- /dev/null +++ b/db/migrate/20260504100000_add_metadata_to_tournaments.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AddMetadataToTournaments < ActiveRecord::Migration[7.0] + def change + change_table :tournaments, bulk: true do |t| + t.string :location + t.datetime :starts_at + t.datetime :ends_at + t.string :organizer_name + t.string :contact_email + t.string :website_url + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 773e654..766e949 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.1].define(version: 2026_04_30_121000) do +ActiveRecord::Schema[8.1].define(version: 2026_05_04_100000) do create_table "beamers", force: :cascade do |t| t.json "config", default: {}, null: false t.datetime "created_at", null: false @@ -163,14 +163,19 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_30_121000) do create_table "tournaments", force: :cascade do |t| t.string "code", null: false + t.string "contact_email" t.datetime "created_at", precision: nil, null: false t.string "description" + t.datetime "ends_at" t.integer "instant_finalists_amount", default: 0 t.integer "intermediate_round_participants_amount", default: 0 + t.string "location" t.string "name", null: false + t.string "organizer_name" t.integer "playoff_teams_amount", default: 0 t.boolean "public", default: true t.boolean "read_only_mode", default: false, null: false + t.datetime "starts_at" t.string "sync_auth_token" t.string "sync_auth_token_digest" t.datetime "sync_last_imported_snapshot_at" @@ -184,6 +189,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_30_121000) do t.datetime "timestamp" t.datetime "updated_at", precision: nil, null: false t.integer "user_id", null: false + t.string "website_url" t.index ["code"], name: "index_tournaments_on_code", unique: true t.index ["user_id"], name: "index_tournaments_on_user_id" end diff --git a/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb index 4f396b3..fdbc7d2 100644 --- a/spec/controllers/tournaments_controller_spec.rb +++ b/spec/controllers/tournaments_controller_spec.rb @@ -105,6 +105,7 @@ RSpec.describe TournamentsController, type: :controller do expected_keys = %i[ id name code public description playoff_teams_amount instant_finalists_amount intermediate_round_participants_amount + location starts_at ends_at organizer_name contact_email website_url timestamp timer_mode timer_reason timer_reason_text owner_username stages teams team_action_lists ] expect(json.keys).to match_array(expected_keys) @@ -407,6 +408,29 @@ RSpec.describe TournamentsController, type: :controller do end.to change(Tournament, :count).by(1) end + it 'stores tournament metadata' do + starts_at = '2030-06-01T10:00:00+02:00' + ends_at = '2030-06-01T18:00:00+02:00' + + post :create, params: create_playoff_tournament_data.merge( + location: 'Stadtpark Halle', + starts_at: starts_at, + ends_at: ends_at, + organizer_name: 'BPWSTR Crew', + contact_email: 'orga@example.com', + website_url: 'https://example.com/tournaments/summer-cup' + ) + + expect(response).to have_http_status(:created) + tournament = Tournament.find(deserialize_response(response)[:id]) + expect(tournament.location).to eq('Stadtpark Halle') + expect(tournament.starts_at.iso8601).to eq(Time.iso8601(starts_at).utc.iso8601) + expect(tournament.ends_at.iso8601).to eq(Time.iso8601(ends_at).utc.iso8601) + expect(tournament.organizer_name).to eq('BPWSTR Crew') + expect(tournament.contact_email).to eq('orga@example.com') + expect(tournament.website_url).to eq('https://example.com/tournaments/summer-cup') + end + it 'associates the new tournament with the authenticated user' do expect do post :create, params: create_playoff_tournament_data @@ -587,6 +611,49 @@ RSpec.describe TournamentsController, type: :controller do expect(@tournament.name).to eq(valid_update[:name]) end + it 'updates tournament metadata' do + starts_at = '2030-06-01T10:00:00+02:00' + ends_at = '2030-06-01T18:00:00+02:00' + + put :update, params: { + id: @tournament.to_param, + location: 'Arena Nord', + starts_at: starts_at, + ends_at: ends_at, + organizer_name: 'Turniere Ops', + contact_email: 'ops@example.com', + website_url: 'https://example.com/events/arena-nord' + } + + expect(response).to have_http_status(:ok) + @tournament.reload + json = deserialize_response(response) + expect(@tournament.location).to eq('Arena Nord') + expect(@tournament.starts_at.iso8601).to eq(Time.iso8601(starts_at).utc.iso8601) + expect(@tournament.ends_at.iso8601).to eq(Time.iso8601(ends_at).utc.iso8601) + expect(@tournament.organizer_name).to eq('Turniere Ops') + expect(@tournament.contact_email).to eq('ops@example.com') + expect(@tournament.website_url).to eq('https://example.com/events/arena-nord') + expect(json[:starts_at]).to eq(@tournament.starts_at.iso8601) + expect(json[:ends_at]).to eq(@tournament.ends_at.iso8601) + end + + it 'rejects invalid tournament metadata' do + put :update, params: { + id: @tournament.to_param, + starts_at: '2030-06-01T18:00:00+02:00', + ends_at: '2030-06-01T10:00:00+02:00', + contact_email: 'not-an-email', + website_url: 'notaurl' + } + + expect(response).to have_http_status(:unprocessable_content) + json = deserialize_response(response) + expect(json[:ends_at]).to include('must be after starts_at') + expect(json[:contact_email]).to include('is invalid') + expect(json[:website_url]).to include('is invalid') + end + it 'renders a JSON response with the tournament' do put :update, params: { id: @tournament.to_param }.merge(valid_update) expect(response).to have_http_status(:ok) @@ -810,6 +877,12 @@ RSpec.describe TournamentsController, type: :controller do code: 'abc123', description: 'Synced description', public: true, + location: 'Synced Arena', + starts_at: '2030-06-01T10:00:00+02:00', + ends_at: '2030-06-01T18:00:00+02:00', + organizer_name: 'Synced Crew', + contact_email: 'sync@example.com', + website_url: 'https://example.com/synced', playoff_teams_amount: 4, instant_finalists_amount: 4, intermediate_round_participants_amount: 0, @@ -827,6 +900,12 @@ RSpec.describe TournamentsController, type: :controller do @tournament.reload expect(@tournament.sync_source_tournament_id).to eq(123) expect(@tournament.name).to eq('Synced Tournament') + expect(@tournament.location).to eq('Synced Arena') + expect(@tournament.starts_at.iso8601).to eq(Time.iso8601('2030-06-01T10:00:00+02:00').utc.iso8601) + expect(@tournament.ends_at.iso8601).to eq(Time.iso8601('2030-06-01T18:00:00+02:00').utc.iso8601) + expect(@tournament.organizer_name).to eq('Synced Crew') + expect(@tournament.contact_email).to eq('sync@example.com') + expect(@tournament.website_url).to eq('https://example.com/synced') expect(@tournament.teams.pluck(:name)).to eq(['Alpha']) expect(deserialize_response(response)).not_to have_key(:sync_auth_token) expect(deserialize_response(response)).not_to have_key(:sync_auth_token_digest) diff --git a/spec/e2e/http/api_surface_spec.rb b/spec/e2e/http/api_surface_spec.rb index f8600d7..a92fbac 100644 --- a/spec/e2e/http/api_surface_spec.rb +++ b/spec/e2e/http/api_surface_spec.rb @@ -82,6 +82,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do expect(full_show[:status]).to eq(200) expect(full_show.dig(:json, :stages)).to be_an(Array) expect(full_show.dig(:json, :teams)).to be_an(Array) + expect(full_show.dig(:json, :location)).to start_with('Venue') + expect(full_show.dig(:json, :starts_at)).to eq('2030-06-01T08:00:00Z') + expect(full_show.dig(:json, :ends_at)).to eq('2030-06-01T16:00:00Z') + expect(full_show.dig(:json, :organizer_name)).to eq('Turniere E2E') + expect(full_show.dig(:json, :contact_email)).to eq('events@example.com') + expect(full_show.dig(:json, :website_url)).to eq('https://example.com/turniere-e2e') expect(invalid_index[:status]).to eq(400) expect(invalid_index.dig(:json, :error)).to eq('invalid type') @@ -119,6 +125,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do update_response = owner_client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: "Renamed #{unique_suffix}", + location: 'Updated E2E Arena', + starts_at: '2030-07-01T10:00:00+02:00', + ends_at: '2030-07-01T18:00:00+02:00', + organizer_name: 'Updated E2E Org', + contact_email: 'updated-events@example.com', + website_url: 'https://example.com/updated-turniere-e2e', playoff_teams_amount: 8, instant_finalists_amount: 8, intermediate_round_participants_amount: 0 @@ -172,6 +184,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do expect(update_response[:status]).to eq(200) expect(update_response.dig(:json, :name)).to start_with('Renamed') + expect(update_response.dig(:json, :location)).to eq('Updated E2E Arena') + expect(update_response.dig(:json, :starts_at)).to eq('2030-07-01T08:00:00Z') + expect(update_response.dig(:json, :ends_at)).to eq('2030-07-01T16:00:00Z') + expect(update_response.dig(:json, :organizer_name)).to eq('Updated E2E Org') + expect(update_response.dig(:json, :contact_email)).to eq('updated-events@example.com') + expect(update_response.dig(:json, :website_url)).to eq('https://example.com/updated-turniere-e2e') expect(auto_recalc_response[:status]).to eq(200) expect(auto_recalc_response.dig(:json, :playoff_teams_amount)).to eq(4) @@ -511,6 +529,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do name: "#{name_prefix} #{unique_suffix}", description: 'HTTP API surface E2E group-stage tournament', public: public, + location: "Venue #{name_prefix}", + starts_at: '2030-06-01T10:00:00+02:00', + ends_at: '2030-06-01T18:00:00+02:00', + organizer_name: 'Turniere E2E', + contact_email: 'events@example.com', + website_url: 'https://example.com/turniere-e2e', group_stage: true, playoff_teams_amount: 8, teams: 4.times.flat_map do |group_index| @@ -567,6 +591,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do name: "#{name_prefix} #{unique_suffix}", description: 'HTTP API surface E2E playoff tournament', public: public, + location: "Venue #{name_prefix}", + starts_at: '2030-06-01T10:00:00+02:00', + ends_at: '2030-06-01T18:00:00+02:00', + organizer_name: 'Turniere E2E', + contact_email: 'events@example.com', + website_url: 'https://example.com/turniere-e2e', teams: 4.times.map do |index| { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } end diff --git a/spec/e2e/http/tournament_follow_sync_spec.rb b/spec/e2e/http/tournament_follow_sync_spec.rb index aa3f87c..7560aed 100644 --- a/spec/e2e/http/tournament_follow_sync_spec.rb +++ b/spec/e2e/http/tournament_follow_sync_spec.rb @@ -111,6 +111,12 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do log_phase!('tighten playoff cutoff and rename teams') update_cutoff = source_owner_client.patch("/tournaments/#{source.fetch(:id)}", body: { + location: 'Source Updated Arena', + starts_at: '2030-07-01T10:00:00+02:00', + ends_at: '2030-07-01T18:00:00+02:00', + organizer_name: 'Updated Sync Crew', + contact_email: 'updated-sync@example.com', + website_url: 'https://example.com/updated-sync-source', playoff_teams_amount: 4, instant_finalists_amount: 4, intermediate_round_participants_amount: 0 @@ -233,6 +239,12 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do name: "#{name_prefix} #{unique_suffix}", description: 'Follower sync source tournament', public: public, + location: "Source Venue #{name_prefix}", + starts_at: '2030-06-01T10:00:00+02:00', + ends_at: '2030-06-01T18:00:00+02:00', + organizer_name: 'Sync Source Crew', + contact_email: 'sync-source@example.com', + website_url: 'https://example.com/sync-source', group_stage: true, playoff_teams_amount: 8, teams: 4.times.flat_map do |group_index| @@ -255,6 +267,12 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do name: "#{name_prefix} #{unique_suffix}", description: 'Unrelated playoff tournament', public: public, + location: "Noise Venue #{name_prefix}", + starts_at: '2030-06-01T10:00:00+02:00', + ends_at: '2030-06-01T18:00:00+02:00', + organizer_name: 'Noise Crew', + contact_email: 'noise@example.com', + website_url: 'https://example.com/noise', teams: 4.times.map do |index| { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } end @@ -270,6 +288,12 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do name: "#{name_prefix} #{unique_suffix}", description: 'Follower tournament', public: public, + location: "Follower Venue #{name_prefix}", + starts_at: '2030-05-01T10:00:00+02:00', + ends_at: '2030-05-01T18:00:00+02:00', + organizer_name: 'Follower Crew', + contact_email: 'follower@example.com', + website_url: 'https://example.com/follower', read_only_mode: true, sync_auth_token: sync_token }) @@ -500,6 +524,12 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do { name: tournament.fetch(:name), public: tournament.fetch(:public), + location: tournament[:location], + starts_at: tournament[:starts_at], + ends_at: tournament[:ends_at], + organizer_name: tournament[:organizer_name], + contact_email: tournament[:contact_email], + website_url: tournament[:website_url], playoff_teams_amount: tournament.fetch(:playoff_teams_amount), instant_finalists_amount: tournament.fetch(:instant_finalists_amount), intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount), diff --git a/spec/e2e/http/tournament_sync_test_spec.rb b/spec/e2e/http/tournament_sync_test_spec.rb index a1d26f9..8df3498 100644 --- a/spec/e2e/http/tournament_sync_test_spec.rb +++ b/spec/e2e/http/tournament_sync_test_spec.rb @@ -33,7 +33,13 @@ RSpec.describe 'Tournament sync test HTTP E2E' do rename = client.patch("/tournaments/#{leader.fetch(:id)}", body: { name: "Leader Renamed #{SecureRandom.hex(3)}", - description: 'Roundtrip sync test' + description: 'Roundtrip sync test', + location: 'Sync Test Arena', + starts_at: '2030-08-01T10:00:00+02:00', + ends_at: '2030-08-01T18:00:00+02:00', + organizer_name: 'Sync Test Org', + contact_email: 'sync-test@example.com', + website_url: 'https://example.com/sync-test' }) expect(rename[:status]).to eq(200) @@ -52,6 +58,12 @@ RSpec.describe 'Tournament sync test HTTP E2E' do expect(after_follower.fetch(:name)).to eq(after_leader.fetch(:name)) expect(after_follower.fetch(:description)).to eq(after_leader.fetch(:description)) + expect(after_follower.fetch(:location)).to eq(after_leader.fetch(:location)) + expect(after_follower.fetch(:starts_at)).to eq(after_leader.fetch(:starts_at)) + expect(after_follower.fetch(:ends_at)).to eq(after_leader.fetch(:ends_at)) + expect(after_follower.fetch(:organizer_name)).to eq(after_leader.fetch(:organizer_name)) + expect(after_follower.fetch(:contact_email)).to eq(after_leader.fetch(:contact_email)) + expect(after_follower.fetch(:website_url)).to eq(after_leader.fetch(:website_url)) expect(after_follower.fetch(:teams).map { |team| team.fetch(:name) }) .to match_array(after_leader.fetch(:teams).map { |team| team.fetch(:name) }) end diff --git a/spec/services/tournament_snapshot_importer_spec.rb b/spec/services/tournament_snapshot_importer_spec.rb index b86e6fe..f9b563b 100644 --- a/spec/services/tournament_snapshot_importer_spec.rb +++ b/spec/services/tournament_snapshot_importer_spec.rb @@ -7,6 +7,12 @@ RSpec.describe TournamentSnapshotImporter do it 'replaces follower tournament graph using source ids as stable mapping keys' do source = create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match) source.update!( + location: 'Court 4', + starts_at: Time.utc(2026, 4, 24, 9, 0, 0), + ends_at: Time.utc(2026, 4, 24, 18, 30, 0), + organizer_name: 'Sync Ops', + contact_email: 'sync-ops@example.com', + website_url: 'https://example.com/sync-cup', timestamp: Time.utc(2026, 4, 24, 10, 15, 0), timer_mode: 'countdown', timer_reason: 'custom', @@ -28,6 +34,12 @@ RSpec.describe TournamentSnapshotImporter do follower.reload expect(follower.sync_source_tournament_id).to eq(source.id) expect(follower.name).to eq(source.name) + expect(follower.location).to eq(source.location) + expect(follower.starts_at).to eq(source.starts_at) + expect(follower.ends_at).to eq(source.ends_at) + expect(follower.organizer_name).to eq(source.organizer_name) + expect(follower.contact_email).to eq(source.contact_email) + expect(follower.website_url).to eq(source.website_url) expect(follower.timer_mode).to eq(source.timer_mode) expect(follower.timer_reason).to eq(source.timer_reason) expect(follower.timer_reason_text).to eq(source.timer_reason_text) @@ -197,6 +209,12 @@ RSpec.describe TournamentSnapshotImporter do { name: tournament.name, public: tournament.public, + location: tournament.location, + starts_at: tournament.starts_at&.iso8601, + ends_at: tournament.ends_at&.iso8601, + organizer_name: tournament.organizer_name, + contact_email: tournament.contact_email, + website_url: tournament.website_url, playoff_teams_amount: tournament.playoff_teams_amount, instant_finalists_amount: tournament.instant_finalists_amount, intermediate_round_participants_amount: tournament.intermediate_round_participants_amount, From 794f83514882d6fcb8c5ed7553d500da9f960e9b Mon Sep 17 00:00:00 2001 From: Malaber Date: Mon, 4 May 2026 14:55:48 +0200 Subject: [PATCH 2/2] fix(tournaments): keep owner data out metadata Tournament metadata remains optional and tournament-scoped. --- app/controllers/tournaments_controller.rb | 2 +- app/models/tournament.rb | 3 +-- app/serializers/simple_tournament_serializer.rb | 2 +- app/services/tournament_snapshot_importer.rb | 2 -- app/services/tournament_sync_schema.rb | 2 -- ...20260504100000_add_metadata_to_tournaments.rb | 2 -- db/schema.rb | 2 -- spec/controllers/tournaments_controller_spec.rb | 16 +--------------- spec/e2e/http/api_surface_spec.rb | 10 ---------- spec/e2e/http/tournament_follow_sync_spec.rb | 10 ---------- spec/e2e/http/tournament_sync_test_spec.rb | 4 ---- .../tournament_snapshot_importer_spec.rb | 6 ------ 12 files changed, 4 insertions(+), 57 deletions(-) diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index 5d05a5f..d82f58e 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -285,7 +285,7 @@ class TournamentsController < ApplicationController params.slice( :name, :description, :public, :teams, :group_stage, :playoff_teams_amount, :instant_finalists_amount, :intermediate_round_participants_amount, - :location, :starts_at, :ends_at, :organizer_name, :contact_email, :website_url, + :location, :starts_at, :ends_at, :website_url, :read_only_mode, :sync_target_url, :sync_auth_token ).permit! end diff --git a/app/models/tournament.rb b/app/models/tournament.rb index 2b2f2d8..6f0ad3a 100644 --- a/app/models/tournament.rb +++ b/app/models/tournament.rb @@ -6,7 +6,7 @@ require 'openssl' class Tournament < ApplicationRecord TIMER_MODES = %w[countdown countup].freeze TIMER_REASONS = %w[tournament_start playoff_start match_end custom].freeze - METADATA_STRING_ATTRIBUTES = %i[location organizer_name contact_email website_url].freeze + METADATA_STRING_ATTRIBUTES = %i[location website_url].freeze belongs_to :user belongs_to :owner, class_name: 'User', foreign_key: :user_id @@ -22,7 +22,6 @@ class Tournament < ApplicationRecord validates :timer_reason, inclusion: { in: TIMER_REASONS }, allow_nil: true validates :sync_target_url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true validates :website_url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true - validates :contact_email, format: URI::MailTo::EMAIL_REGEXP, allow_blank: true validate :sync_configuration_is_complete validate :playoff_teams_amount_is_positive_power_of_two diff --git a/app/serializers/simple_tournament_serializer.rb b/app/serializers/simple_tournament_serializer.rb index 6bb26e4..496cbf4 100644 --- a/app/serializers/simple_tournament_serializer.rb +++ b/app/serializers/simple_tournament_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SimpleTournamentSerializer < ApplicationSerializer - attributes :name, :code, :public, :location, :organizer_name, :contact_email, :website_url + attributes :name, :code, :public, :location, :website_url attribute :starts_at do object.starts_at&.iso8601 diff --git a/app/services/tournament_snapshot_importer.rb b/app/services/tournament_snapshot_importer.rb index 0b443f3..db97c91 100644 --- a/app/services/tournament_snapshot_importer.rb +++ b/app/services/tournament_snapshot_importer.rb @@ -7,8 +7,6 @@ class TournamentSnapshotImporter description public location - organizer_name - contact_email website_url playoff_teams_amount instant_finalists_amount diff --git a/app/services/tournament_sync_schema.rb b/app/services/tournament_sync_schema.rb index 7c9f436..f395516 100644 --- a/app/services/tournament_sync_schema.rb +++ b/app/services/tournament_sync_schema.rb @@ -12,8 +12,6 @@ class TournamentSyncSchema location starts_at ends_at - organizer_name - contact_email website_url playoff_teams_amount instant_finalists_amount diff --git a/db/migrate/20260504100000_add_metadata_to_tournaments.rb b/db/migrate/20260504100000_add_metadata_to_tournaments.rb index 5d10c4e..bcb19f0 100644 --- a/db/migrate/20260504100000_add_metadata_to_tournaments.rb +++ b/db/migrate/20260504100000_add_metadata_to_tournaments.rb @@ -6,8 +6,6 @@ class AddMetadataToTournaments < ActiveRecord::Migration[7.0] t.string :location t.datetime :starts_at t.datetime :ends_at - t.string :organizer_name - t.string :contact_email t.string :website_url end end diff --git a/db/schema.rb b/db/schema.rb index 766e949..4b41a05 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -163,7 +163,6 @@ ActiveRecord::Schema[8.1].define(version: 2026_05_04_100000) do create_table "tournaments", force: :cascade do |t| t.string "code", null: false - t.string "contact_email" t.datetime "created_at", precision: nil, null: false t.string "description" t.datetime "ends_at" @@ -171,7 +170,6 @@ ActiveRecord::Schema[8.1].define(version: 2026_05_04_100000) do t.integer "intermediate_round_participants_amount", default: 0 t.string "location" t.string "name", null: false - t.string "organizer_name" t.integer "playoff_teams_amount", default: 0 t.boolean "public", default: true t.boolean "read_only_mode", default: false, null: false diff --git a/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb index fdbc7d2..87b0b2a 100644 --- a/spec/controllers/tournaments_controller_spec.rb +++ b/spec/controllers/tournaments_controller_spec.rb @@ -105,7 +105,7 @@ RSpec.describe TournamentsController, type: :controller do expected_keys = %i[ id name code public description playoff_teams_amount instant_finalists_amount intermediate_round_participants_amount - location starts_at ends_at organizer_name contact_email website_url + location starts_at ends_at website_url timestamp timer_mode timer_reason timer_reason_text owner_username stages teams team_action_lists ] expect(json.keys).to match_array(expected_keys) @@ -416,8 +416,6 @@ RSpec.describe TournamentsController, type: :controller do location: 'Stadtpark Halle', starts_at: starts_at, ends_at: ends_at, - organizer_name: 'BPWSTR Crew', - contact_email: 'orga@example.com', website_url: 'https://example.com/tournaments/summer-cup' ) @@ -426,8 +424,6 @@ RSpec.describe TournamentsController, type: :controller do expect(tournament.location).to eq('Stadtpark Halle') expect(tournament.starts_at.iso8601).to eq(Time.iso8601(starts_at).utc.iso8601) expect(tournament.ends_at.iso8601).to eq(Time.iso8601(ends_at).utc.iso8601) - expect(tournament.organizer_name).to eq('BPWSTR Crew') - expect(tournament.contact_email).to eq('orga@example.com') expect(tournament.website_url).to eq('https://example.com/tournaments/summer-cup') end @@ -620,8 +616,6 @@ RSpec.describe TournamentsController, type: :controller do location: 'Arena Nord', starts_at: starts_at, ends_at: ends_at, - organizer_name: 'Turniere Ops', - contact_email: 'ops@example.com', website_url: 'https://example.com/events/arena-nord' } @@ -631,8 +625,6 @@ RSpec.describe TournamentsController, type: :controller do expect(@tournament.location).to eq('Arena Nord') expect(@tournament.starts_at.iso8601).to eq(Time.iso8601(starts_at).utc.iso8601) expect(@tournament.ends_at.iso8601).to eq(Time.iso8601(ends_at).utc.iso8601) - expect(@tournament.organizer_name).to eq('Turniere Ops') - expect(@tournament.contact_email).to eq('ops@example.com') expect(@tournament.website_url).to eq('https://example.com/events/arena-nord') expect(json[:starts_at]).to eq(@tournament.starts_at.iso8601) expect(json[:ends_at]).to eq(@tournament.ends_at.iso8601) @@ -643,14 +635,12 @@ RSpec.describe TournamentsController, type: :controller do id: @tournament.to_param, starts_at: '2030-06-01T18:00:00+02:00', ends_at: '2030-06-01T10:00:00+02:00', - contact_email: 'not-an-email', website_url: 'notaurl' } expect(response).to have_http_status(:unprocessable_content) json = deserialize_response(response) expect(json[:ends_at]).to include('must be after starts_at') - expect(json[:contact_email]).to include('is invalid') expect(json[:website_url]).to include('is invalid') end @@ -880,8 +870,6 @@ RSpec.describe TournamentsController, type: :controller do location: 'Synced Arena', starts_at: '2030-06-01T10:00:00+02:00', ends_at: '2030-06-01T18:00:00+02:00', - organizer_name: 'Synced Crew', - contact_email: 'sync@example.com', website_url: 'https://example.com/synced', playoff_teams_amount: 4, instant_finalists_amount: 4, @@ -903,8 +891,6 @@ RSpec.describe TournamentsController, type: :controller do expect(@tournament.location).to eq('Synced Arena') expect(@tournament.starts_at.iso8601).to eq(Time.iso8601('2030-06-01T10:00:00+02:00').utc.iso8601) expect(@tournament.ends_at.iso8601).to eq(Time.iso8601('2030-06-01T18:00:00+02:00').utc.iso8601) - expect(@tournament.organizer_name).to eq('Synced Crew') - expect(@tournament.contact_email).to eq('sync@example.com') expect(@tournament.website_url).to eq('https://example.com/synced') expect(@tournament.teams.pluck(:name)).to eq(['Alpha']) expect(deserialize_response(response)).not_to have_key(:sync_auth_token) diff --git a/spec/e2e/http/api_surface_spec.rb b/spec/e2e/http/api_surface_spec.rb index a92fbac..b521e8c 100644 --- a/spec/e2e/http/api_surface_spec.rb +++ b/spec/e2e/http/api_surface_spec.rb @@ -85,8 +85,6 @@ RSpec.describe 'Backend API surface HTTP E2E' do expect(full_show.dig(:json, :location)).to start_with('Venue') expect(full_show.dig(:json, :starts_at)).to eq('2030-06-01T08:00:00Z') expect(full_show.dig(:json, :ends_at)).to eq('2030-06-01T16:00:00Z') - expect(full_show.dig(:json, :organizer_name)).to eq('Turniere E2E') - expect(full_show.dig(:json, :contact_email)).to eq('events@example.com') expect(full_show.dig(:json, :website_url)).to eq('https://example.com/turniere-e2e') expect(invalid_index[:status]).to eq(400) @@ -128,8 +126,6 @@ RSpec.describe 'Backend API surface HTTP E2E' do location: 'Updated E2E Arena', starts_at: '2030-07-01T10:00:00+02:00', ends_at: '2030-07-01T18:00:00+02:00', - organizer_name: 'Updated E2E Org', - contact_email: 'updated-events@example.com', website_url: 'https://example.com/updated-turniere-e2e', playoff_teams_amount: 8, instant_finalists_amount: 8, @@ -187,8 +183,6 @@ RSpec.describe 'Backend API surface HTTP E2E' do expect(update_response.dig(:json, :location)).to eq('Updated E2E Arena') expect(update_response.dig(:json, :starts_at)).to eq('2030-07-01T08:00:00Z') expect(update_response.dig(:json, :ends_at)).to eq('2030-07-01T16:00:00Z') - expect(update_response.dig(:json, :organizer_name)).to eq('Updated E2E Org') - expect(update_response.dig(:json, :contact_email)).to eq('updated-events@example.com') expect(update_response.dig(:json, :website_url)).to eq('https://example.com/updated-turniere-e2e') expect(auto_recalc_response[:status]).to eq(200) @@ -532,8 +526,6 @@ RSpec.describe 'Backend API surface HTTP E2E' do location: "Venue #{name_prefix}", starts_at: '2030-06-01T10:00:00+02:00', ends_at: '2030-06-01T18:00:00+02:00', - organizer_name: 'Turniere E2E', - contact_email: 'events@example.com', website_url: 'https://example.com/turniere-e2e', group_stage: true, playoff_teams_amount: 8, @@ -594,8 +586,6 @@ RSpec.describe 'Backend API surface HTTP E2E' do location: "Venue #{name_prefix}", starts_at: '2030-06-01T10:00:00+02:00', ends_at: '2030-06-01T18:00:00+02:00', - organizer_name: 'Turniere E2E', - contact_email: 'events@example.com', website_url: 'https://example.com/turniere-e2e', teams: 4.times.map do |index| { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } diff --git a/spec/e2e/http/tournament_follow_sync_spec.rb b/spec/e2e/http/tournament_follow_sync_spec.rb index 7560aed..bf27008 100644 --- a/spec/e2e/http/tournament_follow_sync_spec.rb +++ b/spec/e2e/http/tournament_follow_sync_spec.rb @@ -114,8 +114,6 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do location: 'Source Updated Arena', starts_at: '2030-07-01T10:00:00+02:00', ends_at: '2030-07-01T18:00:00+02:00', - organizer_name: 'Updated Sync Crew', - contact_email: 'updated-sync@example.com', website_url: 'https://example.com/updated-sync-source', playoff_teams_amount: 4, instant_finalists_amount: 4, @@ -242,8 +240,6 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do location: "Source Venue #{name_prefix}", starts_at: '2030-06-01T10:00:00+02:00', ends_at: '2030-06-01T18:00:00+02:00', - organizer_name: 'Sync Source Crew', - contact_email: 'sync-source@example.com', website_url: 'https://example.com/sync-source', group_stage: true, playoff_teams_amount: 8, @@ -270,8 +266,6 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do location: "Noise Venue #{name_prefix}", starts_at: '2030-06-01T10:00:00+02:00', ends_at: '2030-06-01T18:00:00+02:00', - organizer_name: 'Noise Crew', - contact_email: 'noise@example.com', website_url: 'https://example.com/noise', teams: 4.times.map do |index| { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } @@ -291,8 +285,6 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do location: "Follower Venue #{name_prefix}", starts_at: '2030-05-01T10:00:00+02:00', ends_at: '2030-05-01T18:00:00+02:00', - organizer_name: 'Follower Crew', - contact_email: 'follower@example.com', website_url: 'https://example.com/follower', read_only_mode: true, sync_auth_token: sync_token @@ -527,8 +519,6 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do location: tournament[:location], starts_at: tournament[:starts_at], ends_at: tournament[:ends_at], - organizer_name: tournament[:organizer_name], - contact_email: tournament[:contact_email], website_url: tournament[:website_url], playoff_teams_amount: tournament.fetch(:playoff_teams_amount), instant_finalists_amount: tournament.fetch(:instant_finalists_amount), diff --git a/spec/e2e/http/tournament_sync_test_spec.rb b/spec/e2e/http/tournament_sync_test_spec.rb index 8df3498..a3f698b 100644 --- a/spec/e2e/http/tournament_sync_test_spec.rb +++ b/spec/e2e/http/tournament_sync_test_spec.rb @@ -37,8 +37,6 @@ RSpec.describe 'Tournament sync test HTTP E2E' do location: 'Sync Test Arena', starts_at: '2030-08-01T10:00:00+02:00', ends_at: '2030-08-01T18:00:00+02:00', - organizer_name: 'Sync Test Org', - contact_email: 'sync-test@example.com', website_url: 'https://example.com/sync-test' }) expect(rename[:status]).to eq(200) @@ -61,8 +59,6 @@ RSpec.describe 'Tournament sync test HTTP E2E' do expect(after_follower.fetch(:location)).to eq(after_leader.fetch(:location)) expect(after_follower.fetch(:starts_at)).to eq(after_leader.fetch(:starts_at)) expect(after_follower.fetch(:ends_at)).to eq(after_leader.fetch(:ends_at)) - expect(after_follower.fetch(:organizer_name)).to eq(after_leader.fetch(:organizer_name)) - expect(after_follower.fetch(:contact_email)).to eq(after_leader.fetch(:contact_email)) expect(after_follower.fetch(:website_url)).to eq(after_leader.fetch(:website_url)) expect(after_follower.fetch(:teams).map { |team| team.fetch(:name) }) .to match_array(after_leader.fetch(:teams).map { |team| team.fetch(:name) }) diff --git a/spec/services/tournament_snapshot_importer_spec.rb b/spec/services/tournament_snapshot_importer_spec.rb index f9b563b..34e4e6a 100644 --- a/spec/services/tournament_snapshot_importer_spec.rb +++ b/spec/services/tournament_snapshot_importer_spec.rb @@ -10,8 +10,6 @@ RSpec.describe TournamentSnapshotImporter do location: 'Court 4', starts_at: Time.utc(2026, 4, 24, 9, 0, 0), ends_at: Time.utc(2026, 4, 24, 18, 30, 0), - organizer_name: 'Sync Ops', - contact_email: 'sync-ops@example.com', website_url: 'https://example.com/sync-cup', timestamp: Time.utc(2026, 4, 24, 10, 15, 0), timer_mode: 'countdown', @@ -37,8 +35,6 @@ RSpec.describe TournamentSnapshotImporter do expect(follower.location).to eq(source.location) expect(follower.starts_at).to eq(source.starts_at) expect(follower.ends_at).to eq(source.ends_at) - expect(follower.organizer_name).to eq(source.organizer_name) - expect(follower.contact_email).to eq(source.contact_email) expect(follower.website_url).to eq(source.website_url) expect(follower.timer_mode).to eq(source.timer_mode) expect(follower.timer_reason).to eq(source.timer_reason) @@ -212,8 +208,6 @@ RSpec.describe TournamentSnapshotImporter do location: tournament.location, starts_at: tournament.starts_at&.iso8601, ends_at: tournament.ends_at&.iso8601, - organizer_name: tournament.organizer_name, - contact_email: tournament.contact_email, website_url: tournament.website_url, playoff_teams_amount: tournament.playoff_teams_amount, instant_finalists_amount: tournament.instant_finalists_amount,