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

This commit is contained in:
David Roetzel 2026-04-22 14:34:08 +02:00 committed by GitHub
parent bc09d3c5f2
commit 1cae543e8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 103 additions and 10 deletions

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

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

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

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

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

View File

@ -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

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

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

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 }

View File

@ -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