Se l'operatore è in ritardo deve ancora trovare la gara nell'app: restano in hub per tutta la giornata e, nei tornei aperti, fino alla fine dell'evento. Co-authored-by: Cursor <cursoragent@cursor.com>
247 lines
7.2 KiB
Ruby
247 lines
7.2 KiB
Ruby
class Match < ApplicationRecord
|
|
include Coverable
|
|
|
|
belongs_to :team
|
|
belongs_to :tournament, optional: true
|
|
belongs_to :home_participant, class_name: "TournamentParticipant", optional: true
|
|
belongs_to :away_participant, class_name: "TournamentParticipant", optional: true
|
|
belongs_to :tournament_group, optional: true
|
|
belongs_to :tournament_round, optional: true
|
|
belongs_to :winner_participant, class_name: "TournamentParticipant", optional: true
|
|
has_many :stream_sessions, dependent: :destroy
|
|
has_many :broadcast_assignments, class_name: "TournamentBroadcastAssignment", dependent: :destroy
|
|
|
|
has_one_attached :opponent_logo_file
|
|
|
|
ACTIVE_SESSION_STATUSES = %w[live connecting reconnecting paused].freeze
|
|
DEFAULT_OPPONENT_COLOR = "#1E3A8A"
|
|
|
|
validates :opponent_name, presence: true
|
|
validates :sets_to_win, numericality: { greater_than: 0, less_than: 6 }
|
|
validates :sport_key, presence: true
|
|
validates :opponent_primary_color, format: { with: Brandable::HEX_COLOR }, allow_blank: true
|
|
validate :sport_key_known
|
|
validate :overlay_kind_allowed
|
|
validate :scoring_rules_shape
|
|
validate :opponent_logo_file_type, if: -> { opponent_logo_file.attached? }
|
|
|
|
before_validation :normalize_sport_key
|
|
before_validation :inherit_sport_from_team, on: :create
|
|
before_validation :sync_tournament_display_names
|
|
after_commit :sync_tournament_broadcast_assignments, on: %i[create update]
|
|
|
|
scope :scheduled_for_live, -> {
|
|
where.not(scheduled_at: nil).where("scheduled_at >= ?", Time.zone.now)
|
|
}
|
|
|
|
scope :scheduled_upcoming, -> { scheduled_for_live }
|
|
|
|
def scheduled_upcoming?
|
|
scheduled_at.present? && scheduled_at >= Time.zone.now
|
|
end
|
|
|
|
def scheduled_on_calendar?
|
|
scheduled_at.present? && scheduled_at >= Time.zone.now.beginning_of_day
|
|
end
|
|
|
|
def active_stream_session
|
|
latest = stream_sessions.max_by(&:created_at)
|
|
return nil if latest.nil? || latest.status.in?(%w[ended error])
|
|
|
|
latest
|
|
end
|
|
|
|
def deletable?
|
|
session = active_stream_session
|
|
session.nil? || !session.resumable?
|
|
end
|
|
|
|
def public_status_label
|
|
active = active_stream_session
|
|
return I18n.t("matches.status.active") if active
|
|
return I18n.t("matches.status.scheduled") if scheduled_upcoming?
|
|
return I18n.t("matches.status.ready") if scheduled_at.present?
|
|
|
|
I18n.t("matches.status.no_time")
|
|
end
|
|
|
|
scope :search_teams_or_opponents, lambda { |query|
|
|
q = query.to_s.strip
|
|
return all if q.blank?
|
|
|
|
term = "%#{sanitize_sql_like(q)}%"
|
|
joins(team: :club).where(
|
|
"teams.name ILIKE :term OR matches.opponent_name ILIKE :term OR clubs.name ILIKE :term OR matches.location ILIKE :term",
|
|
term: term
|
|
)
|
|
}
|
|
|
|
def broadcast_active?
|
|
stream_sessions.where(status: ACTIVE_SESSION_STATUSES).exists?
|
|
end
|
|
|
|
def stream_completed?
|
|
stream_sessions.where(status: %w[ended error]).exists?
|
|
end
|
|
|
|
def opponent_logo_url
|
|
return unless opponent_logo_file.attached?
|
|
|
|
Rails.application.routes.url_helpers.rails_blob_path(opponent_logo_file, only_path: true)
|
|
end
|
|
|
|
def effective_opponent_primary_color
|
|
opponent_primary_color.presence || DEFAULT_OPPONENT_COLOR
|
|
end
|
|
|
|
def coach_hub_visible?
|
|
active = active_stream_session
|
|
return true if active&.resumable?
|
|
return true if active&.idle?
|
|
return false if stream_completed?
|
|
return true if scheduled_on_calendar?
|
|
return true if tournament_open_for_late_stream?
|
|
|
|
false
|
|
end
|
|
|
|
def tournament_open_for_late_stream?
|
|
return false unless tournament_match?
|
|
|
|
event = tournament
|
|
event.present? && !event.archived? && event.ends_on >= Date.current
|
|
end
|
|
|
|
def effective_board_type
|
|
Sports::Catalog.board_for(sport_key)
|
|
end
|
|
|
|
def effective_overlay_kind
|
|
kind = overlay_kind.presence || Sports::Catalog.overlay_for(sport_key)
|
|
kind = "none" if kind == "timer"
|
|
allowed = Sports::Catalog.allowed_overlays_for(sport_key)
|
|
allowed.include?(kind) ? kind : Sports::Catalog.overlay_for(sport_key)
|
|
end
|
|
|
|
def effective_scoring_rules
|
|
merged = Sports::Catalog.default_rules_for(sport_key).deep_merge(normalized_scoring_rules)
|
|
merged["sets_to_win"] = sets_to_win if merged["sets_to_win"].blank? && sets_to_win.present?
|
|
merged
|
|
end
|
|
|
|
def sport_label
|
|
Sports::Catalog.find_optional(sport_key)&.dig(:label) || sport_key.to_s.humanize
|
|
end
|
|
|
|
def tournament_match?
|
|
has_attribute?(:tournament_id) && tournament_id.present?
|
|
end
|
|
|
|
def home_display_name
|
|
home_participant&.name.presence || (tournament_match? ? I18n.t("tournaments.tbd") : team.name)
|
|
end
|
|
|
|
def away_display_name
|
|
away_participant&.name.presence || (tournament_match? ? I18n.t("tournaments.tbd") : opponent_name.presence)
|
|
end
|
|
|
|
def matchup_label
|
|
"#{home_display_name} vs #{away_display_name}"
|
|
end
|
|
|
|
def played?
|
|
result_status == "played"
|
|
end
|
|
|
|
def court_or_location
|
|
court.presence || location
|
|
end
|
|
|
|
# Alias legacy API/spec (colonna rinominata in sport_key).
|
|
def sport
|
|
sport_key
|
|
end
|
|
|
|
def sport=(value)
|
|
self.sport_key = Sports::Catalog.normalize_key(value)
|
|
end
|
|
|
|
private
|
|
|
|
def normalize_sport_key
|
|
self.sport_key = Sports::Catalog.normalize_key(sport_key)
|
|
end
|
|
|
|
def inherit_sport_from_team
|
|
if has_attribute?(:tournament_id) && tournament.present?
|
|
self.sport_key = tournament.sport_key if new_record? || sport_key.blank?
|
|
return
|
|
end
|
|
|
|
return unless team.present?
|
|
|
|
# Il default DB (pallavolo) non deve prevalere sullo sport della squadra alla creazione.
|
|
self.sport_key = team.sport_key if new_record? || sport_key.blank?
|
|
end
|
|
|
|
def sync_tournament_display_names
|
|
return unless has_attribute?(:tournament_id) && tournament_id.present?
|
|
|
|
self.opponent_name = away_display_name if away_participant.present? || opponent_name.blank?
|
|
venue_court = [tournament&.venue, court].compact_blank
|
|
self.location = venue_court.join(" — ") if venue_court.any?
|
|
|
|
self.category = tournament.name if category.blank? && tournament.present?
|
|
end
|
|
|
|
def sync_tournament_broadcast_assignments
|
|
return unless has_attribute?(:tournament_id) && tournament_id.present?
|
|
return unless saved_change_to_court? || saved_change_to_scheduled_at? || previously_new_record?
|
|
|
|
Tournaments::SyncAssignments.sync_match!(self)
|
|
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 overlay_kind_allowed
|
|
return if overlay_kind.blank?
|
|
|
|
unless Sports::OverlayKind.valid?(overlay_kind)
|
|
errors.add(:overlay_kind, "non valido")
|
|
return
|
|
end
|
|
|
|
allowed = Sports::Catalog.allowed_overlays_for(sport_key)
|
|
errors.add(:overlay_kind, "non consentito per questo sport") unless allowed.include?(overlay_kind)
|
|
end
|
|
|
|
def normalized_scoring_rules
|
|
return {} unless scoring_rules.is_a?(Hash)
|
|
|
|
scoring_rules.each_with_object({}) do |(k, v), h|
|
|
h[k.to_s] = v
|
|
end
|
|
end
|
|
|
|
def opponent_logo_file_type
|
|
return if opponent_logo_file.content_type.in?(%w[image/png image/jpeg image/webp])
|
|
|
|
errors.add(:opponent_logo_file, "deve essere PNG, JPEG o WebP")
|
|
end
|
|
|
|
def scoring_rules_shape
|
|
Sports::RulesSchema.validate!(effective_board_type, normalized_scoring_rules)
|
|
rescue ArgumentError => e
|
|
errors.add(:scoring_rules, e.message)
|
|
end
|
|
|
|
def cover_parent
|
|
nil
|
|
end
|
|
end
|