diff --git a/app/models/user.rb b/app/models/user.rb index fea7d0198..dea6b05c1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -73,6 +73,7 @@ class User < ApplicationRecord has_many :messages, dependent: :destroy has_many :notes, as: :noteable, inverse_of: :noteable has_many :notification_subscriptions, dependent: :destroy + has_many :user_optional_fields, dependent: :destroy has_many :notifications, dependent: :destroy has_many :offender_feedback_messages, class_name: "FeedbackMessage", inverse_of: :offender, foreign_key: :offender_id, dependent: :nullify has_many :organization_memberships, dependent: :destroy diff --git a/app/models/user_optional_field.rb b/app/models/user_optional_field.rb new file mode 100644 index 000000000..07d8f127d --- /dev/null +++ b/app/models/user_optional_field.rb @@ -0,0 +1,13 @@ +class UserOptionalField < ApplicationRecord + belongs_to :user + + validates :label, presence: true, length: { maximum: 30 }, uniqueness: { scope: :user_id } + validates :value, presence: true, length: { maximum: 128 } + validate :validate_quota + + def validate_quota + return unless user + + errors.add(:user_id, "already has the maximum allowable optional fields") unless user.user_optional_fields.count < 3 + end +end diff --git a/db/migrate/20200412194408_create_user_optional_fields.rb b/db/migrate/20200412194408_create_user_optional_fields.rb new file mode 100644 index 000000000..85fdd1af9 --- /dev/null +++ b/db/migrate/20200412194408_create_user_optional_fields.rb @@ -0,0 +1,13 @@ +class CreateUserOptionalFields < ActiveRecord::Migration[5.2] + def change + create_table :user_optional_fields do |t| + t.string :label, null: false + t.string :value, null: false + t.references :user, foreign_key: true, null: false + + t.timestamps + end + + add_index :user_optional_fields, [:label, :user_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 9133e1da9..4bef0accf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1086,6 +1086,16 @@ ActiveRecord::Schema.define(version: 2020_04_26_124118) do t.index ["user_id"], name: "index_user_counters_on_user_id", unique: true end + create_table "user_optional_fields", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "label", null: false + t.datetime "updated_at", null: false + t.bigint "user_id", null: false + t.string "value", null: false + t.index ["label", "user_id"], name: "index_user_optional_fields_on_label_and_user_id", unique: true + t.index ["user_id"], name: "index_user_optional_fields_on_user_id" + end + create_table "users", id: :serial, force: :cascade do |t| t.integer "articles_count", default: 0, null: false t.string "available_for" @@ -1268,6 +1278,7 @@ ActiveRecord::Schema.define(version: 2020_04_26_124118) do add_foreign_key "user_blocks", "users", column: "blocked_id" add_foreign_key "user_blocks", "users", column: "blocker_id" add_foreign_key "user_counters", "users", on_delete: :cascade + add_foreign_key "user_optional_fields", "users" add_foreign_key "users_roles", "users", on_delete: :cascade add_foreign_key "webhook_endpoints", "oauth_applications" add_foreign_key "webhook_endpoints", "users" diff --git a/spec/factories/user_optional_fields.rb b/spec/factories/user_optional_fields.rb new file mode 100644 index 000000000..c2daec856 --- /dev/null +++ b/spec/factories/user_optional_fields.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :user_optional_field do + user + label { "Pronoun" } + value { "They/them" } + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 92729600c..1608fd754 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -136,5 +136,13 @@ FactoryBot.define do user.add_role :tag_moderator, tag end end + + trait :with_user_optional_fields do + after(:create) do |user| + create(:user_optional_field, user: user) + create(:user_optional_field, user: user, label: "another field1", value: "another value1") + create(:user_optional_field, user: user, label: "another field2", value: "another value2") + end + end end end diff --git a/spec/models/user_optional_field_spec.rb b/spec/models/user_optional_field_spec.rb new file mode 100644 index 000000000..255b843f9 --- /dev/null +++ b/spec/models/user_optional_field_spec.rb @@ -0,0 +1,14 @@ +require "rails_helper" + +RSpec.describe UserOptionalField, type: :model do + subject { user_optional_field } + + let(:user_optional_field) { create(:user_optional_field) } + + it { is_expected.to belong_to(:user) } + it { is_expected.to validate_presence_of(:label) } + it { is_expected.to validate_uniqueness_of(:label).scoped_to(:user_id) } + it { is_expected.to validate_length_of(:label).is_at_most(30) } + it { is_expected.to validate_presence_of(:value) } + it { is_expected.to validate_length_of(:value).is_at_most(128) } +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a344a5ce0..f96343154 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -12,6 +12,7 @@ end RSpec.describe User, type: :model do let(:user) { create(:user) } let(:other_user) { create(:user) } + let(:user_with_user_optional_fields) { create(:user, :with_user_optional_fields) } let(:org) { create(:organization) } before { mock_auth_hash } @@ -132,9 +133,16 @@ RSpec.describe User, type: :model do end # rubocop:enable RSpec/NamedSubject + it "has at most three optional fields" do + expect(user_with_user_optional_fields).to have_many(:user_optional_fields).dependent(:destroy) + fourth_field = user_with_user_optional_fields.user_optional_fields.create(label: "some field", value: "some value") + expect(fourth_field).not_to be_valid + end + + it { is_expected.to have_many(:organization_memberships).dependent(:destroy) } it { is_expected.to have_one(:counters).class_name("UserCounter").dependent(:destroy) } it { is_expected.to have_one(:pro_membership).dependent(:destroy) } - + it { is_expected.to validate_uniqueness_of(:username).case_insensitive } it { is_expected.not_to allow_value("#xyz").for(:bg_color_hex) } it { is_expected.not_to allow_value("#xyz").for(:text_color_hex) } it { is_expected.not_to allow_value("AcMe_1%").for(:username) }