Commit 1b344b76 authored by Patrick Bair's avatar Patrick Bair

Merge branch '352938-add-saved-replies-to-db' into 'master'

Add saved replies to DB

See merge request gitlab-org/gitlab!80807
parents 9d5272fe 832f32cc
......@@ -135,6 +135,7 @@ class User < ApplicationRecord
has_many :u2f_registrations, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :webauthn_registrations
has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :saved_replies, class_name: '::Users::SavedReply'
has_one :user_synced_attributes_metadata, autosave: true
has_one :aws_role, class_name: 'Aws::Role'
......
# frozen_string_literal: true
module Users
class SavedReply < ApplicationRecord
self.table_name = 'saved_replies'
belongs_to :user
validates :user_id, :name, :content, presence: true
validates :name,
length: { maximum: 255 },
uniqueness: { scope: [:user_id] },
format: {
with: Gitlab::Regex.saved_reply_name_regex,
message: Gitlab::Regex.saved_reply_name_regex_message
}
validates :content, length: { maximum: 10000 }
end
end
# frozen_string_literal: true
class CreateSavedReplies < Gitlab::Database::Migration[1.0]
enable_lock_retries!
def up
create_table :saved_replies do |t|
t.references :user, index: false, null: false, foreign_key: { on_delete: :cascade }
t.timestamps_with_timezone null: false
t.text :name, null: false, limit: 255
t.text :content, null: false, limit: 10000
t.index [:user_id, :name], name: 'index_saved_replies_on_name_text_pattern_ops', unique: true, opclass: { name: :text_pattern_ops }
end
end
def down
drop_table :saved_replies, if_exists: true
end
end
5931c4981c89d65c5aaca05dc8375c2c21bb595e28354d6623986d906ece165d
\ No newline at end of file
......@@ -20171,6 +20171,26 @@ CREATE SEQUENCE saml_providers_id_seq
ALTER SEQUENCE saml_providers_id_seq OWNED BY saml_providers.id;
CREATE TABLE saved_replies (
id bigint NOT NULL,
user_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
name text NOT NULL,
content text NOT NULL,
CONSTRAINT check_0cb57dc22a CHECK ((char_length(content) <= 10000)),
CONSTRAINT check_2eb3366d7f CHECK ((char_length(name) <= 255))
);
CREATE SEQUENCE saved_replies_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE saved_replies_id_seq OWNED BY saved_replies.id;
CREATE TABLE schema_migrations (
version character varying NOT NULL,
finished_at timestamp with time zone DEFAULT now()
......@@ -22966,6 +22986,8 @@ ALTER TABLE ONLY saml_group_links ALTER COLUMN id SET DEFAULT nextval('saml_grou
ALTER TABLE ONLY saml_providers ALTER COLUMN id SET DEFAULT nextval('saml_providers_id_seq'::regclass);
ALTER TABLE ONLY saved_replies ALTER COLUMN id SET DEFAULT nextval('saved_replies_id_seq'::regclass);
ALTER TABLE ONLY scim_identities ALTER COLUMN id SET DEFAULT nextval('scim_identities_id_seq'::regclass);
ALTER TABLE ONLY scim_oauth_access_tokens ALTER COLUMN id SET DEFAULT nextval('scim_oauth_access_tokens_id_seq'::regclass);
......@@ -25112,6 +25134,9 @@ ALTER TABLE ONLY saml_group_links
ALTER TABLE ONLY saml_providers
ADD CONSTRAINT saml_providers_pkey PRIMARY KEY (id);
ALTER TABLE ONLY saved_replies
ADD CONSTRAINT saved_replies_pkey PRIMARY KEY (id);
ALTER TABLE ONLY schema_migrations
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
......@@ -28894,6 +28919,8 @@ CREATE UNIQUE INDEX index_saml_group_links_on_group_id_and_saml_group_name ON sa
CREATE INDEX index_saml_providers_on_group_id ON saml_providers USING btree (group_id);
CREATE UNIQUE INDEX index_saved_replies_on_name_text_pattern_ops ON saved_replies USING btree (user_id, name text_pattern_ops);
CREATE INDEX index_scim_identities_on_group_id ON scim_identities USING btree (group_id);
CREATE UNIQUE INDEX index_scim_identities_on_lower_extern_uid_and_group_id ON scim_identities USING btree (lower((extern_uid)::text), group_id);
......@@ -32789,6 +32816,9 @@ ALTER TABLE ONLY resource_milestone_events
ALTER TABLE ONLY term_agreements
ADD CONSTRAINT fk_rails_a88721bcdf FOREIGN KEY (term_id) REFERENCES application_setting_terms(id);
ALTER TABLE ONLY saved_replies
ADD CONSTRAINT fk_rails_a8bf5bf111 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipeline_artifacts
ADD CONSTRAINT fk_rails_a9e811a466 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
......@@ -460,6 +460,7 @@ reviews: :gitlab_main
routes: :gitlab_main
saml_group_links: :gitlab_main
saml_providers: :gitlab_main
saved_replies: :gitlab_main
schema_migrations: :gitlab_shared
scim_identities: :gitlab_main
scim_oauth_access_tokens: :gitlab_main
......
......@@ -459,6 +459,15 @@ module Gitlab
"can contain only lowercase letters, digits, '_' and '-'. " \
"Must start with a letter, and cannot end with '-' or '_'"
end
def saved_reply_name_regex
@saved_reply_name_regex ||= /\A[a-z]([a-z0-9\-_]*[a-z0-9])?\z/.freeze
end
def saved_reply_name_regex_message
"can contain only lowercase letters, digits, '_' and '-'. " \
"Must start with a letter, and cannot end with '-' or '_'"
end
end
end
......
# frozen_string_literal: true
FactoryBot.define do
factory :saved_reply, class: 'Users::SavedReply' do
sequence(:name) { |n| "saved_reply_#{n}" }
content { 'Saved Reply Content' }
user
end
end
......@@ -990,4 +990,19 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('../../../../../1.2.3') }
it { is_expected.not_to match('%2e%2e%2f1.2.3') }
end
describe '.saved_reply_name_regex' do
subject { described_class.saved_reply_name_regex }
it { is_expected.to match('test') }
it { is_expected.to match('test123') }
it { is_expected.to match('test-test') }
it { is_expected.to match('test-test_0123') }
it { is_expected.not_to match('test test') }
it { is_expected.not_to match('test-') }
it { is_expected.not_to match('/z/test_') }
it { is_expected.not_to match('.xtest_') }
it { is_expected.not_to match('.xt.est_') }
it { is_expected.not_to match('0test1') }
end
end
......@@ -116,6 +116,7 @@ RSpec.describe User do
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
it { is_expected.to have_many(:saved_replies).class_name('::Users::SavedReply') }
it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Users::SavedReply do
let_it_be(:saved_reply) { create(:saved_reply) }
describe 'validations' do
it { is_expected.to validate_presence_of(:user_id) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:content) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to([:user_id]) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_length_of(:content).is_at_most(10000) }
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment