上映会のし組み作り(#295) (#296)
#295 #295 #295 #295 #295 #295 #295 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #296
This commit was merged in pull request #296.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
class TheatresController < ApplicationController
|
||||
def show
|
||||
theatre = Theatre.find_by(id: params[:id])
|
||||
return head :not_found unless theatre
|
||||
|
||||
render json: TheatreRepr.base(theatre)
|
||||
end
|
||||
|
||||
def watching
|
||||
return head :unauthorized unless current_user
|
||||
|
||||
theatre = Theatre.find_by(id: params[:id])
|
||||
return head :not_found unless theatre
|
||||
|
||||
host_flg = false
|
||||
post_id = nil
|
||||
post_started_at = nil
|
||||
|
||||
theatre.with_lock do
|
||||
TheatreWatchingUser.find_or_initialize_by(theatre:, user: current_user).tap {
|
||||
_1.expires_at = 30.seconds.from_now
|
||||
}.save!
|
||||
|
||||
if (!(theatre.host_user_id?) ||
|
||||
!(theatre.watching_users.exists?(id: theatre.host_user_id)))
|
||||
theatre.update!(host_user_id: current_user.id)
|
||||
end
|
||||
|
||||
host_flg = theatre.host_user_id == current_user.id
|
||||
post_id = theatre.current_post_id
|
||||
post_started_at = theatre.current_post_started_at
|
||||
end
|
||||
|
||||
render json: { host_flg:, post_id:, post_started_at: }
|
||||
end
|
||||
|
||||
def next_post
|
||||
return head :unauthorized unless current_user
|
||||
|
||||
theatre = Theatre.find_by(id: params[:id])
|
||||
return head :not_found unless theatre
|
||||
return head :forbidden if theatre.host_user != current_user
|
||||
|
||||
post = Post.where("url LIKE '%nicovideo.jp%'")
|
||||
.or(Post.where("url LIKE '%youtube.com%'"))
|
||||
.order('RAND()')
|
||||
.first
|
||||
theatre.update!(current_post: post, current_post_started_at: Time.current)
|
||||
|
||||
head :no_content
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
class Theatre < ApplicationRecord
|
||||
include MyDiscard
|
||||
|
||||
has_many :comments, class_name: 'TheatreComment'
|
||||
has_many :theatre_watching_users, dependent: :delete_all
|
||||
has_many :active_theatre_watching_users, -> { active },
|
||||
class_name: 'TheatreWatchingUser', inverse_of: :theatre
|
||||
has_many :watching_users, through: :active_theatre_watching_users, source: :user
|
||||
|
||||
belongs_to :host_user, class_name: 'User', optional: true
|
||||
belongs_to :current_post, class_name: 'Post', optional: true
|
||||
belongs_to :created_by_user, class_name: 'User'
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
class TheatreComment < ApplicationRecord
|
||||
include MyDiscard
|
||||
|
||||
self.primary_key = :theatre_id, :no
|
||||
|
||||
belongs_to :theatre
|
||||
end
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
class TheatreWatchingUser < ApplicationRecord
|
||||
self.primary_key = :theatre_id, :user_id
|
||||
|
||||
belongs_to :theatre
|
||||
belongs_to :user
|
||||
|
||||
scope :active, -> { where('expires_at >= ?', Time.current) }
|
||||
scope :expired, -> { where('expires_at < ?', Time.current) }
|
||||
|
||||
def active? = expires_at >= Time.current
|
||||
|
||||
def refresh! = update!(expires_at: 30.seconds.from_now)
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
|
||||
module TheatreRepr
|
||||
BASE = { only: [:id, :name, :opens_at, :closes_at, :created_at, :updated_at],
|
||||
include: { created_by_user: { only: [:id, :name] } } }.freeze
|
||||
|
||||
module_function
|
||||
|
||||
def base theatre
|
||||
theatre.as_json(BASE)
|
||||
end
|
||||
|
||||
def many theatre
|
||||
theatre.map { |t| base(t) }
|
||||
end
|
||||
end
|
||||
@@ -72,4 +72,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :theatres, only: [:show] do
|
||||
member do
|
||||
put :watching
|
||||
patch :next_post
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
class CreateTheatres < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :theatres do |t|
|
||||
t.string :name
|
||||
t.datetime :opens_at, null: false, index: true
|
||||
t.datetime :closes_at, index: true
|
||||
t.integer :kind, null: false, index: true
|
||||
t.references :current_post, foreign_key: { to_table: :posts }, index: true
|
||||
t.datetime :current_post_started_at
|
||||
t.integer :next_comment_no, null: false, default: 1
|
||||
t.references :host_user, foreign_key: { to_table: :users }
|
||||
t.references :created_by_user, null: false, foreign_key: { to_table: :users }, index: true
|
||||
t.timestamps
|
||||
t.datetime :discarded_at, index: true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
class CreateTheatreComments < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :theatre_comments, primary_key: [:theatre_id, :no] do |t|
|
||||
t.references :theatre, null: false, foreign_key: { to_table: :theatres }
|
||||
t.integer :no, null: false
|
||||
t.references :user, foreign_key: { to_table: :users }
|
||||
t.text :content, null: false
|
||||
t.timestamps
|
||||
t.datetime :discarded_at, index: true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
class CreateTheatreWatchingUsers < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :theatre_watching_users, primary_key: [:theatre_id, :user_id] do |t|
|
||||
t.references :theatre, null: false, foreign_key: { to_table: :theatres }
|
||||
t.references :user, null: false, foreign_key: { to_table: :users }, index: true
|
||||
t.datetime :expires_at, null: false, index: true
|
||||
t.timestamps
|
||||
|
||||
t.index [:theatre_id, :expires_at]
|
||||
end
|
||||
end
|
||||
end
|
||||
Generated
+55
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_03_11_232300) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_03_17_015000) do
|
||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -167,6 +167,53 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_11_232300) do
|
||||
t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true
|
||||
end
|
||||
|
||||
create_table "theatre_comments", primary_key: ["theatre_id", "no"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "theatre_id", null: false
|
||||
t.integer "no", null: false
|
||||
t.bigint "user_id"
|
||||
t.text "content", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "discarded_at"
|
||||
t.index ["discarded_at"], name: "index_theatre_comments_on_discarded_at"
|
||||
t.index ["theatre_id"], name: "index_theatre_comments_on_theatre_id"
|
||||
t.index ["user_id"], name: "index_theatre_comments_on_user_id"
|
||||
end
|
||||
|
||||
create_table "theatre_watching_users", primary_key: ["theatre_id", "user_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "theatre_id", null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.datetime "expires_at", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["expires_at"], name: "index_theatre_watching_users_on_expires_at"
|
||||
t.index ["theatre_id", "expires_at"], name: "index_theatre_watching_users_on_theatre_id_and_expires_at"
|
||||
t.index ["theatre_id"], name: "index_theatre_watching_users_on_theatre_id"
|
||||
t.index ["user_id"], name: "index_theatre_watching_users_on_user_id"
|
||||
end
|
||||
|
||||
create_table "theatres", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.datetime "opens_at", null: false
|
||||
t.datetime "closes_at"
|
||||
t.integer "kind", null: false
|
||||
t.bigint "current_post_id"
|
||||
t.datetime "current_post_started_at"
|
||||
t.integer "next_comment_no", default: 1, null: false
|
||||
t.bigint "host_user_id"
|
||||
t.bigint "created_by_user_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "discarded_at"
|
||||
t.index ["closes_at"], name: "index_theatres_on_closes_at"
|
||||
t.index ["created_by_user_id"], name: "index_theatres_on_created_by_user_id"
|
||||
t.index ["current_post_id"], name: "index_theatres_on_current_post_id"
|
||||
t.index ["discarded_at"], name: "index_theatres_on_discarded_at"
|
||||
t.index ["host_user_id"], name: "index_theatres_on_host_user_id"
|
||||
t.index ["kind"], name: "index_theatres_on_kind"
|
||||
t.index ["opens_at"], name: "index_theatres_on_opens_at"
|
||||
end
|
||||
|
||||
create_table "user_ips", primary_key: ["user_id", "ip_address_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.bigint "ip_address_id", null: false
|
||||
@@ -262,6 +309,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_11_232300) do
|
||||
add_foreign_key "tag_similarities", "tags"
|
||||
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
|
||||
add_foreign_key "tags", "tag_names"
|
||||
add_foreign_key "theatre_comments", "theatres"
|
||||
add_foreign_key "theatre_comments", "users"
|
||||
add_foreign_key "theatre_watching_users", "theatres"
|
||||
add_foreign_key "theatre_watching_users", "users"
|
||||
add_foreign_key "theatres", "posts", column: "current_post_id"
|
||||
add_foreign_key "theatres", "users", column: "created_by_user_id"
|
||||
add_foreign_key "theatres", "users", column: "host_user_id"
|
||||
add_foreign_key "user_ips", "ip_addresses"
|
||||
add_foreign_key "user_ips", "users"
|
||||
add_foreign_key "user_post_views", "posts"
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
require 'rails_helper'
|
||||
require 'active_support/testing/time_helpers'
|
||||
|
||||
|
||||
RSpec.describe 'Theatres API', type: :request do
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
around do |example|
|
||||
travel_to(Time.zone.parse('2026-03-18 21:00:00')) do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
let(:member) { create(:user, :member, name: 'member user') }
|
||||
let(:other_user) { create(:user, :member, name: 'other user') }
|
||||
|
||||
let!(:youtube_post) do
|
||||
Post.create!(
|
||||
title: 'youtube post',
|
||||
url: 'https://www.youtube.com/watch?v=spec123'
|
||||
)
|
||||
end
|
||||
|
||||
let!(:other_post) do
|
||||
Post.create!(
|
||||
title: 'other post',
|
||||
url: 'https://example.com/posts/1'
|
||||
)
|
||||
end
|
||||
|
||||
let!(:theatre) do
|
||||
Theatre.create!(
|
||||
name: 'spec theatre',
|
||||
opens_at: Time.zone.parse('2026-03-18 20:00:00'),
|
||||
kind: 0,
|
||||
created_by_user: member
|
||||
)
|
||||
end
|
||||
|
||||
describe 'GET /theatres/:id' do
|
||||
subject(:do_request) do
|
||||
get "/theatres/#{theatre_id}"
|
||||
end
|
||||
|
||||
context 'when theatre exists' do
|
||||
let(:theatre_id) { theatre.id }
|
||||
|
||||
it 'returns theatre json' do
|
||||
do_request
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to include(
|
||||
'id' => theatre.id,
|
||||
'name' => 'spec theatre'
|
||||
)
|
||||
expect(json).to have_key('opens_at')
|
||||
expect(json).to have_key('closes_at')
|
||||
expect(json).to have_key('created_at')
|
||||
expect(json).to have_key('updated_at')
|
||||
|
||||
expect(json['created_by_user']).to include(
|
||||
'id' => member.id,
|
||||
'name' => 'member user'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when theatre does not exist' do
|
||||
let(:theatre_id) { 999_999_999 }
|
||||
|
||||
it 'returns 404' do
|
||||
do_request
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /theatres/:id/watching' do
|
||||
subject(:do_request) do
|
||||
put "/theatres/#{theatre_id}/watching"
|
||||
end
|
||||
|
||||
let(:theatre_id) { theatre.id }
|
||||
|
||||
context 'when not logged in' do
|
||||
it 'returns 401' do
|
||||
sign_out
|
||||
do_request
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when theatre does not exist' do
|
||||
let(:theatre_id) { 999_999_999 }
|
||||
|
||||
it 'returns 404' do
|
||||
sign_in_as(member)
|
||||
do_request
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when theatre has no host yet' do
|
||||
before do
|
||||
sign_in_as(member)
|
||||
end
|
||||
|
||||
it 'creates watching row, assigns current user as host, and returns current theatre info' do
|
||||
expect { do_request }
|
||||
.to change { TheatreWatchingUser.count }.by(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
theatre.reload
|
||||
watch = TheatreWatchingUser.find_by!(theatre: theatre, user: member)
|
||||
|
||||
expect(theatre.host_user_id).to eq(member.id)
|
||||
expect(watch.expires_at).to be_within(1.second).of(30.seconds.from_now)
|
||||
|
||||
expect(json).to eq(
|
||||
'host_flg' => true,
|
||||
'post_id' => nil,
|
||||
'post_started_at' => nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current user is already watching' do
|
||||
let!(:watching_row) do
|
||||
TheatreWatchingUser.create!(
|
||||
theatre: theatre,
|
||||
user: member,
|
||||
expires_at: 5.seconds.from_now
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in_as(member)
|
||||
end
|
||||
|
||||
it 'refreshes expires_at without creating another row' do
|
||||
expect { do_request }
|
||||
.not_to change { TheatreWatchingUser.count }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
expect(watching_row.reload.expires_at)
|
||||
.to be_within(1.second).of(30.seconds.from_now)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when another active host exists' do
|
||||
before do
|
||||
TheatreWatchingUser.create!(
|
||||
theatre: theatre,
|
||||
user: other_user,
|
||||
expires_at: 10.minutes.from_now
|
||||
)
|
||||
theatre.update!(host_user: other_user)
|
||||
sign_in_as(member)
|
||||
end
|
||||
|
||||
it 'does not steal host and returns host_flg false' do
|
||||
expect { do_request }
|
||||
.to change { TheatreWatchingUser.count }.by(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(theatre.reload.host_user_id).to eq(other_user.id)
|
||||
|
||||
expect(json).to eq(
|
||||
'host_flg' => false,
|
||||
'post_id' => nil,
|
||||
'post_started_at' => nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when host is set but no longer actively watching' do
|
||||
let(:started_at) { 2.minutes.ago }
|
||||
|
||||
before do
|
||||
TheatreWatchingUser.create!(
|
||||
theatre: theatre,
|
||||
user: other_user,
|
||||
expires_at: 1.second.ago
|
||||
)
|
||||
theatre.update!(
|
||||
host_user: other_user,
|
||||
current_post: youtube_post,
|
||||
current_post_started_at: started_at
|
||||
)
|
||||
sign_in_as(member)
|
||||
end
|
||||
|
||||
it 'reassigns host to current user and returns current post info' do
|
||||
expect { do_request }
|
||||
.to change { TheatreWatchingUser.count }.by(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
theatre.reload
|
||||
expect(theatre.host_user_id).to eq(member.id)
|
||||
|
||||
expect(json['host_flg']).to eq(true)
|
||||
expect(json['post_id']).to eq(youtube_post.id)
|
||||
expect(Time.zone.parse(json['post_started_at']))
|
||||
.to be_within(1.second).of(started_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /theatres/:id/next_post' do
|
||||
subject(:do_request) do
|
||||
patch "/theatres/#{theatre_id}/next_post"
|
||||
end
|
||||
|
||||
let(:theatre_id) { theatre.id }
|
||||
|
||||
context 'when not logged in' do
|
||||
it 'returns 401' do
|
||||
sign_out
|
||||
do_request
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when theatre does not exist' do
|
||||
let(:theatre_id) { 999_999_999 }
|
||||
|
||||
it 'returns 404' do
|
||||
sign_in_as(member)
|
||||
do_request
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when logged in but not host' do
|
||||
before do
|
||||
theatre.update!(host_user: other_user)
|
||||
sign_in_as(member)
|
||||
end
|
||||
|
||||
it 'returns 403' do
|
||||
do_request
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current user is host' do
|
||||
before do
|
||||
theatre.update!(host_user: member)
|
||||
sign_in_as(member)
|
||||
end
|
||||
|
||||
it 'sets current_post to an eligible post and updates current_post_started_at' do
|
||||
expect { do_request }
|
||||
.to change { theatre.reload.current_post_id }
|
||||
.from(nil).to(youtube_post.id)
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
expect(theatre.reload.current_post_started_at)
|
||||
.to be_within(1.second).of(Time.current)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current user is host and no eligible post exists' do
|
||||
before do
|
||||
youtube_post.destroy!
|
||||
theatre.update!(
|
||||
host_user: member,
|
||||
current_post: other_post,
|
||||
current_post_started_at: 1.hour.ago
|
||||
)
|
||||
sign_in_as(member)
|
||||
end
|
||||
|
||||
it 'still returns 204 and clears current_post' do
|
||||
do_request
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
|
||||
theatre.reload
|
||||
expect(theatre.current_post_id).to be_nil
|
||||
expect(theatre.current_post_started_at)
|
||||
.to be_within(1.second).of(Time.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user