Add per-user maximum number of collections (#38769)

This commit is contained in:
David Roetzel authored and GitHub committed 2026-04-22 12:34:08 +00:00
1 parent bc09d3c5f2
commit 1cae543e8f
10 files changed
+103 -10

No files matched your search

+8
View File
@@ -53,6 +53,7 @@ class Collection < ApplicationRecord
validates :language, language: { if: :local?, allow_nil: true }
validate :tag_is_usable
validate :items_do_not_exceed_limit
validate :user_does_not_exceed_limit, on: :create
scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) }
scope :with_tag, -> { includes(:tag) }
@@ -105,4 +106,11 @@ class Collection < ApplicationRecord
def items_do_not_exceed_limit
errors.add(:collection_items, :too_many, count: MAX_ITEMS) if pending_or_accepted_items.size > MAX_ITEMS
end
def user_does_not_exceed_limit
return unless local?
limit = account.user.role.collection_limit
errors.add(:base, :too_many, count: limit) if account.collections.count >= limit
end
end
+11 -9
View File
@@ -4,15 +4,16 @@
#
# Table name: user_roles
#
# id :bigint(8) not null, primary key
# color :string default(""), not null
# highlighted :boolean default(FALSE), not null
# name :string default(""), not null
# permissions :bigint(8) default(0), not null
# position :integer default(0), not null
# require_2fa :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# id :bigint(8) not null, primary key
# collection_limit :integer default(10), not null
# color :string default(""), not null
# highlighted :boolean default(FALSE), not null
# name :string default(""), not null
# permissions :bigint(8) default(0), not null
# position :integer default(0), not null
# require_2fa :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class UserRole < ApplicationRecord
@@ -104,6 +105,7 @@ class UserRole < ApplicationRecord
validates :name, presence: true, unless: :everyone?
validates :color, format: { with: CSS_COLORS }, if: :color?
validates :position, numericality: { in: (-POSITION_LIMIT..POSITION_LIMIT) }
validates :collection_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validate :validate_permissions_elevation
validate :validate_position_elevation
+2
View File
@@ -3,6 +3,8 @@
class REST::RoleSerializer < ActiveModel::Serializer
attributes :id, :name, :permissions, :color, :highlighted
attribute :collection_limit, if: -> { Mastodon::Feature.collections_enabled? }
def id
object.id.to_s
end
+8
View File
@@ -32,6 +32,14 @@
%hr.spacer/
- if Mastodon::Feature.collections_enabled?
.fields-group
= form.input :collection_limit,
wrapper: :with_label
%hr.spacer/
- unless current_user.role == form.object
.field-group
+2
View File
@@ -160,6 +160,7 @@ en:
other: We have to make sure you're at least %{count} to use %{domain}. We won't store this.
role: The role controls which permissions the user has.
user_role:
collection_limit: Limits the number of Collections that a single user with this role can create. Please note that when you decrease this number, users who are already at this limit will not lose any Collections. But they will not be able to create additional ones.
color: Color to be used for the role throughout the UI, as RGB in hex format
highlighted: This makes the role publicly visible
name: Public name of the role, if role is set to be displayed as a badge
@@ -386,6 +387,7 @@ en:
role: Role
time_zone: Time zone
user_role:
collection_limit: Maximum number of Collections per user
color: Badge color
highlighted: Display role as badge on user profiles
name: Name
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddCollectionLimitToUserRoles < ActiveRecord::Migration[8.1]
def change
add_column :user_roles, :collection_limit, :integer, null: false, default: 10
end
end
+2 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_04_15_133505) do
ActiveRecord::Schema[8.1].define(version: 2026_04_20_124030) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@@ -1337,6 +1337,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_15_133505) do
end
create_table "user_roles", force: :cascade do |t|
t.integer "collection_limit", default: 10, null: false
t.string "color", default: "", null: false
t.datetime "created_at", null: false
t.boolean "highlighted", default: false, null: false
+14
View File
@@ -71,6 +71,20 @@ RSpec.describe Collection do
it { is_expected.to be_valid }
end
end
context 'when the user is already at the per-user limit of collections' do
subject { Fabricate.build(:collection, account:) }
let(:role) { Fabricate(:user_role, collection_limit: 2) }
let(:user) { Fabricate(:user, role:) }
let(:account) { user.account }
before do
Fabricate.times(2, :collection, account:)
end
it { is_expected.to_not be_valid }
end
end
describe '#item_for' do
+6
View File
@@ -31,6 +31,12 @@ RSpec.describe UserRole do
it { is_expected.to_not allow_values('x', '112233445566', '#xxyyzz').for(:color) }
end
describe 'collection_limit' do
subject { Fabricate.build :user_role }
it { is_expected.to validate_numericality_of(:collection_limit).only_integer.is_greater_than_or_equal_to(0) }
end
context 'when current_account is set' do
subject { Fabricate :user_role }
@@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe REST::RoleSerializer do
subject { serialized_record_json(role, described_class) }
let(:everyone) do
Fabricate.build(:user_role, permissions: 0)
end
let(:role) do
Fabricate.build(:user_role, id: 2342, name: 'test role', color: '#ABC', highlighted: true, permissions: 2300, collection_limit: 11)
end
before do
allow(UserRole).to receive(:everyone).and_return(everyone)
end
it 'includes the relevant attributes' do
expect(subject)
.to include({
'id' => '2342',
'name' => 'test role',
'color' => '#ABC',
'highlighted' => true,
'permissions' => '2300',
})
end
context 'when collections are enabled', feature: :collections do
it 'includes the relevant attributes' do
expect(subject)
.to include({
'id' => '2342',
'name' => 'test role',
'color' => '#ABC',
'highlighted' => true,
'permissions' => '2300',
'collection_limit' => 11,
})
end
end
end