Browse Source

#206

feature/206
みてるぞ 1 week ago
parent
commit
663206c14c
3 changed files with 245 additions and 42 deletions
  1. +32
    -28
      backend/app/controllers/posts_controller.rb
  2. +200
    -1
      backend/spec/requests/posts_spec.rb
  3. +13
    -13
      frontend/src/pages/posts/PostSearchPage.tsx

+ 32
- 28
backend/app/controllers/posts_controller.rb View File

@@ -2,38 +2,46 @@ class PostsController < ApplicationController
Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true)

def index
url = params[:url].presence
title = params[:title].presence
original_created_from = params[:original_created_from].presence
original_created_to = params[:original_created_to].presence
created_between = params[:created_from].presence, params[:created_to].presence
updated_between = params[:updated_from].presence, params[:updated_to].presence

page = (params[:page].presence || 1).to_i
limit = (params[:limit].presence || 20).to_i
cursor = params[:cursor].presence

page = 1 if page < 1
limit = 1 if limit < 1

offset = (page - 1) * limit

sort_sql =
'COALESCE(posts.original_created_before - INTERVAL 1 SECOND,' +
'posts.original_created_from,' +
'posts.created_at)'
q =
filtered_posts
.preload(tags: { tag_name: :wiki_page })
.with_attached_thumbnail
.select("posts.*, #{ sort_sql } AS sort_ts")
.order(Arel.sql("#{ sort_sql } DESC"))
posts =
if cursor
q.where("#{ sort_sql } < ?", Time.iso8601(cursor)).limit(limit + 1)
else
q.limit(limit).offset(offset)
end
.to_a

next_cursor = nil
if cursor && posts.length > limit
next_cursor = posts.last.read_attribute('sort_ts').iso8601(6)
posts = posts.first(limit)

q = q.where('posts.url LIKE ?', "%#{ url }%") if url
q = q.where('posts.title LIKE ?', "%#{ title }%") if title
if original_created_from
q = q.where('posts.original_created_before > ?', original_created_from)
end
if original_created_to
q = q.where('posts.original_created_from <= ?', original_created_to)
end
q = q.where('posts.created_at >= ?', created_between[0]) if created_between[0]
q = q.where('posts.created_at <= ?', created_between[1]) if created_between[1]
q = q.where('posts.updated_at >= ?', updated_between[0]) if updated_between[0]
q = q.where('posts.updated_at <= ?', updated_between[1]) if updated_between[1]

sort_sql =
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE,' +
'posts.original_created_from,' +
'posts.created_at)'
posts = q.select("posts.*, #{ sort_sql } AS sort_ts")
.order(Arel.sql("#{ sort_sql } DESC"))
.limit(limit).offset(offset).to_a

render json: { posts: posts.map { |post|
PostRepr.base(post).tap do |json|
@@ -44,11 +52,7 @@ class PostsController < ApplicationController
nil
end
end
}, count: if filtered_posts.group_values.present?
filtered_posts.count.size
else
filtered_posts.count
end, next_cursor: }
}, count: q.group_values.present? ? q.count.size : q.count }
end

def random
@@ -63,7 +67,7 @@ class PostsController < ApplicationController
end

def show
post = Post.includes(tags: { tag_name: :wiki_page }).find(params[:id])
post = Post.includes(tags: { tag_name: :wiki_page }).find_by(id: params[:id])
return head :not_found unless post

viewed = current_user&.viewed?(post) || false
@@ -84,7 +88,7 @@ class PostsController < ApplicationController
title = params[:title].presence
url = params[:url]
thumbnail = params[:thumbnail]
tag_names = params[:tags].to_s.split(' ')
tag_names = params[:tags].to_s.split
original_created_from = params[:original_created_from]
original_created_before = params[:original_created_before]

@@ -125,7 +129,7 @@ class PostsController < ApplicationController
return head :forbidden unless current_user.member?

title = params[:title].presence
tag_names = params[:tags].to_s.split(' ')
tag_names = params[:tags].to_s.split
original_created_from = params[:original_created_from]
original_created_before = params[:original_created_before]

@@ -192,7 +196,7 @@ class PostsController < ApplicationController
private

def filtered_posts
tag_names = params[:tags].to_s.split(' ')
tag_names = params[:tags].to_s.split
match_type = params[:match]
if tag_names.present?
filter_posts_by_tags(tag_names, match_type)


+ 200
- 1
backend/spec/requests/posts_spec.rb View File

@@ -1,7 +1,8 @@
include ActiveSupport::Testing::TimeHelpers

require 'rails_helper'
require 'set'


RSpec.describe 'Posts API', type: :request do
# create / update で thumbnail.attach は走るが、
# resized_thumbnail! が MiniMagick 依存でコケやすいので request spec ではスタブしとくのが無難。
@@ -114,6 +115,204 @@ RSpec.describe 'Posts API', type: :request do
expect(json.fetch('count')).to eq(0)
end
end

context 'when url is provided' do
let!(:url_hit_post) do
Post.create!(uploaded_user: user, title: 'url hit',
url: 'https://example.com/needle-url-xyz').tap do |p|
PostTag.create!(post: p, tag:)
end
end

let!(:url_miss_post) do
Post.create!(uploaded_user: user, title: 'url miss',
url: 'https://example.com/other-url').tap do |p|
PostTag.create!(post: p, tag:)
end
end

it 'filters posts by url substring' do
get '/posts', params: { url: 'needle-url-xyz' }

expect(response).to have_http_status(:ok)
ids = json.fetch('posts').map { |p| p['id'] }

expect(ids).to include(url_hit_post.id)
expect(ids).not_to include(url_miss_post.id)
expect(json.fetch('count')).to eq(1)
end
end

context 'when title is provided' do
let!(:title_hit_post) do
Post.create!(uploaded_user: user, title: 'needle-title-xyz',
url: 'https://example.com/title-hit').tap do |p|
PostTag.create!(post: p, tag:)
end
end

let!(:title_miss_post) do
Post.create!(uploaded_user: user, title: 'other title',
url: 'https://example.com/title-miss').tap do |p|
PostTag.create!(post: p, tag:)
end
end

it 'filters posts by title substring' do
get '/posts', params: { title: 'needle-title-xyz' }

expect(response).to have_http_status(:ok)
ids = json.fetch('posts').map { |p| p['id'] }

expect(ids).to include(title_hit_post.id)
expect(ids).not_to include(title_miss_post.id)
expect(json.fetch('count')).to eq(1)
end
end

context 'when created_from/created_to are provided' do
let(:t_created_hit) { Time.zone.local(2010, 1, 5, 12, 0, 0) }
let(:t_created_miss) { Time.zone.local(2012, 1, 5, 12, 0, 0) }

let!(:created_hit_post) do
travel_to(t_created_hit) do
Post.create!(uploaded_user: user, title: 'created hit',
url: 'https://example.com/created-hit').tap do |p|
PostTag.create!(post: p, tag:)
end
end
end

let!(:created_miss_post) do
travel_to(t_created_miss) do
Post.create!(uploaded_user: user, title: 'created miss',
url: 'https://example.com/created-miss').tap do |p|
PostTag.create!(post: p, tag:)
end
end
end

it 'filters posts by created_at range' do
get '/posts', params: {
created_from: Time.zone.local(2010, 1, 1, 0, 0, 0).iso8601,
created_to: Time.zone.local(2010, 12, 31, 23, 59, 59).iso8601
}

expect(response).to have_http_status(:ok)
ids = json.fetch('posts').map { |p| p['id'] }

expect(ids).to include(created_hit_post.id)
expect(ids).not_to include(created_miss_post.id)
expect(json.fetch('count')).to eq(1)
end
end

context 'when updated_from/updated_to are provided' do
let(:t0) { Time.zone.local(2011, 2, 1, 12, 0, 0) }
let(:t1) { Time.zone.local(2011, 2, 10, 12, 0, 0) }

let!(:updated_hit_post) do
p = nil
travel_to(t0) do
p = Post.create!(uploaded_user: user, title: 'updated hit',
url: 'https://example.com/updated-hit').tap do |pp|
PostTag.create!(post: pp, tag:)
end
end
travel_to(t1) do
p.update!(title: 'updated hit v2')
end
p
end

let!(:updated_miss_post) do
travel_to(Time.zone.local(2013, 1, 1, 12, 0, 0)) do
Post.create!(uploaded_user: user, title: 'updated miss',
url: 'https://example.com/updated-miss').tap do |p|
PostTag.create!(post: p, tag:)
end
end
end

it 'filters posts by updated_at range' do
get '/posts', params: {
updated_from: Time.zone.local(2011, 2, 5, 0, 0, 0).iso8601,
updated_to: Time.zone.local(2011, 2, 20, 23, 59, 59).iso8601
}

expect(response).to have_http_status(:ok)
ids = json.fetch('posts').map { |p| p['id'] }

expect(ids).to include(updated_hit_post.id)
expect(ids).not_to include(updated_miss_post.id)
expect(json.fetch('count')).to eq(1)
end
end

context 'when original_created_from/original_created_to are provided' do
# 注意: controller の現状ロジックに合わせてる
# original_created_from は `original_created_before > ?`
# original_created_to は `original_created_from <= ?`

let!(:oc_hit_post) do
Post.create!(uploaded_user: user, title: 'oc hit',
url: 'https://example.com/oc-hit',
original_created_from: Time.zone.local(2015, 1, 1, 0, 0, 0),
original_created_before: Time.zone.local(2015, 1, 10, 0, 0, 0)).tap do |p|
PostTag.create!(post: p, tag:)
end
end

# original_created_from の条件は「original_created_before > param」なので、
# before が param 以下になるようにする(ただし before >= from は守る)
let!(:oc_miss_post_for_from) do
Post.create!(
uploaded_user: user,
title: 'oc miss for from',
url: 'https://example.com/oc-miss-from',
original_created_from: Time.zone.local(2014, 12, 1, 0, 0, 0),
original_created_before: Time.zone.local(2015, 1, 1, 0, 0, 0)
).tap { |p| PostTag.create!(post: p, tag:) }
end

# original_created_to の条件は「original_created_from <= param」なので、
# from が param より後になるようにする(before >= from は守る)
let!(:oc_miss_post_for_to) do
Post.create!(
uploaded_user: user,
title: 'oc miss for to',
url: 'https://example.com/oc-miss-to',
original_created_from: Time.zone.local(2015, 2, 1, 0, 0, 0),
original_created_before: Time.zone.local(2015, 2, 10, 0, 0, 0)
).tap { |p| PostTag.create!(post: p, tag:) }
end

it 'filters posts by original_created_from (current controller behavior)' do
get '/posts', params: {
original_created_from: Time.zone.local(2015, 1, 5, 0, 0, 0).iso8601
}

expect(response).to have_http_status(:ok)
ids = json.fetch('posts').map { |p| p['id'] }

expect(ids).to include(oc_hit_post.id)
expect(ids).not_to include(oc_miss_post_for_from.id)
expect(json.fetch('count')).to eq(2)
end

it 'filters posts by original_created_to (current controller behavior)' do
get '/posts', params: {
original_created_to: Time.zone.local(2015, 1, 15, 0, 0, 0).iso8601
}

expect(response).to have_http_status(:ok)
ids = json.fetch('posts').map { |p| p['id'] }

expect(ids).to include(oc_hit_post.id)
expect(ids).not_to include(oc_miss_post_for_to.id)
expect(json.fetch('count')).to eq(2)
end
end
end

describe 'GET /posts/:id' do


+ 13
- 13
frontend/src/pages/posts/PostSearchPage.tsx View File

@@ -78,7 +78,7 @@ export default (() => {
<div>
<Label>URL</Label>
<input
type="url"
type="text"
value={url}
onChange={e => setURL (e.target.value)}
className="w-full border p-2 rounded"/>
@@ -113,6 +113,18 @@ export default (() => {
</fieldset>
</div>

{/* オリジナルの投稿日時 */}
<div>
<Label>オリジナルの投稿日時</Label>
<DateTimeField
value={originalCreatedFrom ?? undefined}
onChange={isoUTC => setOriginalCreatedFrom (isoUTC ?? undefined)}/>
<span className="mx-1">〜</span>
<DateTimeField
value={originalCreatedTo ?? undefined}
onChange={isoUTC => setOriginalCreatedTo (isoUTC ?? undefined)}/>
</div>

{/* 投稿日時 */}
<div>
<Label>投稿日時</Label>
@@ -137,18 +149,6 @@ export default (() => {
onChange={isoUTC => setUpdatedTo (isoUTC ?? undefined)}/>
</div>

{/* オリジナルの投稿日時 */}
<div>
<Label>オリジナルの投稿日時</Label>
<DateTimeField
value={originalCreatedFrom ?? undefined}
onChange={isoUTC => setOriginalCreatedFrom (isoUTC ?? undefined)}/>
<span className="mx-1">〜</span>
<DateTimeField
value={originalCreatedTo ?? undefined}
onChange={isoUTC => setOriginalCreatedTo (isoUTC ?? undefined)}/>
</div>

{/* 検索 */}
<div className="py-3">
<button


Loading…
Cancel
Save