Add unique indexes - part 4 (#8059)

* Rubocop unique index cop needs a static list of columns

* Add unique index on organizations secret

* Add unique index on chat_channel_memberships chat_channel_id

* Add unique index to chat_channel slug

* Add unique index to badge_achievements badge_id
This commit is contained in:
rhymes 2020-05-27 09:29:20 +02:00 committed by GitHub
parent e39f5b04d1
commit acb35afe03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 117 additions and 26 deletions

View file

@ -1,6 +1,9 @@
class ChatChannel < ApplicationRecord
attr_accessor :current_user, :usernames_string
CHANNEL_TYPES = %w[open invite_only direct].freeze
STATUSES = %w[active inactive blocked].freeze
has_many :messages, dependent: :destroy
has_many :chat_channel_memberships, dependent: :destroy
has_many :users, through: :chat_channel_memberships
@ -15,8 +18,8 @@ class ChatChannel < ApplicationRecord
has_many :rejected_users, through: :rejected_memberships, class_name: "User", source: :user
has_many :mod_users, through: :mod_memberships, class_name: "User", source: :user
validates :channel_type, presence: true, inclusion: { in: %w[open invite_only direct] }
validates :status, presence: true, inclusion: { in: %w[active inactive blocked] }
validates :channel_type, presence: true, inclusion: { in: CHANNEL_TYPES }
validates :status, presence: true, inclusion: { in: STATUSES }
validates :slug, uniqueness: true, presence: true
validates :description, length: { maximum: 200 }, allow_blank: true

View file

@ -5,13 +5,17 @@ class ChatChannelMembership < ApplicationRecord
SEARCH_SERIALIZER = Search::ChatChannelMembershipSerializer
SEARCH_CLASS = Search::ChatChannelMembership
ROLES = %w[member mod].freeze
STATUSES = %w[active inactive pending rejected left_channel removed_from_channel joining_request].freeze
belongs_to :chat_channel
belongs_to :user
validates :user_id, presence: true, uniqueness: { scope: :chat_channel_id }
validates :chat_channel_id, presence: true, uniqueness: { scope: :user_id }
validates :status, inclusion: { in: %w[active inactive pending rejected left_channel removed_from_channel joining_request] }
validates :role, inclusion: { in: %w[member mod] }
validates :role, inclusion: { in: ROLES }
validates :status, inclusion: { in: STATUSES }
validates :user_id, presence: true
validate :permission
after_commit :index_to_elasticsearch, on: %i[create update]
@ -89,7 +93,11 @@ class ChatChannelMembership < ApplicationRecord
end
def permission
errors.add(:user_id, "is not allowed in chat") if chat_channel.direct? && chat_channel.slug.split("/").exclude?(user.username)
return unless chat_channel
return unless chat_channel.direct? && chat_channel.slug.split("/").exclude?(user.username)
errors.add(:user_id, "is not allowed in chat")
# To be possibly implemented in future
# if chat_channel.users.size > 128
# errors.add(:base, "too many members in channel")

View file

@ -35,7 +35,8 @@ class Organization < ApplicationRecord
validates :name, :summary, :url, :profile_image, presence: true
validates :name, length: { maximum: 50 }
validates :proof, length: { maximum: 1500 }
validates :secret, length: { is: 100 }, uniqueness: { allow_blank: true }, allow_blank: true
validates :secret, length: { is: 100 }, allow_nil: true
validates :secret, uniqueness: true
validates :slug, exclusion: { in: ReservedWords.all, message: MESSAGES[:reserved_word] }
validates :slug, format: { with: SLUG_REGEXP }, length: { in: 2..18 }
validates :slug, presence: true, uniqueness: { case_sensitive: false }

View file

@ -3,7 +3,6 @@ class ResponseTemplate < ApplicationRecord
belongs_to :user, optional: true
UNIQUENESS_SCOPE = %i[user_id type_of content_type].freeze
TYPE_OF_TYPES = %w[personal_comment mod_comment abuse_report_email_reply email_reply tag_adjustment].freeze
USER_NIL_TYPE_OF_TYPES = %w[mod_comment abuse_report_email_reply email_reply tag_adjustment].freeze
CONTENT_TYPES = %w[plain_text html body_markdown].freeze
@ -14,7 +13,7 @@ class ResponseTemplate < ApplicationRecord
USER_NIL_TYPE_OF_MSG = "cannot have a user ID associated.".freeze
validates :type_of, :content_type, :content, :title, presence: true
validates :content, uniqueness: { scope: UNIQUENESS_SCOPE }
validates :content, uniqueness: { scope: %i[user_id type_of content_type] }
validates :type_of, inclusion: { in: TYPE_OF_TYPES }
validates :content_type, inclusion: { in: CONTENT_TYPES }
validates :content_type,

View file

@ -0,0 +1,7 @@
class AddUniqueIndexToOrganizationsSecret < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_index :organizations, :secret, unique: true, algorithm: :concurrently
end
end

View file

@ -0,0 +1,7 @@
class AddUniqueIndexToChatChannelMembershipsChatChannelId < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_index :chat_channel_memberships, %i[chat_channel_id user_id], unique: true, algorithm: :concurrently
end
end

View file

@ -0,0 +1,7 @@
class AddUniqueIndexToChatChannelsSlug < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_index :chat_channels, :slug, unique: true, algorithm: :concurrently
end
end

View file

@ -0,0 +1,7 @@
class AddUniqueIndexToBadgeAchievementsBadgeId < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_index :badge_achievements, %i[badge_id user_id], unique: true, algorithm: :concurrently
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_05_25_125611) do
ActiveRecord::Schema.define(version: 2020_05_26_151807) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -163,6 +163,7 @@ ActiveRecord::Schema.define(version: 2020_05_25_125611) do
t.text "rewarding_context_message_markdown"
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["badge_id", "user_id"], name: "index_badge_achievements_on_badge_id_and_user_id", unique: true
t.index ["badge_id"], name: "index_badge_achievements_on_badge_id"
t.index ["user_id", "badge_id"], name: "index_badge_achievements_on_user_id_and_badge_id"
t.index ["user_id"], name: "index_badge_achievements_on_user_id"
@ -297,6 +298,7 @@ ActiveRecord::Schema.define(version: 2020_05_25_125611) do
t.string "status", default: "active"
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["chat_channel_id", "user_id"], name: "index_chat_channel_memberships_on_chat_channel_id_and_user_id", unique: true
t.index ["chat_channel_id"], name: "index_chat_channel_memberships_on_chat_channel_id"
t.index ["user_id", "chat_channel_id"], name: "index_chat_channel_memberships_on_user_id_and_chat_channel_id"
t.index ["user_id"], name: "index_chat_channel_memberships_on_user_id"
@ -312,6 +314,7 @@ ActiveRecord::Schema.define(version: 2020_05_25_125611) do
t.string "slug"
t.string "status", default: "active"
t.datetime "updated_at", null: false
t.index ["slug"], name: "index_chat_channels_on_slug", unique: true
end
create_table "classified_listing_categories", force: :cascade do |t|
@ -765,6 +768,7 @@ ActiveRecord::Schema.define(version: 2020_05_25_125611) do
t.integer "unspent_credits_count", default: 0, null: false
t.datetime "updated_at", null: false
t.string "url"
t.index ["secret"], name: "index_organizations_on_secret", unique: true
t.index ["slug"], name: "index_organizations_on_slug", unique: true
end

View file

@ -4,10 +4,14 @@ RSpec.describe BadgeAchievement, type: :model do
let_it_be(:achievement) { create(:badge_achievement) }
describe "validations" do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:badge) }
it { is_expected.to belong_to(:rewarder).class_name("User").optional }
it { is_expected.to validate_uniqueness_of(:badge_id).scoped_to(:user_id) }
describe "builtin validations" do
subject { achievement }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:badge) }
it { is_expected.to belong_to(:rewarder).class_name("User").optional }
it { is_expected.to validate_uniqueness_of(:badge_id).scoped_to(:user_id) }
end
end
it "turns rewarding_context_message_markdown into rewarding_context_message HTML" do

View file

@ -1,7 +1,30 @@
require "rails_helper"
RSpec.describe ChatChannelMembership, type: :model do
let(:chat_channel_membership) { FactoryBot.create(:chat_channel_membership) }
let(:chat_channel) { create(:chat_channel) }
let(:chat_channel_membership) { create(:chat_channel_membership, chat_channel: chat_channel) }
describe "validations" do
describe "builtin validations" do
subject { chat_channel_membership }
it { is_expected.to belong_to(:chat_channel) }
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_inclusion_of(:role).in_array(%w[member mod]) }
# rubocop:disable RSpec/NamedSubject
it {
expect(subject).to validate_inclusion_of(:status).
in_array(%w[active inactive pending rejected left_channel removed_from_channel joining_request])
}
# rubocop:enable RSpec/NamedSubject
it { is_expected.to validate_presence_of(:chat_channel_id) }
it { is_expected.to validate_presence_of(:user_id) }
it { is_expected.to validate_uniqueness_of(:chat_channel_id).scoped_to(:user_id) }
end
end
describe "#channel_text" do
it "sets channel text using name, slug, and human names" do

View file

@ -5,10 +5,22 @@ RSpec.describe ChatChannel, type: :model do
let_it_be(:users) { create_list(:user, 2) }
it { is_expected.to have_many(:messages).dependent(:destroy) }
it { is_expected.to have_many(:chat_channel_memberships).dependent(:destroy) }
it { is_expected.to have_many(:users) }
it { is_expected.to validate_presence_of(:channel_type) }
describe "validations" do
describe "builtin validations" do
subject { chat_channel }
it { is_expected.to have_many(:messages).dependent(:destroy) }
it { is_expected.to have_many(:chat_channel_memberships).dependent(:destroy) }
it { is_expected.to have_many(:users).through(:chat_channel_memberships) }
it { is_expected.to validate_inclusion_of(:channel_type).in_array(%w[open invite_only direct]) }
it { is_expected.to validate_inclusion_of(:status).in_array(%w[active inactive blocked]) }
it { is_expected.to validate_length_of(:description).is_at_most(200) }
it { is_expected.to validate_presence_of(:channel_type) }
it { is_expected.to validate_presence_of(:status) }
it { is_expected.to validate_uniqueness_of(:slug) }
end
end
describe "#clear_channel" do
before { allow(Pusher).to receive(:trigger) }

View file

@ -40,6 +40,7 @@ RSpec.describe Organization, type: :model do
it { is_expected.to validate_length_of(:company_size).is_at_most(7) }
it { is_expected.to validate_length_of(:story).is_at_most(640) }
it { is_expected.to validate_length_of(:tech_stack).is_at_most(640) }
it { is_expected.to validate_uniqueness_of(:secret).allow_nil }
it { is_expected.to validate_uniqueness_of(:slug).case_insensitive }
it { is_expected.not_to allow_value("#xyz").for(:bg_color_hex) }
@ -53,7 +54,21 @@ RSpec.describe Organization, type: :model do
end
end
describe "#after_commit" do
context "when callbacks are triggered before save" do
it "generates a secret if set to empty string" do
organization.secret = ""
organization.save
expect(organization.reload.secret).not_to eq("")
end
it "generates a secret if set to nil" do
organization.secret = nil
organization.save
expect(organization.reload.secret).not_to be(nil)
end
end
context "when callbacks are triggered after commit" do
it "on update syncs elasticsearch data" do
article = create(:article, organization: organization)
sidekiq_perform_enqueued_jobs
@ -269,10 +284,4 @@ RSpec.describe Organization, type: :model do
expect(organization.enough_credits?(1)).to be(true)
end
end
describe "#decoarated" do
it "returns not fully banished" do
expect(organization.decorate.fully_banished?).to eq(false)
end
end
end