add backend beamer
This commit is contained in:
parent
5d69f4fb07
commit
85fce7faac
|
|
@ -0,0 +1,69 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BeamersController < ApplicationController
|
||||||
|
before_action :set_tournament
|
||||||
|
before_action :set_beamer, only: %i[show update destroy]
|
||||||
|
before_action :authenticate_user!, only: %i[create update destroy]
|
||||||
|
before_action -> { require_owner! @tournament.owner }, only: %i[create update destroy]
|
||||||
|
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
||||||
|
|
||||||
|
# GET /tournaments/:tournament_id/beamers
|
||||||
|
def index
|
||||||
|
render json: @tournament.beamers, each_serializer: BeamerSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /tournaments/:tournament_id/beamers/:id
|
||||||
|
def show
|
||||||
|
render json: @beamer
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /tournaments/:tournament_id/beamers/by_name?name=beamername
|
||||||
|
def by_name
|
||||||
|
beamer = @tournament.beamers.find_by(name: params[:name])
|
||||||
|
beamer ||= @tournament.beamers.default_beamer.first
|
||||||
|
if beamer
|
||||||
|
render json: beamer
|
||||||
|
else
|
||||||
|
render json: { error: 'No beamer found' }, status: :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /tournaments/:tournament_id/beamers
|
||||||
|
def create
|
||||||
|
beamer = @tournament.beamers.new(beamer_params)
|
||||||
|
if beamer.save
|
||||||
|
render json: beamer, status: :created
|
||||||
|
else
|
||||||
|
render json: beamer.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# PATCH /tournaments/:tournament_id/beamers/:id
|
||||||
|
def update
|
||||||
|
if @beamer.update(beamer_params)
|
||||||
|
render json: @beamer
|
||||||
|
else
|
||||||
|
render json: @beamer.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /tournaments/:tournament_id/beamers/:id
|
||||||
|
def destroy
|
||||||
|
@beamer.destroy
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_tournament
|
||||||
|
@tournament = Tournament.find(params[:tournament_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_beamer
|
||||||
|
@beamer = @tournament.beamers.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def beamer_params
|
||||||
|
params.permit(:name, :display_state, :is_default, :qr_code_url, :logo_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Beamer < ApplicationRecord
|
||||||
|
DISPLAY_STATES = %w[
|
||||||
|
matches_all matches_in_progress matches_upcoming matches_not_started
|
||||||
|
matches_finished groups between_games during_game
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
belongs_to :tournament
|
||||||
|
|
||||||
|
validates :name, presence: true, uniqueness: { scope: :tournament_id }
|
||||||
|
validates :display_state, presence: true, inclusion: { in: DISPLAY_STATES }
|
||||||
|
|
||||||
|
scope :default_beamer, -> { where(is_default: true) }
|
||||||
|
|
||||||
|
delegate :owner, to: :tournament
|
||||||
|
end
|
||||||
|
|
@ -6,6 +6,7 @@ class Tournament < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
has_many :teams, dependent: :destroy
|
has_many :teams, dependent: :destroy
|
||||||
has_many :stages, dependent: :destroy
|
has_many :stages, dependent: :destroy
|
||||||
|
has_many :beamers, dependent: :destroy
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validates :code, presence: true, uniqueness: true
|
validates :code, presence: true, uniqueness: true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BeamerSerializer < ApplicationSerializer
|
||||||
|
attributes :name, :display_state, :is_default, :qr_code_url, :logo_url
|
||||||
|
end
|
||||||
|
|
@ -17,6 +17,11 @@ Rails.application.routes.draw do
|
||||||
resources :tournaments do
|
resources :tournaments do
|
||||||
resources :statistics, only: %i[index]
|
resources :statistics, only: %i[index]
|
||||||
resources :matches, only: %i[index]
|
resources :matches, only: %i[index]
|
||||||
|
resources :beamers, only: %i[index show create update destroy] do
|
||||||
|
collection do
|
||||||
|
get :by_name
|
||||||
|
end
|
||||||
|
end
|
||||||
member do
|
member do
|
||||||
get :timer_end
|
get :timer_end
|
||||||
patch :set_timer_end
|
patch :set_timer_end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateBeamers < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :beamers do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.string :display_state, null: false, default: 'matches_all'
|
||||||
|
t.boolean :is_default, default: false
|
||||||
|
t.string :qr_code_url
|
||||||
|
t.string :logo_url
|
||||||
|
t.belongs_to :tournament, index: true, foreign_key: { on_delete: :cascade }, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
add_index :beamers, %i[tournament_id name], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe BeamersController, type: :controller do
|
||||||
|
before do
|
||||||
|
@tournament = create(:tournament)
|
||||||
|
@owner = @tournament.owner
|
||||||
|
@beamer = create(:beamer, tournament: @tournament)
|
||||||
|
@default_beamer = create(:default_beamer, tournament: @tournament)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
it 'returns all beamers for the tournament' do
|
||||||
|
get :index, params: { tournament_id: @tournament.to_param }
|
||||||
|
expect(response).to be_successful
|
||||||
|
body = deserialize_response response
|
||||||
|
expect(body.length).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #show' do
|
||||||
|
it 'returns the beamer' do
|
||||||
|
get :show, params: { tournament_id: @tournament.to_param, id: @beamer.to_param }
|
||||||
|
expect(response).to be_successful
|
||||||
|
body = deserialize_response response
|
||||||
|
expect(body[:name]).to eq(@beamer.name)
|
||||||
|
expect(body[:display_state]).to eq(@beamer.display_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 for nonexistent beamer' do
|
||||||
|
get :show, params: { tournament_id: @tournament.to_param, id: 999_999 }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #by_name' do
|
||||||
|
it 'returns beamer by name' do
|
||||||
|
get :by_name, params: { tournament_id: @tournament.to_param, name: @beamer.name }
|
||||||
|
expect(response).to be_successful
|
||||||
|
body = deserialize_response response
|
||||||
|
expect(body[:name]).to eq(@beamer.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'falls back to default beamer when name not found' do
|
||||||
|
get :by_name, params: { tournament_id: @tournament.to_param, name: 'nonexistent' }
|
||||||
|
expect(response).to be_successful
|
||||||
|
body = deserialize_response response
|
||||||
|
expect(body[:name]).to eq(@default_beamer.name)
|
||||||
|
expect(body[:is_default]).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'falls back to default beamer when name param is missing' do
|
||||||
|
get :by_name, params: { tournament_id: @tournament.to_param }
|
||||||
|
expect(response).to be_successful
|
||||||
|
body = deserialize_response response
|
||||||
|
expect(body[:is_default]).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when no beamers exist at all' do
|
||||||
|
empty_tournament = create(:tournament)
|
||||||
|
get :by_name, params: { tournament_id: empty_tournament.to_param, name: 'anything' }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
let(:valid_params) do
|
||||||
|
{ name: 'new-beamer', display_state: 'groups', is_default: false }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as owner' do
|
||||||
|
before(:each) { apply_authentication_headers_for @owner }
|
||||||
|
|
||||||
|
it 'creates a new beamer' do
|
||||||
|
expect {
|
||||||
|
post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params)
|
||||||
|
}.to change(@tournament.beamers, :count).by(1)
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the created beamer' do
|
||||||
|
post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params)
|
||||||
|
body = deserialize_response response
|
||||||
|
expect(body[:name]).to eq('new-beamer')
|
||||||
|
expect(body[:display_state]).to eq('groups')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects invalid display_state' do
|
||||||
|
post :create, params: {
|
||||||
|
tournament_id: @tournament.to_param, name: 'bad', display_state: 'invalid'
|
||||||
|
}
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects duplicate name in same tournament' do
|
||||||
|
post :create, params: {
|
||||||
|
tournament_id: @tournament.to_param, name: @beamer.name, display_state: 'matches_all'
|
||||||
|
}
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as another user' do
|
||||||
|
before(:each) { apply_authentication_headers_for create(:user) }
|
||||||
|
|
||||||
|
it 'returns forbidden' do
|
||||||
|
post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params)
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'unauthenticated' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params)
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH #update' do
|
||||||
|
context 'as owner' do
|
||||||
|
before(:each) { apply_authentication_headers_for @owner }
|
||||||
|
|
||||||
|
it 'updates the display_state' do
|
||||||
|
patch :update, params: {
|
||||||
|
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
||||||
|
display_state: 'during_game'
|
||||||
|
}
|
||||||
|
expect(response).to be_successful
|
||||||
|
body = deserialize_response response
|
||||||
|
expect(body[:display_state]).to eq('during_game')
|
||||||
|
expect(@beamer.reload.display_state).to eq('during_game')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates optional fields' do
|
||||||
|
patch :update, params: {
|
||||||
|
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
||||||
|
qr_code_url: 'https://example.com/qr', logo_url: '/static/logo.png'
|
||||||
|
}
|
||||||
|
expect(response).to be_successful
|
||||||
|
@beamer.reload
|
||||||
|
expect(@beamer.qr_code_url).to eq('https://example.com/qr')
|
||||||
|
expect(@beamer.logo_url).to eq('/static/logo.png')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects invalid display_state' do
|
||||||
|
patch :update, params: {
|
||||||
|
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
||||||
|
display_state: 'bogus'
|
||||||
|
}
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as another user' do
|
||||||
|
before(:each) { apply_authentication_headers_for create(:user) }
|
||||||
|
|
||||||
|
it 'returns forbidden' do
|
||||||
|
patch :update, params: {
|
||||||
|
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
||||||
|
display_state: 'groups'
|
||||||
|
}
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE #destroy' do
|
||||||
|
context 'as owner' do
|
||||||
|
before(:each) { apply_authentication_headers_for @owner }
|
||||||
|
|
||||||
|
it 'deletes the beamer' do
|
||||||
|
expect {
|
||||||
|
delete :destroy, params: {
|
||||||
|
tournament_id: @tournament.to_param, id: @beamer.to_param
|
||||||
|
}
|
||||||
|
}.to change(@tournament.beamers, :count).by(-1)
|
||||||
|
expect(response).to have_http_status(:no_content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as another user' do
|
||||||
|
before(:each) { apply_authentication_headers_for create(:user) }
|
||||||
|
|
||||||
|
it 'returns forbidden' do
|
||||||
|
delete :destroy, params: {
|
||||||
|
tournament_id: @tournament.to_param, id: @beamer.to_param
|
||||||
|
}
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :beamer do
|
||||||
|
sequence(:name) { |n| "beamer-#{n}" }
|
||||||
|
display_state { 'matches_all' }
|
||||||
|
is_default { false }
|
||||||
|
tournament
|
||||||
|
|
||||||
|
factory :default_beamer do
|
||||||
|
name { 'default' }
|
||||||
|
is_default { true }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Beamer, type: :model do
|
||||||
|
describe 'validation' do
|
||||||
|
it { should validate_presence_of :name }
|
||||||
|
it { should validate_presence_of :display_state }
|
||||||
|
|
||||||
|
it 'validates uniqueness of name scoped to tournament' do
|
||||||
|
beamer = create(:beamer)
|
||||||
|
duplicate = build(:beamer, name: beamer.name, tournament: beamer.tournament)
|
||||||
|
expect(duplicate).not_to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows same name in different tournaments' do
|
||||||
|
beamer = create(:beamer, name: 'projector')
|
||||||
|
other = build(:beamer, name: 'projector', tournament: create(:tournament))
|
||||||
|
expect(other).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'validates display_state inclusion' do
|
||||||
|
beamer = build(:beamer, display_state: 'invalid_state')
|
||||||
|
expect(beamer).not_to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
Beamer::DISPLAY_STATES.each do |state|
|
||||||
|
it "accepts display_state '#{state}'" do
|
||||||
|
beamer = build(:beamer, display_state: state)
|
||||||
|
expect(beamer).to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'association' do
|
||||||
|
it { should belong_to :tournament }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'scopes' do
|
||||||
|
describe '.default_beamer' do
|
||||||
|
it 'returns only default beamers' do
|
||||||
|
tournament = create(:tournament)
|
||||||
|
default = create(:default_beamer, tournament: tournament)
|
||||||
|
create(:beamer, tournament: tournament)
|
||||||
|
|
||||||
|
result = tournament.beamers.default_beamer
|
||||||
|
expect(result).to contain_exactly(default)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#owner' do
|
||||||
|
it 'delegates to tournament' do
|
||||||
|
beamer = create(:beamer)
|
||||||
|
expect(beamer.owner).to eq(beamer.tournament.owner)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -29,6 +29,7 @@ RSpec.describe Tournament, type: :model do
|
||||||
it { should belong_to :user }
|
it { should belong_to :user }
|
||||||
it { should have_many :teams }
|
it { should have_many :teams }
|
||||||
it { should have_many :stages }
|
it { should have_many :stages }
|
||||||
|
it { should have_many :beamers }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#matches' do
|
describe '#matches' do
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe BeamersController, type: :routing do
|
||||||
|
describe 'routing' do
|
||||||
|
it 'routes to #index' do
|
||||||
|
expect(get: '/tournaments/1/beamers').to route_to(
|
||||||
|
'beamers#index', tournament_id: '1'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes to #show' do
|
||||||
|
expect(get: '/tournaments/1/beamers/2').to route_to(
|
||||||
|
'beamers#show', tournament_id: '1', id: '2'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes to #by_name' do
|
||||||
|
expect(get: '/tournaments/1/beamers/by_name').to route_to(
|
||||||
|
'beamers#by_name', tournament_id: '1'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes to #create' do
|
||||||
|
expect(post: '/tournaments/1/beamers').to route_to(
|
||||||
|
'beamers#create', tournament_id: '1'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes to #update via PATCH' do
|
||||||
|
expect(patch: '/tournaments/1/beamers/2').to route_to(
|
||||||
|
'beamers#update', tournament_id: '1', id: '2'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes to #destroy' do
|
||||||
|
expect(delete: '/tournaments/1/beamers/2').to route_to(
|
||||||
|
'beamers#destroy', tournament_id: '1', id: '2'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue