diff --git a/app/models/achievements/achievement.rb b/app/models/achievements/achievement.rb new file mode 100644 index 0000000000000000000000000000000000000000..904961491b54225d130414d0bb08f95ef574e9b1 --- /dev/null +++ b/app/models/achievements/achievement.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Achievements + class Achievement < ApplicationRecord + include Avatarable + include StripAttribute + + belongs_to :namespace, inverse_of: :achievements, optional: false + + strip_attributes! :name, :description + + validates :name, + presence: true, + length: { maximum: 255 }, + uniqueness: { case_sensitive: false, scope: [:namespace_id] } + validates :description, length: { maximum: 1024 } + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 5ee4fbedd5f8dafbd1259472a8a47760f261f284..54cd97253cf06be8c6c03d693fffaefd0a37fa4d 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -86,6 +86,7 @@ class Namespace < ApplicationRecord has_many :issues, inverse_of: :namespace has_many :timelog_categories, class_name: 'TimeTracking::TimelogCategory' + has_many :achievements, class_name: 'Achievements::Achievement' validates :owner, presence: true, if: ->(n) { n.owner_required? } validates :name, diff --git a/db/docs/achievements.yml b/db/docs/achievements.yml new file mode 100644 index 0000000000000000000000000000000000000000..20f9d1616b321ec27aa248a76cbed70281e64aef --- /dev/null +++ b/db/docs/achievements.yml @@ -0,0 +1,10 @@ +--- +table_name: achievements +classes: +- Achievements::Achivement +feature_categories: +- users +description: Achievements which can be created by namespaces to award them to users +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105871 +milestone: '15.7' +gitlab_schema: gitlab_main diff --git a/db/migrate/20221202144210_create_achievements.rb b/db/migrate/20221202144210_create_achievements.rb new file mode 100644 index 0000000000000000000000000000000000000000..30b2fd528eebe310f1272ae610c55600e8f6fc21 --- /dev/null +++ b/db/migrate/20221202144210_create_achievements.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class CreateAchievements < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def up + create_table :achievements do |t| + t.references :namespace, + null: false, + index: false, + foreign_key: { on_delete: :cascade } + t.timestamps_with_timezone null: false + t.text :name, null: false, limit: 255 + t.text :avatar, limit: 255 + t.text :description, limit: 1024 + t.boolean :revokeable, default: false, null: false + t.index 'namespace_id, LOWER(name)', unique: true + end + end + + def down + drop_table :achievements + end +end diff --git a/db/schema_migrations/20221202144210 b/db/schema_migrations/20221202144210 new file mode 100644 index 0000000000000000000000000000000000000000..3b37793b1a96a883ba43e39b82a597f894886fda --- /dev/null +++ b/db/schema_migrations/20221202144210 @@ -0,0 +1 @@ +5e29c2ebe99ef811cac0f894b3a77d2d158ba43070fb924c663db4622b8e79d7 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 2465f70c59d35da5772ccbdbe879e60f47a62d6a..74c2a23ce4d1fa4e7046673468f1775eb7750b29 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10614,6 +10614,29 @@ CREATE SEQUENCE abuse_reports_id_seq ALTER SEQUENCE abuse_reports_id_seq OWNED BY abuse_reports.id; +CREATE TABLE achievements ( + id bigint NOT NULL, + namespace_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + name text NOT NULL, + avatar text, + description text, + revokeable boolean DEFAULT false NOT NULL, + CONSTRAINT check_5171b03f22 CHECK ((char_length(name) <= 255)), + CONSTRAINT check_a7a7b84a80 CHECK ((char_length(description) <= 1024)), + CONSTRAINT check_e174e93a9e CHECK ((char_length(avatar) <= 255)) +); + +CREATE SEQUENCE achievements_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE achievements_id_seq OWNED BY achievements.id; + CREATE TABLE agent_activity_events ( id bigint NOT NULL, agent_id bigint NOT NULL, @@ -23585,6 +23608,8 @@ ALTER SEQUENCE zoom_meetings_id_seq OWNED BY zoom_meetings.id; ALTER TABLE ONLY abuse_reports ALTER COLUMN id SET DEFAULT nextval('abuse_reports_id_seq'::regclass); +ALTER TABLE ONLY achievements ALTER COLUMN id SET DEFAULT nextval('achievements_id_seq'::regclass); + ALTER TABLE ONLY agent_activity_events ALTER COLUMN id SET DEFAULT nextval('agent_activity_events_id_seq'::regclass); ALTER TABLE ONLY agent_group_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_group_authorizations_id_seq'::regclass); @@ -25224,6 +25249,9 @@ ALTER TABLE ONLY gitlab_partitions_static.product_analytics_events_experimental_ ALTER TABLE ONLY abuse_reports ADD CONSTRAINT abuse_reports_pkey PRIMARY KEY (id); +ALTER TABLE ONLY achievements + ADD CONSTRAINT achievements_pkey PRIMARY KEY (id); + ALTER TABLE ONLY agent_activity_events ADD CONSTRAINT agent_activity_events_pkey PRIMARY KEY (id); @@ -28233,6 +28261,8 @@ CREATE UNIQUE INDEX idx_work_item_types_on_namespace_id_and_name_null_namespace CREATE INDEX index_abuse_reports_on_user_id ON abuse_reports USING btree (user_id); +CREATE UNIQUE INDEX "index_achievements_on_namespace_id_LOWER_name" ON achievements USING btree (namespace_id, lower(name)); + CREATE INDEX index_agent_activity_events_on_agent_id_and_recorded_at_and_id ON agent_activity_events USING btree (agent_id, recorded_at, id); CREATE INDEX index_agent_activity_events_on_agent_token_id ON agent_activity_events USING btree (agent_token_id) WHERE (agent_token_id IS NOT NULL); @@ -34753,6 +34783,9 @@ ALTER TABLE ONLY ci_runner_namespaces ALTER TABLE ONLY software_license_policies ADD CONSTRAINT fk_rails_87b2247ce5 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY achievements + ADD CONSTRAINT fk_rails_87e990f752 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY protected_environment_deploy_access_levels ADD CONSTRAINT fk_rails_898a13b650 FOREIGN KEY (protected_environment_id) REFERENCES protected_environments(id) ON DELETE CASCADE; diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index 96588c20bbfd0347274f0cb3e899c516154e5084..faafd01f24875b1991309dab5e6df8a2e7235cf6 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -1,4 +1,5 @@ abuse_reports: :gitlab_main +achievements: :gitlab_main agent_activity_events: :gitlab_main agent_group_authorizations: :gitlab_main agent_project_authorizations: :gitlab_main diff --git a/spec/factories/achievements/achievements.rb b/spec/factories/achievements/achievements.rb new file mode 100644 index 0000000000000000000000000000000000000000..080a0376999e4a4e33996d35bbc834d6de37ff5b --- /dev/null +++ b/spec/factories/achievements/achievements.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :achievement, class: 'Achievements::Achievement' do + namespace + + name { generate(:name) } + end +end diff --git a/spec/models/achievements/achievement_spec.rb b/spec/models/achievements/achievement_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..10c04d184af7813118c01c0ad6f9cf09692d833c --- /dev/null +++ b/spec/models/achievements/achievement_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Achievements::Achievement, type: :model, feature_category: :users do + describe 'associations' do + it { is_expected.to belong_to(:namespace).required } + end + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Avatarable) } + end + + describe 'validations' do + subject { create(:achievement) } + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to([:namespace_id]) } + it { is_expected.to validate_length_of(:name).is_at_most(255) } + it { is_expected.to validate_length_of(:description).is_at_most(1024) } + end + + describe '#name' do + it 'strips name' do + achievement = described_class.new(name: ' AchievementTest ') + achievement.valid? + + expect(achievement.name).to eq('AchievementTest') + end + end +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 24e10e11d3a28ae9137078fe80014260b3a4e586..80721e110498f5575696a34560c589c83edda5c7 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -34,6 +34,7 @@ it { is_expected.to have_many :member_roles } it { is_expected.to have_one :cluster_enabled_grant } it { is_expected.to have_many(:work_items) } + it { is_expected.to have_many :achievements } it do is_expected.to have_one(:ci_cd_settings).class_name('NamespaceCiCdSetting').inverse_of(:namespace).autosave(true)