Aggiunge i tornei con hub, pagina pubblica e tabellone per semifinali e finali.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,34 +9,23 @@ module Api
|
||||
return render json: { valid: false, error: "Invito non valido o scaduto" }, status: :not_found
|
||||
end
|
||||
|
||||
render json: {
|
||||
valid: true,
|
||||
email: invitation.email,
|
||||
team_id: invitation.team_id,
|
||||
team_name: invitation.team.name,
|
||||
club_name: invitation.team.club.name,
|
||||
staff_kind: invitation.staff_kind,
|
||||
expires_at: invitation.expires_at
|
||||
}
|
||||
render json: invitation_json(invitation).merge(valid: true)
|
||||
end
|
||||
|
||||
def accept
|
||||
invitation = find_pending_invitation
|
||||
return render json: { error: "Invito non valido o scaduto" }, status: :not_found unless invitation
|
||||
|
||||
if current_user.email.downcase != invitation.email.downcase
|
||||
email = invitation.email
|
||||
if current_user.email.downcase != email.downcase
|
||||
return render json: {
|
||||
error: "Questo invito è per #{invitation.email}. Accedi con quell'indirizzo email.",
|
||||
invited_email: invitation.email
|
||||
error: "Questo invito è per #{email}. Accedi con quell'indirizzo email.",
|
||||
invited_email: email
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
invitation.accept!(current_user)
|
||||
render json: {
|
||||
team_id: invitation.team_id,
|
||||
team_name: invitation.team.name,
|
||||
message: "Sei entrato in #{invitation.team.name} come responsabile trasmissione."
|
||||
}
|
||||
render json: invitation_json(invitation).merge(message: accept_message(invitation))
|
||||
end
|
||||
|
||||
private
|
||||
@@ -45,7 +34,42 @@ module Api
|
||||
token = params[:token].to_s
|
||||
return nil if token.blank?
|
||||
|
||||
TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(token))
|
||||
digest = Digest::SHA256.hexdigest(token)
|
||||
TeamInvitation.pending.find_by(token_digest: digest) ||
|
||||
TournamentBroadcastInvitation.pending.find_by(token_digest: digest)
|
||||
end
|
||||
|
||||
def invitation_json(invitation)
|
||||
if invitation.is_a?(TournamentBroadcastInvitation)
|
||||
tournament = invitation.tournament
|
||||
{
|
||||
email: invitation.email,
|
||||
tournament_id: tournament.id,
|
||||
tournament_name: tournament.name,
|
||||
club_name: tournament.club.name,
|
||||
team_id: tournament.broadcast_team_id,
|
||||
team_name: tournament.name,
|
||||
staff_kind: "transmission",
|
||||
expires_at: invitation.expires_at
|
||||
}
|
||||
else
|
||||
{
|
||||
email: invitation.email,
|
||||
team_id: invitation.team_id,
|
||||
team_name: invitation.team.name,
|
||||
club_name: invitation.team.club.name,
|
||||
staff_kind: invitation.staff_kind,
|
||||
expires_at: invitation.expires_at
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def accept_message(invitation)
|
||||
if invitation.is_a?(TournamentBroadcastInvitation)
|
||||
"Sei incaricato delle dirette per #{invitation.tournament.name}."
|
||||
else
|
||||
"Sei entrato in #{invitation.team.name} come responsabile trasmissione."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,13 +10,22 @@ module Api
|
||||
|
||||
def index
|
||||
matches = @team.matches
|
||||
.includes(:team, :stream_sessions)
|
||||
.includes(:team, :stream_sessions, :home_participant, :away_participant)
|
||||
.order(Arel.sql("scheduled_at ASC NULLS LAST"), created_at: :desc)
|
||||
.select(&:coach_hub_visible?)
|
||||
render json: matches.map { |m| match_json(m) }
|
||||
unless current_user.club_admin?(@team.club)
|
||||
if @team.tournament_broadcast?
|
||||
assigned_ids = current_user.tournament_broadcast_assignments.select(:match_id)
|
||||
matches = matches.where(id: assigned_ids)
|
||||
end
|
||||
end
|
||||
render json: matches.select(&:coach_hub_visible?).map { |m| match_json(m) }
|
||||
end
|
||||
|
||||
def create
|
||||
if @team.tournament_broadcast?
|
||||
return render json: { error: "Le partite del torneo si programmano dal sito web." }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
attrs = match_params.to_h
|
||||
attrs["sport_key"] = @team.sport_key
|
||||
normalize_scoring_rules!(attrs)
|
||||
@@ -112,9 +121,12 @@ module Api
|
||||
{
|
||||
id: match.id,
|
||||
team_id: match.team_id,
|
||||
team_name: team.name,
|
||||
opponent_name: match.opponent_name,
|
||||
location: match.location,
|
||||
team_name: match.home_display_name,
|
||||
opponent_name: match.away_display_name,
|
||||
location: match.court_or_location,
|
||||
court: match.court,
|
||||
tournament_id: match.tournament_id,
|
||||
tournament_name: match.tournament&.name,
|
||||
scheduled_at: match.scheduled_at,
|
||||
sport: match.sport_key,
|
||||
sport_key: match.sport_key,
|
||||
@@ -126,11 +138,11 @@ module Api
|
||||
scoring_rules: match.scoring_rules.presence,
|
||||
effective_scoring_rules: match.effective_scoring_rules,
|
||||
category: match.category,
|
||||
home_primary_color: team.effective_primary_color,
|
||||
home_secondary_color: team.effective_secondary_color,
|
||||
home_logo_url: api_absolute_url(team.effective_logo_url),
|
||||
opponent_primary_color: match.effective_opponent_primary_color,
|
||||
opponent_logo_url: api_absolute_url(match.opponent_logo_url),
|
||||
home_primary_color: match.home_participant&.effective_primary_color || team.effective_primary_color,
|
||||
home_secondary_color: match.home_participant&.effective_secondary_color || team.effective_secondary_color,
|
||||
home_logo_url: api_absolute_url(match.home_participant&.effective_logo_url || team.effective_logo_url),
|
||||
opponent_primary_color: match.away_participant&.effective_primary_color || match.effective_opponent_primary_color,
|
||||
opponent_logo_url: api_absolute_url(match.away_participant&.effective_logo_url || match.opponent_logo_url),
|
||||
**match_cover_json(match),
|
||||
active_session_id: active&.id,
|
||||
active_session_status: active&.status,
|
||||
|
||||
@@ -6,6 +6,10 @@ module Api
|
||||
def create
|
||||
team_ids = current_user.streamable_teams.map(&:id)
|
||||
match = Match.where(team_id: team_ids).find(params[:match_id])
|
||||
unless current_user.can_broadcast_match?(match)
|
||||
return render json: { error: "Non sei incaricato di trasmettere questa partita" }, status: :forbidden
|
||||
end
|
||||
|
||||
session = Sessions::Create.new(user: current_user, match: match, params: session_params).call
|
||||
render json: session_json(session), status: :created
|
||||
end
|
||||
|
||||
@@ -110,6 +110,9 @@ module Api
|
||||
secondary_color: team.effective_secondary_color,
|
||||
club_id: team.club_id,
|
||||
club_name: team.club.name,
|
||||
tournament_broadcast: team.tournament_broadcast?,
|
||||
tournament_id: team.broadcast_tournament&.id,
|
||||
tournament_name: team.broadcast_tournament&.name,
|
||||
youtube_connected: yt.connected?,
|
||||
youtube_selectable: yt.selectable?,
|
||||
youtube_channel_title: yt.channel_title,
|
||||
|
||||
@@ -56,9 +56,9 @@ module Public
|
||||
def show
|
||||
require_club_owner!(@club)
|
||||
apply_checkout_flash!
|
||||
@entitlements_team = @club.teams.first
|
||||
@entitlements_team = @club.teams.visible.first
|
||||
@entitlements = @entitlements_team&.entitlements
|
||||
@teams = @club.teams.order(:name)
|
||||
@teams = @club.teams.visible.order(:name)
|
||||
@replay_stats = Recordings::ClubStats.new(@club).call if @entitlements&.can_access_recordings?
|
||||
end
|
||||
|
||||
|
||||
@@ -2,26 +2,40 @@ module Public
|
||||
class InvitationsController < WebBaseController
|
||||
def show
|
||||
@token = params[:token]
|
||||
@invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(@token.to_s))
|
||||
unless @invitation
|
||||
digest = Digest::SHA256.hexdigest(@token.to_s)
|
||||
@invitation = TeamInvitation.pending.find_by(token_digest: digest)
|
||||
@tournament_invitation = TournamentBroadcastInvitation.pending.find_by(token_digest: digest) unless @invitation
|
||||
unless @invitation || @tournament_invitation
|
||||
redirect_to public_pricing_path, alert: t("flash.invitations.invalid_or_expired")
|
||||
end
|
||||
end
|
||||
|
||||
def accept
|
||||
invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(params[:token].to_s))
|
||||
return redirect_to public_pricing_path, alert: t("flash.invitations.invalid") unless invitation
|
||||
token = params[:token].to_s
|
||||
digest = Digest::SHA256.hexdigest(token)
|
||||
invitation = TeamInvitation.pending.find_by(token_digest: digest)
|
||||
tournament_invitation = TournamentBroadcastInvitation.pending.find_by(token_digest: digest) unless invitation
|
||||
|
||||
if logged_in?
|
||||
if current_user.email.downcase != invitation.email.downcase
|
||||
redirect_to public_pricing_path, alert: t("flash.invitations.wrong_email", email: invitation.email)
|
||||
return
|
||||
end
|
||||
unless invitation || tournament_invitation
|
||||
return redirect_to public_pricing_path, alert: t("flash.invitations.invalid")
|
||||
end
|
||||
|
||||
target_email = (invitation || tournament_invitation).email
|
||||
unless logged_in?
|
||||
session[:pending_invite_token] = token
|
||||
return redirect_to public_signup_path, notice: t("flash.invitations.signup_to_accept", email: target_email)
|
||||
end
|
||||
|
||||
if current_user.email.downcase != target_email.downcase
|
||||
return redirect_to public_pricing_path, alert: t("flash.invitations.wrong_email", email: target_email)
|
||||
end
|
||||
|
||||
if invitation
|
||||
invitation.accept!(current_user)
|
||||
redirect_to public_team_details_path(invitation.team), notice: t("flash.invitations.joined_team")
|
||||
else
|
||||
session[:pending_invite_token] = params[:token]
|
||||
redirect_to public_signup_path, notice: t("flash.invitations.signup_to_accept", email: invitation.email)
|
||||
tournament_invitation.accept!(current_user)
|
||||
redirect_to public_account_path, notice: t("flash.invitations.joined_tournament")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,7 +33,11 @@ module Public
|
||||
.order(scheduled_at: :asc)
|
||||
.limit(50)
|
||||
|
||||
@online_paths = Mediamtx::Client.new.online_path_names
|
||||
tournaments = Tournament.listed_on_live.with_attached_logo_file.includes(:club).search_public(@query)
|
||||
tournaments = tournaments.where(club_id: @club.id) if @club
|
||||
@public_tournaments = tournaments.order(starts_on: :desc, name: :asc).limit(12)
|
||||
|
||||
@online_paths = fetch_online_paths
|
||||
|
||||
return unless logged_in? && @club
|
||||
|
||||
@@ -83,5 +87,13 @@ module Public
|
||||
score: session.score_state&.as_cable_payload
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_online_paths
|
||||
Mediamtx::Client.new.online_path_names
|
||||
rescue Mediamtx::Client::Error, Errno::ECONNREFUSED, SocketError
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,11 +17,18 @@ module Public
|
||||
session[:user_id] = @user.id
|
||||
if session[:pending_invite_token].present?
|
||||
token = session.delete(:pending_invite_token)
|
||||
invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(token))
|
||||
digest = Digest::SHA256.hexdigest(token)
|
||||
invitation = TeamInvitation.pending.find_by(token_digest: digest)
|
||||
if invitation && invitation.email.downcase == @user.email.downcase
|
||||
invitation.accept!(@user)
|
||||
return redirect_to public_team_details_path(invitation.team), notice: t("flash.registrations.welcome_to_team")
|
||||
end
|
||||
|
||||
tournament_invitation = TournamentBroadcastInvitation.pending.find_by(token_digest: digest)
|
||||
if tournament_invitation && tournament_invitation.email.downcase == @user.email.downcase
|
||||
tournament_invitation.accept!(@user)
|
||||
return redirect_to public_account_path, notice: t("flash.invitations.joined_tournament")
|
||||
end
|
||||
end
|
||||
redirect_to public_new_club_path, notice: t("flash.registrations.account_created")
|
||||
else
|
||||
|
||||
@@ -45,6 +45,8 @@ module Public
|
||||
PRIVATE_WEB_CONTROLLERS = %w[
|
||||
accounts clubs teams club_recordings club_billing
|
||||
club_matches matches team_roster_members
|
||||
tournaments tournament_participants tournament_groups
|
||||
tournament_matches tournament_invitations tournament_results
|
||||
].freeze
|
||||
|
||||
def load_site_announcements
|
||||
|
||||
@@ -11,6 +11,7 @@ module Public
|
||||
{ loc: "#{base}/contatti", changefreq: "monthly", priority: "0.6" },
|
||||
{ loc: "#{base}/live", changefreq: "hourly", priority: "0.85" },
|
||||
{ loc: "#{base}/squadre", changefreq: "daily", priority: "0.85" },
|
||||
{ loc: "#{base}/tornei", changefreq: "daily", priority: "0.8" },
|
||||
{ loc: "#{base}/privacy", changefreq: "yearly", priority: "0.3" },
|
||||
{ loc: "#{base}/support", changefreq: "yearly", priority: "0.3" },
|
||||
{ loc: "#{base}/cookie", changefreq: "yearly", priority: "0.3" },
|
||||
@@ -25,6 +26,14 @@ module Public
|
||||
}
|
||||
end
|
||||
|
||||
Tournament.visible_to_public.find_each do |tournament|
|
||||
@entries << {
|
||||
loc: "#{base}/tornei/#{tournament.slug}",
|
||||
changefreq: "daily",
|
||||
priority: "0.75"
|
||||
}
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.xml { render layout: false }
|
||||
end
|
||||
|
||||
@@ -27,6 +27,11 @@ module Public
|
||||
end
|
||||
|
||||
def details
|
||||
if @team.tournament_broadcast? && @team.broadcast_tournament
|
||||
redirect_to public_tournament_path(@team.broadcast_tournament)
|
||||
return
|
||||
end
|
||||
|
||||
load_team_details!
|
||||
render :details
|
||||
end
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
module Public
|
||||
class TournamentAssignmentsController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_tournament
|
||||
|
||||
def destroy
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
unless @tournament.writable?
|
||||
redirect_to public_tournament_path(@tournament, tab: "dirette"),
|
||||
alert: t("flash.tournaments.archived_locked")
|
||||
return
|
||||
end
|
||||
assignment = @tournament.broadcast_assignments.find(params[:assignment_id])
|
||||
Tournaments::RevokeAssignment.call(assignment: assignment)
|
||||
redirect_to public_tournament_path(@tournament, tab: "dirette"),
|
||||
notice: t("flash.tournaments.assignment_revoked")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tournament
|
||||
@tournament = Tournament.find(params[:id])
|
||||
@club = @tournament.club
|
||||
require_club_owner!(@club)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
module Public
|
||||
class TournamentGroupsController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_tournament
|
||||
|
||||
def create
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
position = @tournament.groups.maximum(:position).to_i + 1
|
||||
name = params.dig(:tournament_group, :name).presence || "Girone #{('A'.ord + position).chr}"
|
||||
@tournament.groups.create!(name: name, position: position)
|
||||
redirect_to public_tournament_path(@tournament, tab: "struttura"), notice: t("flash.tournaments.group_added")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
end
|
||||
|
||||
def destroy
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
@tournament.groups.find(params[:group_id]).destroy!
|
||||
redirect_to public_tournament_path(@tournament, tab: "struttura"), notice: t("flash.tournaments.group_removed")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tournament
|
||||
@tournament = Tournament.find(params[:id])
|
||||
@club = @tournament.club
|
||||
require_club_owner!(@club)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,136 @@
|
||||
module Public
|
||||
class TournamentInvitationsController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_tournament
|
||||
|
||||
def create
|
||||
if params[:intent] == "save"
|
||||
save_draft
|
||||
return
|
||||
end
|
||||
|
||||
match_ids = Array(params[:match_ids]).reject(&:blank?)
|
||||
scope_kind = if match_ids.any?
|
||||
"matches"
|
||||
elsif params[:whole_court].present? || params[:scope_kind].to_s == "court_day"
|
||||
"court_day"
|
||||
else
|
||||
raise ArgumentError, t("tournaments.hub.stream_plan_need_selection")
|
||||
end
|
||||
persist_draft! unless calendar_invite?
|
||||
invitation, token = Tournaments::Invite.call(
|
||||
tournament: @tournament,
|
||||
email: params[:email],
|
||||
scope_kind: scope_kind,
|
||||
match_ids: match_ids,
|
||||
court: params[:court],
|
||||
on_date: params[:on_date].presence,
|
||||
invited_by: current_user,
|
||||
note: params[:note]
|
||||
)
|
||||
invite_url = public_invitation_url(token: token)
|
||||
flash[:invite_url] = invite_url unless calendar_invite?
|
||||
body_html = Tournaments::ComposeInviteEmail.call(
|
||||
html: draft_html,
|
||||
invite_url: invite_url,
|
||||
expires_on: I18n.l(invitation.expires_at.to_date, format: :long)
|
||||
)
|
||||
invitation.update!(email_html: body_html)
|
||||
begin
|
||||
Tournaments::InvitationMailer.transmission_invite(
|
||||
tournament: @tournament,
|
||||
invitation: invitation,
|
||||
invite_url: invite_url,
|
||||
invited_by: current_user,
|
||||
body_html: body_html
|
||||
).deliver_now
|
||||
flash[:notice] = t("flash.tournaments.invite_email_sent", email: invitation.email)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("[tournament_invite] #{e.class}: #{e.message}")
|
||||
flash[:alert] = t("flash.tournaments.invite_email_failed", email: invitation.email)
|
||||
end
|
||||
redirect_to public_tournament_path(@tournament, tab: invite_return_tab, anchor: calendar_invite? ? nil : "invito-generato")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
rescue ActiveRecord::RecordInvalid, ArgumentError => e
|
||||
redirect_to public_tournament_path(@tournament, tab: invite_return_tab), alert: e.message
|
||||
end
|
||||
|
||||
def save_draft
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
persist_draft!
|
||||
redirect_to public_tournament_path(@tournament, tab: "dirette"),
|
||||
notice: t("flash.tournaments.invite_draft_saved")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
redirect_to public_tournament_path(@tournament, tab: "dirette"), alert: e.message
|
||||
end
|
||||
|
||||
def upload_image
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
file = params[:file]
|
||||
unless file.respond_to?(:content_type) && file.content_type.to_s.in?(%w[image/png image/jpeg image/webp image/gif])
|
||||
return render json: { error: t("tournaments.hub.invite_image_invalid") }, status: :unprocessable_entity
|
||||
end
|
||||
if file.size > 2.megabytes
|
||||
return render json: { error: t("tournaments.hub.invite_image_too_big") }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@tournament.invite_email_images.attach(file)
|
||||
blob = @tournament.invite_email_images.blobs.last
|
||||
render json: { url: url_for(blob) }
|
||||
rescue Tournaments::EntitlementError => e
|
||||
render json: { error: e.message }, status: :forbidden
|
||||
end
|
||||
|
||||
def destroy
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
invitation = @tournament.broadcast_invitations.find(params[:invitation_id])
|
||||
invitation.destroy!
|
||||
redirect_to public_tournament_path(@tournament, tab: invite_return_tab), notice: t("flash.tournaments.invite_canceled")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tournament
|
||||
@tournament = Tournament.find(params[:id])
|
||||
@club = @tournament.club
|
||||
require_club_owner!(@club)
|
||||
end
|
||||
|
||||
def persist_draft!
|
||||
@tournament.update!(
|
||||
invite_draft_html: Tournaments::ComposeInviteEmail.sanitize_html(draft_html),
|
||||
invite_draft_note: params[:note].to_s.strip.presence,
|
||||
invite_draft_email: params[:email].to_s.strip.presence,
|
||||
invite_draft_saved_at: Time.current
|
||||
)
|
||||
end
|
||||
|
||||
def draft_html
|
||||
html = params[:email_html].to_s
|
||||
stripped = ActionController::Base.helpers.strip_tags(html).to_s.gsub(/\s+/, "")
|
||||
return html if stripped.present?
|
||||
|
||||
stored = @tournament.invite_draft_html.to_s
|
||||
if ActionController::Base.helpers.strip_tags(stored).to_s.gsub(/\s+/, "").present?
|
||||
return stored
|
||||
end
|
||||
|
||||
Tournaments::ComposeInviteEmail.default_html(tournament: @tournament, invited_by: current_user)
|
||||
end
|
||||
|
||||
def calendar_invite?
|
||||
params[:from].to_s == "calendar" || invite_return_tab == "calendario"
|
||||
end
|
||||
|
||||
def invite_return_tab
|
||||
tab = params[:tab].to_s
|
||||
tab = "dirette" if tab == "delega"
|
||||
tab.presence_in(%w[calendario dirette]) || "dirette"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
module Public
|
||||
class TournamentMatchesController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_tournament
|
||||
|
||||
def create
|
||||
Tournaments::ScheduleMatch.call(tournament: @tournament, attrs: match_params)
|
||||
redirect_to public_tournament_path(@tournament, tab: "calendario"), notice: t("flash.tournaments.match_scheduled")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
redirect_to public_tournament_path(@tournament, tab: "calendario"), alert: e.record.errors.full_messages.join(", ")
|
||||
end
|
||||
|
||||
def update
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
match = @tournament.matches.find(params[:match_id])
|
||||
attrs = match_params
|
||||
if match.played? || match.result_status.to_s.start_with?("walkover")
|
||||
attrs = attrs.except(:home_participant_id, :away_participant_id)
|
||||
end
|
||||
match.update!(attrs)
|
||||
schedule_changed = match.previous_changes.keys.intersect?(%w[scheduled_at court location])
|
||||
recorded = record_result_if_present!(match)
|
||||
notice = if recorded && !schedule_changed
|
||||
t("flash.tournaments.result_saved")
|
||||
else
|
||||
t("flash.tournaments.match_updated")
|
||||
end
|
||||
redirect_to public_tournament_path(@tournament, tab: hub_tab), notice: notice
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
redirect_to public_tournament_path(@tournament, tab: "calendario"), alert: e.record.errors.full_messages.join(", ")
|
||||
end
|
||||
|
||||
def swap
|
||||
match = @tournament.matches.find(params[:match_id])
|
||||
Tournaments::SwapSides.call(match: match)
|
||||
respond_to do |format|
|
||||
format.json { render json: { ok: true, matchup_label: match.reload.matchup_label } }
|
||||
format.html { redirect_to public_tournament_path(@tournament, tab: "calendario"), notice: t("flash.tournaments.sides_swapped") }
|
||||
end
|
||||
rescue Tournaments::EntitlementError => e
|
||||
respond_to do |format|
|
||||
format.json { render json: { ok: false, error: e.message }, status: :unprocessable_entity }
|
||||
format.html { redirect_to public_club_billing_path(@club), alert: e.message }
|
||||
end
|
||||
rescue Tournaments::SwapSides::LiveBroadcastError => e
|
||||
respond_to do |format|
|
||||
format.json { render json: { ok: false, error: e.message }, status: :unprocessable_entity }
|
||||
format.html { redirect_to public_tournament_path(@tournament, tab: "calendario"), alert: e.message }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
match = @tournament.matches.find(params[:match_id])
|
||||
unless match.deletable?
|
||||
redirect_to public_tournament_path(@tournament, tab: "calendario"),
|
||||
alert: t("flash.matches.close_live_before_delete")
|
||||
return
|
||||
end
|
||||
|
||||
match.destroy!
|
||||
redirect_to public_tournament_path(@tournament, tab: "calendario"), notice: t("flash.tournaments.match_deleted")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tournament
|
||||
@tournament = Tournament.find(params[:id])
|
||||
@club = @tournament.club
|
||||
require_club_owner!(@club)
|
||||
end
|
||||
|
||||
def record_result_if_present!(match)
|
||||
home = params[:home_score]
|
||||
away = params[:away_score]
|
||||
return false if home.blank? || away.blank?
|
||||
|
||||
Tournaments::RecordResult.call(match: match, home_score: home, away_score: away, source: "manual")
|
||||
true
|
||||
end
|
||||
|
||||
def match_params
|
||||
p = params.require(:match).permit(
|
||||
:home_participant_id, :away_participant_id, :tournament_group_id,
|
||||
:tournament_round_id, :court, :location, :scheduled_at
|
||||
)
|
||||
%i[home_participant_id away_participant_id].each do |key|
|
||||
p[key] = p[key].presence if p.key?(key)
|
||||
end
|
||||
p[:scheduled_at] = parse_scheduled_at(p[:scheduled_at]) if p[:scheduled_at].present?
|
||||
p
|
||||
end
|
||||
|
||||
def parse_scheduled_at(value)
|
||||
raw = value.to_s.strip
|
||||
return nil if raw.blank?
|
||||
|
||||
Time.zone.strptime(raw, "%Y-%m-%dT%H:%M")
|
||||
rescue ArgumentError
|
||||
Time.zone.parse(raw)
|
||||
end
|
||||
|
||||
def hub_tab
|
||||
params[:tab].to_s.presence_in(%w[squadre struttura calendario dirette tabellone]) || "calendario"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
module Public
|
||||
class TournamentPagesController < SiteBaseController
|
||||
include Public::LiveHelper
|
||||
|
||||
layout "marketing_live"
|
||||
|
||||
def index
|
||||
@tournaments = Tournament
|
||||
.visible_to_public
|
||||
.with_attached_logo_file
|
||||
.includes(:club)
|
||||
.order(starts_on: :desc, name: :asc)
|
||||
end
|
||||
|
||||
def show
|
||||
@tournament = Tournament.find_by!(slug: params[:slug])
|
||||
unless @tournament.published? || owner_access?
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
||||
@club = @tournament.club
|
||||
@owner_preview = !@tournament.published? && owner_access?
|
||||
@owner_manage = owner_access?
|
||||
@tab = params[:tab].to_s.presence_in(%w[risultati tabellone]) || "risultati"
|
||||
load_public_content!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def owner_access?
|
||||
logged_in? && @tournament.club.owned_by?(current_user)
|
||||
end
|
||||
|
||||
def load_public_content!
|
||||
@online_paths = fetch_online_paths
|
||||
|
||||
@matches = @tournament.matches
|
||||
.includes(
|
||||
:tournament_group,
|
||||
:tournament_round,
|
||||
home_participant: { logo_file_attachment: :blob },
|
||||
away_participant: { logo_file_attachment: :blob }
|
||||
)
|
||||
.order(Arel.sql("scheduled_at ASC NULLS LAST"), created_at: :asc)
|
||||
|
||||
@live_sessions = StreamSession
|
||||
.broadcasting
|
||||
.publicly_listed
|
||||
.where(platform: "matchlivetv")
|
||||
.includes(:score_state, match: [:home_participant, :away_participant, { team: :club }])
|
||||
.where(match_id: @tournament.matches.select(:id))
|
||||
.order(Arel.sql("started_at DESC NULLS LAST"), created_at: :desc)
|
||||
@live_by_match_id = @live_sessions.index_by(&:match_id)
|
||||
|
||||
@recordings = Recording.ready.publicly_listed
|
||||
.joins(stream_session: :match)
|
||||
.where(matches: { tournament_id: @tournament.id })
|
||||
.includes(stream_session: { match: [:home_participant, :away_participant] })
|
||||
.order(recorded_at: :desc)
|
||||
@recording_by_match_id = {}
|
||||
@recordings.each do |rec|
|
||||
match_id = rec.stream_session.match_id
|
||||
@recording_by_match_id[match_id] ||= rec
|
||||
end
|
||||
|
||||
@groups = @tournament.groups.order(:position)
|
||||
@standings_by_group = @groups.index_with { |group| Tournaments::Standings.call(group) }
|
||||
@rounds = @tournament.rounds.order(:position)
|
||||
@matches_by_round = @matches.select { |match| match.tournament_round_id.present? }
|
||||
.group_by(&:tournament_round_id)
|
||||
end
|
||||
|
||||
def fetch_online_paths
|
||||
Mediamtx::Client.new.online_path_names
|
||||
rescue Mediamtx::Client::Error, Errno::ECONNREFUSED, SocketError
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
module Public
|
||||
class TournamentParticipantsController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_tournament
|
||||
|
||||
def create
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
@tournament.participants.create!(participant_params)
|
||||
redirect_to public_tournament_path(@tournament, tab: "squadre"), notice: t("flash.tournaments.participant_added")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
redirect_to public_tournament_path(@tournament, tab: "squadre"), alert: e.record.errors.full_messages.join(", ")
|
||||
end
|
||||
|
||||
def update
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
participant = @tournament.participants.find(params[:participant_id])
|
||||
participant.update!(participant_params)
|
||||
file = params.dig(:tournament_participant, :logo_file)
|
||||
participant.logo_file.attach(file) if file.present?
|
||||
redirect_to public_tournament_path(@tournament, tab: "squadre"), notice: t("flash.tournaments.participant_updated")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
redirect_to public_tournament_path(@tournament, tab: "squadre"), alert: e.record.errors.full_messages.join(", ")
|
||||
end
|
||||
|
||||
def destroy
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
@tournament.participants.find(params[:participant_id]).destroy!
|
||||
redirect_to public_tournament_path(@tournament, tab: "squadre"), notice: t("flash.tournaments.participant_removed")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tournament
|
||||
@tournament = Tournament.find(params[:id])
|
||||
@club = @tournament.club
|
||||
require_club_owner!(@club)
|
||||
end
|
||||
|
||||
def participant_params
|
||||
permitted = params.require(:tournament_participant).permit(:name, :group_id, :primary_color, :logo_url, :source_team_id)
|
||||
permitted[:group_id] = permitted[:group_id].presence if permitted.key?(:group_id)
|
||||
permitted
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
module Public
|
||||
class TournamentResultsController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_tournament
|
||||
|
||||
def create
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
match = @tournament.matches.find(params[:match_id])
|
||||
Tournaments::RecordResult.call(
|
||||
match: match,
|
||||
home_score: params[:home_score],
|
||||
away_score: params[:away_score],
|
||||
source: "manual",
|
||||
walkover: params[:walkover].presence
|
||||
)
|
||||
redirect_to public_tournament_path(@tournament, tab: params[:tab].presence || "calendario"),
|
||||
notice: t("flash.tournaments.result_saved")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
redirect_to public_tournament_path(@tournament, tab: "calendario"), alert: e.record.errors.full_messages.join(", ")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tournament
|
||||
@tournament = Tournament.find(params[:id])
|
||||
@club = @tournament.club
|
||||
require_club_owner!(@club)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,190 @@
|
||||
module Public
|
||||
class TournamentsController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_club, only: %i[index new create]
|
||||
before_action :set_tournament, except: %i[index new create]
|
||||
before_action :require_owner!
|
||||
before_action :require_writable!, only: %i[update generate_group_matches propose_knockout]
|
||||
|
||||
def index
|
||||
@tournaments = @club.tournaments.order(starts_on: :desc)
|
||||
@can_create = Tournaments::Entitlements.new(@club).premium_full?
|
||||
end
|
||||
|
||||
def new
|
||||
assert_full!
|
||||
return if performed?
|
||||
|
||||
@tournament = @club.tournaments.build(
|
||||
sport_key: Sports::Catalog.normalize_key(@club.sport),
|
||||
starts_on: Date.current,
|
||||
ends_on: Date.current + 1,
|
||||
format_kind: "mixed",
|
||||
courts: ["Campo 1", "Campo 2"],
|
||||
knockout_size: 4
|
||||
)
|
||||
end
|
||||
|
||||
def create
|
||||
@tournament = Tournaments::Create.call(club: @club, attrs: tournament_params)
|
||||
redirect_to public_tournament_path(@tournament), notice: t("flash.tournaments.created")
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@tournament = e.record
|
||||
flash.now[:alert] = e.record.errors.full_messages.join(", ")
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def show
|
||||
if params[:tab] == "delega"
|
||||
redirect_to public_tournament_path(@tournament, tab: "dirette")
|
||||
return
|
||||
end
|
||||
|
||||
load_hub!
|
||||
end
|
||||
|
||||
def update
|
||||
attrs = tournament_params
|
||||
file = attrs.delete(:logo_file)
|
||||
@tournament.assign_attributes(attrs)
|
||||
@tournament.logo_file.attach(file) if file.present?
|
||||
@tournament.save!
|
||||
redirect_to public_tournament_path(@tournament, tab: hub_tab || params[:tab]), notice: t("flash.tournaments.updated")
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
load_hub!
|
||||
flash.now[:alert] = e.record.errors.full_messages.join(", ")
|
||||
render :show, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def publish
|
||||
assert_full!
|
||||
return if performed?
|
||||
|
||||
@tournament.update!(status: "published")
|
||||
redirect_to public_tournament_path(@tournament), notice: t("flash.tournaments.published")
|
||||
end
|
||||
|
||||
def unpublish
|
||||
assert_full!
|
||||
return if performed?
|
||||
|
||||
@tournament.update!(status: "draft")
|
||||
redirect_to public_tournament_path(@tournament), notice: t("flash.tournaments.unpublished")
|
||||
end
|
||||
|
||||
def archive
|
||||
assert_full!
|
||||
return if performed?
|
||||
|
||||
@tournament.update!(status: "archived")
|
||||
redirect_to public_tournament_path(@tournament), notice: t("flash.tournaments.archived")
|
||||
end
|
||||
|
||||
def destroy
|
||||
Tournaments::Destroy.call(tournament: @tournament)
|
||||
redirect_to public_club_tournaments_path(@club), notice: t("flash.tournaments.deleted")
|
||||
rescue Tournaments::Destroy::LiveBroadcastError => e
|
||||
redirect_to public_tournament_path(@tournament), alert: e.message
|
||||
end
|
||||
|
||||
def generate_group_matches
|
||||
created = Tournaments::GenerateGroupMatches.call(tournament: @tournament)
|
||||
redirect_to public_tournament_path(@tournament, tab: "calendario"),
|
||||
notice: t("flash.tournaments.group_matches_created", count: created.size)
|
||||
rescue Tournaments::EntitlementError, ActiveRecord::RecordInvalid => e
|
||||
redirect_to public_tournament_path(@tournament, tab: "calendario"), alert: e.message
|
||||
end
|
||||
|
||||
def propose_knockout
|
||||
updated = Tournaments::ProposeKnockout.call(@tournament)
|
||||
redirect_to public_tournament_path(@tournament, tab: "tabellone"),
|
||||
notice: t("flash.tournaments.knockout_proposed", count: updated.size)
|
||||
rescue Tournaments::EntitlementError, ActiveRecord::RecordInvalid => e
|
||||
redirect_to public_tournament_path(@tournament, tab: "tabellone"), alert: e.message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_club
|
||||
@club = Club.find(params[:club_id] || params[:id])
|
||||
end
|
||||
|
||||
def set_tournament
|
||||
@tournament = Tournament.find(params[:id])
|
||||
@club = @tournament.club
|
||||
end
|
||||
|
||||
def require_owner!
|
||||
require_club_owner!(@club)
|
||||
end
|
||||
|
||||
def assert_full!
|
||||
Tournaments::Entitlements.new(@club).assert_writable!
|
||||
rescue Tournaments::EntitlementError => e
|
||||
redirect_to public_club_billing_path(@club), alert: e.message
|
||||
end
|
||||
|
||||
def require_writable!
|
||||
assert_full!
|
||||
return if performed?
|
||||
return if @tournament.writable?
|
||||
|
||||
redirect_to public_tournament_path(@tournament), alert: t("flash.tournaments.archived_locked")
|
||||
end
|
||||
|
||||
def load_hub!
|
||||
@tab = hub_tab || "squadre"
|
||||
@participants = @tournament.participants.with_attached_logo_file.includes(:group, :source_team).order(:position, :name)
|
||||
@groups = @tournament.groups.order(:position)
|
||||
@rounds = @tournament.rounds.order(:position)
|
||||
@matches = @tournament.matches.includes(
|
||||
{ home_participant: { logo_file_attachment: :blob } },
|
||||
{ away_participant: { logo_file_attachment: :blob } },
|
||||
:tournament_group, :tournament_round, :stream_sessions,
|
||||
{ broadcast_assignments: :user }
|
||||
).order(:scheduled_at)
|
||||
@invitations = @tournament.broadcast_invitations.order(created_at: :desc)
|
||||
@pending_invites_by_match_id = pending_invites_by_match_id
|
||||
@standings_by_group = @groups.index_with { |group| Tournaments::Standings.call(group) }
|
||||
@overlap_warnings = overlap_warnings
|
||||
@writable = @tournament.writable? && Tournaments::Entitlements.new(@club).premium_full?
|
||||
end
|
||||
|
||||
def hub_tab
|
||||
raw = params[:tab].to_s
|
||||
raw = "dirette" if raw == "delega"
|
||||
raw.presence_in(%w[squadre struttura calendario dirette tabellone])
|
||||
end
|
||||
|
||||
def pending_invites_by_match_id
|
||||
pending = @tournament.broadcast_invitations.pending.to_a
|
||||
map = Hash.new { |h, k| h[k] = [] }
|
||||
@matches.each do |match|
|
||||
pending.each do |invitation|
|
||||
map[match.id] << invitation if invitation.covers_match?(match)
|
||||
end
|
||||
end
|
||||
map
|
||||
end
|
||||
|
||||
def overlap_warnings
|
||||
limit = @tournament.concurrent_limit
|
||||
return [] unless limit
|
||||
|
||||
@matches.group_by { |m| m.scheduled_at&.strftime("%Y-%m-%d %H:%M") }.filter_map do |slot, list|
|
||||
next if slot.blank? || list.size <= limit
|
||||
|
||||
t("tournaments.hub.overlap_warning", slot: slot, count: list.size, limit: limit)
|
||||
end
|
||||
end
|
||||
|
||||
def tournament_params
|
||||
params.require(:tournament).permit(
|
||||
:name, :sport_key, :venue, :starts_on, :ends_on, :format_kind,
|
||||
:description, :knockout_size, :courts, :logo_file
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user