mirror of
https://github.com/mastodon/mastodon.git
synced 2026-02-07 15:21:52 +00:00
Add models to represent "Collections" (#36977)
This commit is contained in:
parent
cfa4f402ef
commit
7ffa5fa0c4
53
app/models/collection.rb
Normal file
53
app/models/collection.rb
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: collections
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# description :text not null
|
||||
# discoverable :boolean not null
|
||||
# local :boolean not null
|
||||
# name :string not null
|
||||
# original_number_of_items :integer
|
||||
# sensitive :boolean not null
|
||||
# uri :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint(8) not null
|
||||
# tag_id :bigint(8)
|
||||
#
|
||||
class Collection < ApplicationRecord
|
||||
MAX_ITEMS = 25
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :tag, optional: true
|
||||
|
||||
has_many :collection_items, dependent: :delete_all
|
||||
|
||||
validates :name, presence: true
|
||||
validates :description, presence: true
|
||||
validates :uri, presence: true, if: :remote?
|
||||
validates :original_number_of_items,
|
||||
presence: true,
|
||||
numericality: { greater_than_or_equal: 0 },
|
||||
if: :remote?
|
||||
validate :tag_is_usable
|
||||
validate :items_do_not_exceed_limit
|
||||
|
||||
def remote?
|
||||
!local?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tag_is_usable
|
||||
return if tag.blank?
|
||||
|
||||
errors.add(:tag, :unusable) unless tag.usable?
|
||||
end
|
||||
|
||||
def items_do_not_exceed_limit
|
||||
errors.add(:collection_items, :too_many, count: MAX_ITEMS) if collection_items.size > MAX_ITEMS
|
||||
end
|
||||
end
|
||||
40
app/models/collection_item.rb
Normal file
40
app/models/collection_item.rb
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: collection_items
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# activity_uri :string
|
||||
# approval_last_verified_at :datetime
|
||||
# approval_uri :string
|
||||
# object_uri :string
|
||||
# position :integer default(1), not null
|
||||
# state :integer default("pending"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint(8)
|
||||
# collection_id :bigint(8) not null
|
||||
#
|
||||
class CollectionItem < ApplicationRecord
|
||||
belongs_to :collection
|
||||
belongs_to :account, optional: true
|
||||
|
||||
enum :state,
|
||||
{ pending: 0, accepted: 1, rejected: 2, revoked: 3 },
|
||||
validate: true
|
||||
|
||||
delegate :local?, :remote?, to: :collection
|
||||
|
||||
validates :position, numericality: { only_integer: true, greater_than: 0 }
|
||||
validates :activity_uri, presence: true, if: :local_item_with_remote_account?
|
||||
validates :approval_uri, absence: true, unless: :local?
|
||||
validates :account, presence: true, if: :accepted?
|
||||
validates :object_uri, presence: true, if: -> { account.nil? }
|
||||
|
||||
scope :ordered, -> { order(position: :asc) }
|
||||
|
||||
def local_item_with_remote_account?
|
||||
local? && account&.remote?
|
||||
end
|
||||
end
|
||||
|
|
@ -13,6 +13,8 @@ module Account::Associations
|
|||
has_many :account_warnings
|
||||
has_many :aliases, class_name: 'AccountAlias'
|
||||
has_many :bookmarks
|
||||
has_many :collections
|
||||
has_many :collection_items
|
||||
has_many :conversations, class_name: 'AccountConversation'
|
||||
has_many :custom_filters
|
||||
has_many :favourites
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ en:
|
|||
attributes:
|
||||
url:
|
||||
invalid: is not a valid URL
|
||||
collection:
|
||||
attributes:
|
||||
collection_items:
|
||||
too_many: are too many, no more than %{count} are allowed
|
||||
tag:
|
||||
unusable: may not be used
|
||||
doorkeeper/application:
|
||||
attributes:
|
||||
website:
|
||||
|
|
|
|||
19
db/migrate/20251118115657_create_collections.rb
Normal file
19
db/migrate/20251118115657_create_collections.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateCollections < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :collections do |t|
|
||||
t.references :account, null: false, foreign_key: true
|
||||
t.string :name, null: false
|
||||
t.text :description, null: false
|
||||
t.string :uri
|
||||
t.boolean :local, null: false # rubocop:disable Rails/ThreeStateBooleanColumn
|
||||
t.boolean :sensitive, null: false # rubocop:disable Rails/ThreeStateBooleanColumn
|
||||
t.boolean :discoverable, null: false # rubocop:disable Rails/ThreeStateBooleanColumn
|
||||
t.references :tag, foreign_key: true
|
||||
t.integer :original_number_of_items
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
18
db/migrate/20251119093332_create_collection_items.rb
Normal file
18
db/migrate/20251119093332_create_collection_items.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateCollectionItems < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :collection_items do |t|
|
||||
t.references :collection, null: false, foreign_key: { on_delete: :cascade }
|
||||
t.references :account, foreign_key: true
|
||||
t.integer :position, null: false, default: 1
|
||||
t.string :object_uri, index: { unique: true, where: 'activity_uri IS NOT NULL' }
|
||||
t.string :approval_uri, index: { unique: true, where: 'approval_uri IS NOT NULL' }
|
||||
t.string :activity_uri
|
||||
t.datetime :approval_last_verified_at
|
||||
t.integer :state, null: false, default: 0
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
39
db/schema.rb
39
db/schema.rb
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_10_23_210145) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_11_19_093332) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
|
|
@ -351,6 +351,39 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_23_210145) do
|
|||
t.index ["reference_account_id"], name: "index_canonical_email_blocks_on_reference_account_id"
|
||||
end
|
||||
|
||||
create_table "collection_items", force: :cascade do |t|
|
||||
t.bigint "collection_id", null: false
|
||||
t.bigint "account_id"
|
||||
t.integer "position", default: 1, null: false
|
||||
t.string "object_uri"
|
||||
t.string "approval_uri"
|
||||
t.string "activity_uri"
|
||||
t.datetime "approval_last_verified_at"
|
||||
t.integer "state", default: 0, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_collection_items_on_account_id"
|
||||
t.index ["approval_uri"], name: "index_collection_items_on_approval_uri", unique: true, where: "(approval_uri IS NOT NULL)"
|
||||
t.index ["collection_id"], name: "index_collection_items_on_collection_id"
|
||||
t.index ["object_uri"], name: "index_collection_items_on_object_uri", unique: true, where: "(activity_uri IS NOT NULL)"
|
||||
end
|
||||
|
||||
create_table "collections", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.string "name", null: false
|
||||
t.text "description", null: false
|
||||
t.string "uri"
|
||||
t.boolean "local", null: false
|
||||
t.boolean "sensitive", null: false
|
||||
t.boolean "discoverable", null: false
|
||||
t.bigint "tag_id"
|
||||
t.integer "original_number_of_items"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_collections_on_account_id"
|
||||
t.index ["tag_id"], name: "index_collections_on_tag_id"
|
||||
end
|
||||
|
||||
create_table "conversation_mutes", force: :cascade do |t|
|
||||
t.bigint "conversation_id", null: false
|
||||
t.bigint "account_id", null: false
|
||||
|
|
@ -1386,6 +1419,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_23_210145) do
|
|||
add_foreign_key "bulk_import_rows", "bulk_imports", on_delete: :cascade
|
||||
add_foreign_key "bulk_imports", "accounts", on_delete: :cascade
|
||||
add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id", on_delete: :cascade
|
||||
add_foreign_key "collection_items", "accounts"
|
||||
add_foreign_key "collection_items", "collections", on_delete: :cascade
|
||||
add_foreign_key "collections", "accounts"
|
||||
add_foreign_key "collections", "tags"
|
||||
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
|
||||
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
|
||||
add_foreign_key "custom_filter_keywords", "custom_filters", on_delete: :cascade
|
||||
|
|
|
|||
|
|
@ -17,3 +17,7 @@ Fabricator(:account) do
|
|||
discoverable true
|
||||
indexable true
|
||||
end
|
||||
|
||||
Fabricator(:remote_account, from: :account) do
|
||||
domain 'example.com'
|
||||
end
|
||||
|
|
|
|||
10
spec/fabricators/collection_fabricator.rb
Normal file
10
spec/fabricators/collection_fabricator.rb
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Fabricator(:collection) do
|
||||
account { Fabricate.build(:account) }
|
||||
name { sequence(:name) { |i| "Collection ##{i}" } }
|
||||
description 'People to follow'
|
||||
local true
|
||||
sensitive false
|
||||
discoverable true
|
||||
end
|
||||
15
spec/fabricators/collection_item_fabricator.rb
Normal file
15
spec/fabricators/collection_item_fabricator.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Fabricator(:collection_item) do
|
||||
collection { Fabricate.build(:collection) }
|
||||
account { Fabricate.build(:account) }
|
||||
position 1
|
||||
state :accepted
|
||||
end
|
||||
|
||||
Fabricator(:unverified_remote_collection_item, from: :collection_item) do
|
||||
account nil
|
||||
state :pending
|
||||
object_uri { Fabricate.build(:remote_account).uri }
|
||||
approval_uri { sequence(:uri) { |i| "https://example.com/authorizations/#{i}" } }
|
||||
end
|
||||
41
spec/models/collection_item_spec.rb
Normal file
41
spec/models/collection_item_spec.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CollectionItem do
|
||||
describe 'Validations' do
|
||||
subject { Fabricate.build(:collection_item) }
|
||||
|
||||
it { is_expected.to define_enum_for(:state) }
|
||||
|
||||
it { is_expected.to validate_numericality_of(:position).only_integer.is_greater_than(0) }
|
||||
|
||||
context 'when account inclusion is accepted' do
|
||||
subject { Fabricate.build(:collection_item, state: :accepted) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:account) }
|
||||
end
|
||||
|
||||
context 'when item is local and account is remote' do
|
||||
subject { Fabricate.build(:collection_item, account: remote_account) }
|
||||
|
||||
let(:remote_account) { Fabricate.build(:remote_account) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:activity_uri) }
|
||||
end
|
||||
|
||||
context 'when item is not local' do
|
||||
subject { Fabricate.build(:collection_item, collection: remote_collection) }
|
||||
|
||||
let(:remote_collection) { Fabricate.build(:collection, local: false) }
|
||||
|
||||
it { is_expected.to validate_absence_of(:approval_uri) }
|
||||
end
|
||||
|
||||
context 'when account is not present' do
|
||||
subject { Fabricate.build(:unverified_remote_collection_item) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:object_uri) }
|
||||
end
|
||||
end
|
||||
end
|
||||
45
spec/models/collection_spec.rb
Normal file
45
spec/models/collection_spec.rb
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Collection do
|
||||
describe 'Validations' do
|
||||
subject { Fabricate.build :collection }
|
||||
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:description) }
|
||||
|
||||
context 'when collection is remote' do
|
||||
subject { Fabricate.build :collection, local: false }
|
||||
|
||||
it { is_expected.to validate_presence_of(:uri) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:original_number_of_items) }
|
||||
end
|
||||
|
||||
context 'when using a hashtag as category' do
|
||||
subject { Fabricate.build(:collection, tag:) }
|
||||
|
||||
context 'when hashtag is usable' do
|
||||
let(:tag) { Fabricate.build(:tag) }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'when hashtag is not usable' do
|
||||
let(:tag) { Fabricate.build(:tag, usable: false) }
|
||||
|
||||
it { is_expected.to_not be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are more items than allowed' do
|
||||
subject { Fabricate.build(:collection, collection_items:) }
|
||||
|
||||
let(:collection_items) { Fabricate.build_times(described_class::MAX_ITEMS + 1, :collection_item, collection: nil) }
|
||||
|
||||
it { is_expected.to_not be_valid }
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user