First draft of API to get a single collection (#37053)

This commit is contained in:
David Roetzel
2025-12-01 16:04:52 +01:00
committed by GitHub
parent 1f9ddb7cf6
commit 2d203ca72a
11 changed files with 152 additions and 5 deletions

View File

@@ -9,7 +9,14 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create] before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create]
before_action :require_user! before_action :require_user!, only: [:create]
def show
cache_if_unauthenticated!
@collection = Collection.find(params[:id])
render json: @collection, serializer: REST::CollectionSerializer
end
def create def create
@collection = CreateCollectionService.new.call(collection_params, current_user.account) @collection = CreateCollectionService.new.call(collection_params, current_user.account)

View File

@@ -38,10 +38,18 @@ class Collection < ApplicationRecord
validate :tag_is_usable validate :tag_is_usable
validate :items_do_not_exceed_limit validate :items_do_not_exceed_limit
scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) }
def remote? def remote?
!local? !local?
end end
def items_for(account = nil)
result = collection_items.with_accounts
result = result.not_blocked_by(account) unless account.nil?
result
end
private private
def tag_is_usable def tag_is_usable

View File

@@ -33,6 +33,8 @@ class CollectionItem < ApplicationRecord
validates :object_uri, presence: true, if: -> { account.nil? } validates :object_uri, presence: true, if: -> { account.nil? }
scope :ordered, -> { order(position: :asc) } scope :ordered, -> { order(position: :asc) }
scope :with_accounts, -> { includes(account: [:account_stat, :user]) }
scope :not_blocked_by, ->(account) { where.not(accounts: { id: account.blocking }) }
def local_item_with_remote_account? def local_item_with_remote_account?
local? && account&.remote? local? && account&.remote?

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
class REST::CollectionItemSerializer < ActiveModel::Serializer
delegate :accepted?, to: :object
attributes :position, :state
belongs_to :account, serializer: REST::AccountSerializer, if: :accepted?
end

View File

@@ -5,4 +5,11 @@ class REST::CollectionSerializer < ActiveModel::Serializer
:created_at, :updated_at :created_at, :updated_at
belongs_to :account, serializer: REST::AccountSerializer belongs_to :account, serializer: REST::AccountSerializer
belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer
has_many :items, serializer: REST::CollectionItemSerializer
def items
object.items_for(current_user&.account)
end
end end

View File

@@ -8,7 +8,7 @@ namespace :api, format: false do
namespace :v1_alpha do namespace :v1_alpha do
resources :async_refreshes, only: :show resources :async_refreshes, only: :show
resources :collections, only: [:create] resources :collections, only: [:show, :create]
end end
# JSON / REST API # JSON / REST API

View File

@@ -3,7 +3,7 @@
Fabricator(:collection_item) do Fabricator(:collection_item) do
collection { Fabricate.build(:collection) } collection { Fabricate.build(:collection) }
account { Fabricate.build(:account) } account { Fabricate.build(:account) }
position 1 position { sequence(:position, 1) }
state :accepted state :accepted
end end

View File

@@ -48,4 +48,28 @@ RSpec.describe Collection do
it { is_expected.to_not be_valid } it { is_expected.to_not be_valid }
end end
end end
describe '#item_for' do
subject { Fabricate(:collection) }
let!(:items) { Fabricate.times(2, :collection_item, collection: subject) }
context 'when given no account' do
it 'returns all items' do
expect(subject.items_for).to match_array(items)
end
end
context 'when given an account' do
let(:account) { Fabricate(:account) }
before do
account.block!(items.first.account)
end
it 'does not return items blocked by this account' do
expect(subject.items_for(account)).to contain_exactly(items.last)
end
end
end
end end

View File

@@ -5,6 +5,50 @@ require 'rails_helper'
RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
include_context 'with API authentication', oauth_scopes: 'read:collections write:collections' include_context 'with API authentication', oauth_scopes: 'read:collections write:collections'
describe 'GET /api/v1_alpha/collections/:id' do
subject do
get "/api/v1_alpha/collections/#{collection.id}", headers: headers
end
let(:collection) { Fabricate(:collection) }
let!(:items) { Fabricate.times(2, :collection_item, collection:) }
shared_examples 'unfiltered, successful request' do
it 'includes all items in the response' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:items].size).to eq 2
end
end
context 'when user is not signed in' do
let(:headers) { {} }
it_behaves_like 'unfiltered, successful request'
end
context 'when user is signed in' do
context 'when the user has not blocked or muted anyone' do
it_behaves_like 'unfiltered, successful request'
end
context 'when the user has blocked an account' do
before do
user.account.block!(items.first.account)
end
it 'only includes the non-blocked account in the response' do
subject
expect(response).to have_http_status(200)
expect(response.parsed_body[:items].size).to eq 1
expect(response.parsed_body[:items][0]['position']).to eq items.last.position
end
end
end
end
describe 'POST /api/v1_alpha/collections' do describe 'POST /api/v1_alpha/collections' do
subject do subject do
post '/api/v1_alpha/collections', headers: headers, params: params post '/api/v1_alpha/collections', headers: headers, params: params

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe REST::CollectionItemSerializer do
subject { serialized_record_json(collection_item, described_class) }
let(:collection_item) do
Fabricate(:collection_item,
state:,
position: 4)
end
context 'when state is `accepted`' do
let(:state) { :accepted }
it 'includes the relevant attributes including the account' do
expect(subject)
.to include(
'account' => an_instance_of(Hash),
'state' => 'accepted',
'position' => 4
)
end
end
%i(pending rejected revoked).each do |unaccepted_state|
context "when state is `#{unaccepted_state}`" do
let(:state) { unaccepted_state }
it 'does not include an account' do
expect(subject.keys).to_not include('account')
end
end
end
end

View File

@@ -3,15 +3,24 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe REST::CollectionSerializer do RSpec.describe REST::CollectionSerializer do
subject { serialized_record_json(collection, described_class) } subject do
serialized_record_json(collection, described_class, options: {
scope: current_user,
scope_name: :current_user,
})
end
let(:current_user) { nil }
let(:tag) { Fabricate(:tag, name: 'discovery') }
let(:collection) do let(:collection) do
Fabricate(:collection, Fabricate(:collection,
name: 'Exquisite follows', name: 'Exquisite follows',
description: 'Always worth a follow', description: 'Always worth a follow',
local: true, local: true,
sensitive: true, sensitive: true,
discoverable: false) discoverable: false,
tag:)
end end
it 'includes the relevant attributes' do it 'includes the relevant attributes' do
@@ -23,6 +32,7 @@ RSpec.describe REST::CollectionSerializer do
'local' => true, 'local' => true,
'sensitive' => true, 'sensitive' => true,
'discoverable' => false, 'discoverable' => false,
'tag' => a_hash_including('name' => 'discovery'),
'created_at' => match_api_datetime_format, 'created_at' => match_api_datetime_format,
'updated_at' => match_api_datetime_format 'updated_at' => match_api_datetime_format
) )