From 827ba9eb08d6838001d44d5fefec47318d5f6d4d Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 19 Nov 2025 14:54:10 +0100 Subject: [PATCH] Add basic `CollectionItem` model --- app/models/collection.rb | 2 + app/models/collection_item.rb | 39 +++++++++++++++++++ app/models/concerns/account/associations.rb | 2 + .../20251119093332_create_collection_items.rb | 18 +++++++++ db/schema.rb | 21 +++++++++- spec/fabricators/account_fabricator.rb | 4 ++ .../fabricators/collection_item_fabricator.rb | 14 +++++++ spec/models/collection_item_spec.rb | 33 ++++++++++++++++ 8 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 app/models/collection_item.rb create mode 100644 db/migrate/20251119093332_create_collection_items.rb create mode 100644 spec/fabricators/collection_item_fabricator.rb create mode 100644 spec/models/collection_item_spec.rb diff --git a/app/models/collection.rb b/app/models/collection.rb index 0f8cf633668..950090e12f8 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -21,6 +21,8 @@ class Collection < ApplicationRecord 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? diff --git a/app/models/collection_item.rb b/app/models/collection_item.rb new file mode 100644 index 00000000000..7eab6290d9f --- /dev/null +++ b/app/models/collection_item.rb @@ -0,0 +1,39 @@ +# 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 :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 diff --git a/app/models/concerns/account/associations.rb b/app/models/concerns/account/associations.rb index 62c55da5de1..e1684d25607 100644 --- a/app/models/concerns/account/associations.rb +++ b/app/models/concerns/account/associations.rb @@ -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 diff --git a/db/migrate/20251119093332_create_collection_items.rb b/db/migrate/20251119093332_create_collection_items.rb new file mode 100644 index 00000000000..9fc5d99df53 --- /dev/null +++ b/db/migrate/20251119093332_create_collection_items.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index ace8fed3d99..cc00e9d64ef 100644 --- a/db/schema.rb +++ b/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_11_18_115657) 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,23 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_18_115657) 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 @@ -1402,6 +1419,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_18_115657) 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 diff --git a/spec/fabricators/account_fabricator.rb b/spec/fabricators/account_fabricator.rb index 6ec89a1cb65..bce8803be75 100644 --- a/spec/fabricators/account_fabricator.rb +++ b/spec/fabricators/account_fabricator.rb @@ -17,3 +17,7 @@ Fabricator(:account) do discoverable true indexable true end + +Fabricator(:remote_account, from: :account) do + domain 'example.com' +end diff --git a/spec/fabricators/collection_item_fabricator.rb b/spec/fabricators/collection_item_fabricator.rb new file mode 100644 index 00000000000..331379d353a --- /dev/null +++ b/spec/fabricators/collection_item_fabricator.rb @@ -0,0 +1,14 @@ +# 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 + object_uri { Fabricate.build(:remote_account).uri } + approval_uri { sequence(:uri) { |i| "https://example.com/authorizations/#{i}" } } +end diff --git a/spec/models/collection_item_spec.rb b/spec/models/collection_item_spec.rb new file mode 100644 index 00000000000..ab64b26caa3 --- /dev/null +++ b/spec/models/collection_item_spec.rb @@ -0,0 +1,33 @@ +# 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 account is not present' do + subject { Fabricate.build(:unverified_remote_collection_item) } + + it { is_expected.to validate_presence_of(:object_uri) } + end + end +end