Merge branch 'codex/tur-117-match-filters' into 'master'
TUR-117: Add match endpoint filters See merge request turniere/turniere-backend!60
This commit is contained in:
commit
244952093c
|
|
@ -2,10 +2,28 @@
|
|||
|
||||
class TournamentMatchesChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
tournament = Tournament.find(params[:tournament_id])
|
||||
state = params[:state].presence
|
||||
@tournament_id = params[:tournament_id]
|
||||
@filters = match_filters
|
||||
tournament = Tournament.find(@tournament_id)
|
||||
|
||||
stream_for TournamentMatchesBroadcast.stream_target(tournament, state:)
|
||||
transmit TournamentMatchesBroadcastPayload.build(tournament, state:)
|
||||
stream_for TournamentMatchesBroadcast.stream_target(tournament) do
|
||||
transmit_current_payload
|
||||
end
|
||||
transmit TournamentMatchesBroadcastPayload.build(tournament, @filters)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :tournament_id, :filters
|
||||
|
||||
def match_filters
|
||||
params.slice(:state, :stage_id, :stage_level, :position, :group_stage_position)
|
||||
end
|
||||
|
||||
def transmit_current_payload
|
||||
tournament = TournamentMatchesBroadcast.load_tournament(tournament_id)
|
||||
return if tournament.nil?
|
||||
|
||||
transmit TournamentMatchesBroadcastPayload.build(tournament, filters)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class MatchesController < ApplicationController
|
|||
|
||||
# GET/tournaments/1/matches
|
||||
def index
|
||||
render json: TournamentMatchesPayload.build(@tournament, state: match_params['state'])
|
||||
render json: TournamentMatchesPayload.build(@tournament, match_index_params)
|
||||
end
|
||||
|
||||
# GET /matches/1
|
||||
|
|
@ -149,6 +149,10 @@ class MatchesController < ApplicationController
|
|||
params.slice(:state).permit!
|
||||
end
|
||||
|
||||
def match_index_params
|
||||
params.slice(:state, :stage_id, :stage_level, :position, :group_stage_position).permit!
|
||||
end
|
||||
|
||||
def create_match_params
|
||||
params.slice(:team_ids).permit(team_ids: [])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,27 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TournamentMatchesBroadcast
|
||||
STATES = [nil, 'upcoming', *Match.states.keys].freeze
|
||||
|
||||
class << self
|
||||
def call(tournament_id)
|
||||
tournament = load_tournament(tournament_id)
|
||||
return if tournament.nil?
|
||||
|
||||
STATES.each do |state|
|
||||
TournamentMatchesChannel.broadcast_to(
|
||||
stream_target(tournament, state:),
|
||||
TournamentMatchesBroadcastPayload.build(tournament, state:)
|
||||
)
|
||||
end
|
||||
TournamentMatchesChannel.broadcast_to(
|
||||
stream_target(tournament),
|
||||
{ type: 'tournament_matches.changed', tournament_id: tournament.id }
|
||||
)
|
||||
end
|
||||
|
||||
def stream_target(tournament, state:)
|
||||
"#{tournament.to_gid_param}:matches:#{state.presence || 'all'}"
|
||||
def stream_target(tournament)
|
||||
"#{tournament.to_gid_param}:matches"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_tournament(tournament_id)
|
||||
Tournament.includes(
|
||||
stages: [
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
class TournamentMatchesBroadcastPayload
|
||||
class << self
|
||||
def build(tournament, state: nil)
|
||||
def build(tournament, filters = {}, state: nil)
|
||||
filters = TournamentMatchesPayload.normalize_filters(filters, state:)
|
||||
{
|
||||
type: 'tournament_matches.updated',
|
||||
tournament_id: tournament.id,
|
||||
state:,
|
||||
matches: TournamentMatchesPayload.build(tournament, state:)
|
||||
state: filters[:state],
|
||||
matches: TournamentMatchesPayload.build(tournament, filters)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,18 +2,77 @@
|
|||
|
||||
class TournamentMatchesPayload
|
||||
class << self
|
||||
def build(tournament, state: nil)
|
||||
serialized_matches(matches_for(tournament, state:))
|
||||
def build(tournament, filters = {}, state: nil, **filter_keywords)
|
||||
serialized_matches(matches_for(tournament, filters.merge(filter_keywords), state:))
|
||||
end
|
||||
|
||||
def matches_for(tournament, state: nil)
|
||||
def matches_for(tournament, filters = {}, state: nil, **filter_keywords)
|
||||
filters = normalize_filters(filters.merge(filter_keywords), state:)
|
||||
matches = state_filtered_matches(tournament, filters[:state])
|
||||
|
||||
matches.select { |match| match_matches_filters?(match, filters) }
|
||||
end
|
||||
|
||||
def normalize_filters(filters = {}, state: nil)
|
||||
raw_filters = filters.to_h.with_indifferent_access
|
||||
raw_filters[:state] = state unless state.nil?
|
||||
|
||||
{
|
||||
state: raw_filters[:state].presence,
|
||||
stage_id: integer_filter(raw_filters[:stage_id]),
|
||||
stage_level: integer_filter(raw_filters[:stage_level]),
|
||||
position: integer_filter(raw_filters[:position]),
|
||||
group_stage_position: integer_filter(raw_filters[:group_stage_position])
|
||||
}.compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def state_filtered_matches(tournament, state)
|
||||
return tournament.matches if state.blank?
|
||||
return upcoming_matches_for(tournament) if state == 'upcoming'
|
||||
|
||||
tournament.matches.select { |match| match.state == state }
|
||||
end
|
||||
|
||||
private
|
||||
def match_matches_filters?(match, filters)
|
||||
stage = match_stage(match)
|
||||
|
||||
stage_id_matches?(stage, filters) &&
|
||||
stage_level_matches?(stage, filters) &&
|
||||
position_matches?(match, filters) &&
|
||||
group_stage_position_matches?(match, stage, filters)
|
||||
end
|
||||
|
||||
def match_stage(match)
|
||||
match.stage || match.group&.stage
|
||||
end
|
||||
|
||||
def stage_id_matches?(stage, filters)
|
||||
filters[:stage_id].nil? || stage&.id == filters[:stage_id]
|
||||
end
|
||||
|
||||
def stage_level_matches?(stage, filters)
|
||||
filters[:stage_level].nil? || stage&.level == filters[:stage_level]
|
||||
end
|
||||
|
||||
def position_matches?(match, filters)
|
||||
filters[:position].nil? || match.position == filters[:position]
|
||||
end
|
||||
|
||||
def group_stage_position_matches?(match, stage, filters)
|
||||
filters[:group_stage_position].nil? ||
|
||||
(match.group_match? && stage&.level == -1 && match.position == filters[:group_stage_position])
|
||||
end
|
||||
|
||||
def integer_filter(value)
|
||||
return nil if value.blank?
|
||||
return value if value.is_a?(Integer)
|
||||
|
||||
Integer(value, 10)
|
||||
rescue ArgumentError, TypeError
|
||||
:invalid
|
||||
end
|
||||
|
||||
def upcoming_matches_for(tournament)
|
||||
upcoming_group_matches(tournament).presence || upcoming_playoff_matches(tournament)
|
||||
|
|
|
|||
|
|
@ -61,11 +61,15 @@ module TurniereE2E
|
|||
end
|
||||
end
|
||||
|
||||
def subscribe_matches!(tournament_id:, state: nil)
|
||||
def subscribe_matches!(tournament_id:, **filters)
|
||||
identifier = {
|
||||
channel: 'TournamentMatchesChannel',
|
||||
tournament_id:,
|
||||
state:
|
||||
state: filters[:state],
|
||||
stage_id: filters[:stage_id],
|
||||
stage_level: filters[:stage_level],
|
||||
position: filters[:position],
|
||||
group_stage_position: filters[:group_stage_position]
|
||||
}.compact.to_json
|
||||
|
||||
subscribe!(identifier:) do |message|
|
||||
|
|
|
|||
|
|
@ -50,6 +50,27 @@ RSpec.describe MatchesController, type: :controller do
|
|||
expect(match.keys).to match_array(%i[id position state teams match_scores group stage])
|
||||
expect(match[:teams]).to all(include(:id, :name))
|
||||
end
|
||||
|
||||
it 'filters group stage matches by position across groups' do
|
||||
get :index, params: { tournament_id: @tournament.to_param, group_stage_position: 0 }
|
||||
body = deserialize_response(response)
|
||||
|
||||
expect(body).not_to be_empty
|
||||
expect(body).to all(include(position: 0))
|
||||
expect(body.map { |match| match.dig(:stage, :level) }.uniq).to eq([-1])
|
||||
end
|
||||
end
|
||||
|
||||
context 'on a playoff tournament' do
|
||||
it 'filters matches by stage level' do
|
||||
stage = @tournament.stages.find_by!(level: 1)
|
||||
|
||||
get :index, params: { tournament_id: @tournament.to_param, stage_level: stage.level }
|
||||
body = deserialize_response(response)
|
||||
|
||||
expect(body.map { |match| match.dig(:stage, :level) }.uniq).to eq([stage.level])
|
||||
expect(body.map { |match| match[:id] }).to match_array(stage.matches.map(&:id))
|
||||
end
|
||||
end
|
||||
# TODO add test for upcoming once there is test data for a "valid" group stage
|
||||
end
|
||||
|
|
|
|||
|
|
@ -203,6 +203,14 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
matches_all = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches")
|
||||
matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'not_started' })
|
||||
matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
||||
matches_group_position = anonymous_client.get(
|
||||
"/tournaments/#{tournament.fetch(:id)}/matches",
|
||||
params: { group_stage_position: match.fetch(:position) }
|
||||
)
|
||||
matches_group_stage = anonymous_client.get(
|
||||
"/tournaments/#{tournament.fetch(:id)}/matches",
|
||||
params: { stage_level: group_stage.fetch(:level) }
|
||||
)
|
||||
match_show = anonymous_client.get("/matches/#{match.fetch(:id)}")
|
||||
invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' })
|
||||
forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
||||
|
|
@ -271,6 +279,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
expect(matches_not_started[:json].map { |item| item[:id] }).to include(match.fetch(:id))
|
||||
expect(matches_upcoming[:status]).to eq(200)
|
||||
expect(matches_upcoming[:json].map { |item| item[:id] }).to include(match.fetch(:id))
|
||||
expect(matches_group_position[:status]).to eq(200)
|
||||
expect(matches_group_position[:json].map { |item| item.dig(:stage, :level) }.uniq).to eq([-1])
|
||||
expect(matches_group_position[:json]).to all(include(position: match.fetch(:position)))
|
||||
expect(matches_group_stage[:status]).to eq(200)
|
||||
expect(matches_group_stage[:json].count).to eq(tournament_match_count(tournament))
|
||||
expect(matches_group_stage[:json].map { |item| item.dig(:stage, :id) }.uniq).to eq([group_stage.fetch(:id)])
|
||||
expect(match_show[:status]).to eq(200)
|
||||
expect(invalid_match_update[:status]).to eq(422)
|
||||
expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state')
|
||||
|
|
|
|||
|
|
@ -29,8 +29,20 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
|||
|
||||
tournament_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||
matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||
filtered_matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||
stage_matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||
group_position_filter = { group_stage_position: match.fetch(:position) }
|
||||
stage_filter = { stage_level: group_stage.fetch(:level) }
|
||||
tournament_cable.subscribe_tournament!(tournament_id: tournament.fetch(:id))
|
||||
matches_cable.subscribe_matches!(tournament_id: tournament.fetch(:id))
|
||||
filtered_matches_cable.subscribe_matches!(
|
||||
tournament_id: tournament.fetch(:id),
|
||||
**group_position_filter
|
||||
)
|
||||
stage_matches_cable.subscribe_matches!(
|
||||
tournament_id: tournament.fetch(:id),
|
||||
**stage_filter
|
||||
)
|
||||
|
||||
expect(tournament_cable.wait_for_tournament_payload!.fetch('tournament')).to(
|
||||
eq(json_for_api(fetch_tournament(tournament.fetch(:id))))
|
||||
|
|
@ -38,6 +50,12 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
|||
expect(matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
||||
eq(json_for_api(fetch_matches(tournament.fetch(:id))))
|
||||
)
|
||||
expect(filtered_matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
||||
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: group_position_filter)))
|
||||
)
|
||||
expect(stage_matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
||||
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: stage_filter)))
|
||||
)
|
||||
|
||||
play_match!(match.fetch(:id), first_score: 3, second_score: 1)
|
||||
|
||||
|
|
@ -55,9 +73,29 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
|||
|
||||
matches_payload = wait_for_matches_payload!(matches_cable, match_id: match.fetch(:id), state: 'finished')
|
||||
expect(matches_payload.fetch('matches')).to eq(json_for_api(fetch_matches(tournament.fetch(:id))))
|
||||
|
||||
filtered_matches_payload = wait_for_matches_payload!(
|
||||
filtered_matches_cable,
|
||||
match_id: match.fetch(:id),
|
||||
state: 'finished'
|
||||
)
|
||||
expect(filtered_matches_payload.fetch('matches')).to(
|
||||
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: group_position_filter)))
|
||||
)
|
||||
|
||||
stage_matches_payload = wait_for_matches_payload!(
|
||||
stage_matches_cable,
|
||||
match_id: match.fetch(:id),
|
||||
state: 'finished'
|
||||
)
|
||||
expect(stage_matches_payload.fetch('matches')).to(
|
||||
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: stage_filter)))
|
||||
)
|
||||
ensure
|
||||
tournament_cable&.close
|
||||
matches_cable&.close
|
||||
filtered_matches_cable&.close
|
||||
stage_matches_cable&.close
|
||||
end
|
||||
|
||||
def login_client
|
||||
|
|
@ -111,8 +149,8 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
|||
response.fetch(:json)
|
||||
end
|
||||
|
||||
def fetch_matches(tournament_id)
|
||||
response = TurniereE2E::ApiClient.new(base_url:).get("/tournaments/#{tournament_id}/matches")
|
||||
def fetch_matches(tournament_id, params: {})
|
||||
response = TurniereE2E::ApiClient.new(base_url:).get("/tournaments/#{tournament_id}/matches", params:)
|
||||
expect(response[:status]).to eq(200)
|
||||
response.fetch(:json)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,6 +34,29 @@ RSpec.describe 'Tournament live payloads' do
|
|||
expect(serialized_match[:teams].map { |team| team[:id] }).to match_array(match.teams.map(&:id))
|
||||
expect(serialized_match[:match_scores].map { |score| score[:team][:id] }).to match_array(match.teams.map(&:id))
|
||||
end
|
||||
|
||||
it 'filters group stage matches by position across groups' do
|
||||
tournament = create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match)
|
||||
tournament.group_stage.groups.each do |group|
|
||||
group.matches.each_with_index { |match, index| match.update!(position: index) }
|
||||
end
|
||||
|
||||
matches = described_class.matches_for(tournament, group_stage_position: 0)
|
||||
|
||||
expect(matches).to all(be_group_match)
|
||||
expect(matches.map(&:position).uniq).to eq([0])
|
||||
expect(matches.map { |match| match.group.stage.level }.uniq).to eq([-1])
|
||||
expect(matches.map(&:group_id)).to match_array(tournament.group_stage.groups.map(&:id))
|
||||
end
|
||||
|
||||
it 'filters all matches belonging to a stage level' do
|
||||
tournament = create(:stage_tournament, stage_count: 2)
|
||||
stage = tournament.stages.find_by!(level: 1)
|
||||
|
||||
matches = described_class.matches_for(tournament, stage_level: stage.level)
|
||||
|
||||
expect(matches).to match_array(stage.matches)
|
||||
end
|
||||
end
|
||||
|
||||
describe TournamentBroadcastPayload do
|
||||
|
|
|
|||
Loading…
Reference in New Issue