class Tournament < ApplicationRecord include Coverable FORMAT_KINDS = %w[groups knockout mixed free].freeze STATUSES = %w[draft published live archived].freeze INTERNAL_TEAM_KIND = "tournament_broadcast" belongs_to :club belongs_to :broadcast_team, class_name: "Team", optional: true has_many :broadcast_assignments, class_name: "TournamentBroadcastAssignment", dependent: :destroy has_many :matches, dependent: :destroy has_many :broadcast_invitations, class_name: "TournamentBroadcastInvitation", dependent: :destroy has_many :participants, class_name: "TournamentParticipant", dependent: :destroy has_many :rounds, class_name: "TournamentRound", dependent: :destroy has_many :groups, class_name: "TournamentGroup", dependent: :destroy has_many_attached :invite_email_images has_one_attached :logo_file validates :name, presence: true validates :slug, presence: true, uniqueness: true, format: { with: /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/, message: "solo lettere minuscole, numeri e trattini" } validates :sport_key, presence: true validates :starts_on, :ends_on, presence: true validates :format_kind, inclusion: { in: FORMAT_KINDS } validates :status, inclusion: { in: STATUSES } validates :invite_draft_note, length: { maximum: 2000 }, allow_blank: true validate :sport_key_known validate :ends_on_not_before_starts_on validate :logo_file_type, if: -> { logo_file.attached? } before_validation :normalize_sport_key before_validation :assign_slug, on: :create before_validation :normalize_slug, if: -> { slug_changed? && slug.present? } before_validation :normalize_courts scope :visible_to_public, -> { where(status: %w[published live archived]) } scope :listed_on_live, -> { where(status: %w[published live]) } scope :search_public, lambda { |query| q = query.to_s.strip return all if q.blank? term = "%#{sanitize_sql_like(q)}%" left_joins(:club).where( "tournaments.name ILIKE :term OR clubs.name ILIKE :term OR tournaments.venue ILIKE :term", term: term ) } def published? status.in?(%w[published live archived]) end def archived? status == "archived" end def writable? !archived? end def uses_groups? format_kind.in?(%w[groups mixed]) end def uses_knockout? format_kind.in?(%w[knockout mixed]) end def court_list Array(courts).map { |c| c.to_s.strip }.reject(&:blank?) end def days (starts_on..ends_on).to_a end def sport_label Sports::Catalog.find_optional(sport_key)&.dig(:label) || sport_key.to_s.humanize end def public_page_path Rails.application.routes.url_helpers.public_tournament_page_path(slug) end def effective_primary_color club.effective_primary_color end def effective_secondary_color club.effective_secondary_color end def effective_logo_url return unless logo_file.attached? Rails.application.routes.url_helpers.rails_blob_path(logo_file, only_path: true) end def concurrent_limit club.subscription&.plan&.concurrent_streams_limit end private def normalize_sport_key self.sport_key = Sports::Catalog.normalize_key(sport_key) if sport_key.present? end def sport_key_known return if sport_key.blank? return if Sports::Catalog.find_optional(sport_key) errors.add(:sport_key, "non valido") end def ends_on_not_before_starts_on return if starts_on.blank? || ends_on.blank? return if ends_on >= starts_on errors.add(:ends_on, "non può precedere la data di inizio") end def assign_slug self.slug = Tournaments::GenerateSlug.call(self) if slug.blank? end def normalize_slug self.slug = slug.to_s.parameterize end def normalize_courts self.courts = court_list self.courts = ["Campo 1"] if courts.blank? end def cover_parent club end def logo_file_type return if logo_file.content_type.in?(Coverable::COVER_IMAGE_TYPES) errors.add(:logo_file, I18n.t("coverable.errors.invalid_type")) end end