Commit f1835b4f authored by Harish Ramachandran's avatar Harish Ramachandran Committed by Igor Drozdov

Migration for AdminNote

Create an AdminNote model with specs

Associate AdminNote with Namespace

Show the AdminNote on the Group

Namespace has the AdminNote nested attribute

Avoid downtime with this migration
parent 0d75eede
...@@ -30,9 +30,11 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -30,9 +30,11 @@ class Admin::GroupsController < Admin::ApplicationController
def new def new
@group = Group.new @group = Group.new
@group.build_admin_note
end end
def edit def edit
@group.build_admin_note unless @group.admin_note
end end
def create def create
...@@ -49,6 +51,8 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -49,6 +51,8 @@ class Admin::GroupsController < Admin::ApplicationController
end end
def update def update
@group.build_admin_note unless @group.admin_note
if @group.update(group_params) if @group.update(group_params)
redirect_to [:admin, @group], notice: _('Group was successfully updated.') redirect_to [:admin, @group], notice: _('Group was successfully updated.')
else else
...@@ -105,7 +109,10 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -105,7 +109,10 @@ class Admin::GroupsController < Admin::ApplicationController
:require_two_factor_authentication, :require_two_factor_authentication,
:two_factor_grace_period, :two_factor_grace_period,
:project_creation_level, :project_creation_level,
:subgroup_creation_level :subgroup_creation_level,
admin_note_attributes: [
:note
]
] ]
end end
end end
......
...@@ -46,6 +46,9 @@ class Namespace < ApplicationRecord ...@@ -46,6 +46,9 @@ class Namespace < ApplicationRecord
has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule' has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule'
has_one :package_setting_relation, inverse_of: :namespace, class_name: 'PackageSetting' has_one :package_setting_relation, inverse_of: :namespace, class_name: 'PackageSetting'
has_one :admin_note, inverse_of: :namespace
accepts_nested_attributes_for :admin_note, update_only: true
validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name, validates :name,
presence: true, presence: true,
......
# frozen_string_literal: true
class Namespace::AdminNote < ApplicationRecord
belongs_to :namespace, inverse_of: :admin_note
validates :namespace, presence: true
validates :note, length: { maximum: 1000 }
end
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
= render 'shared/group_form', f: f = render 'shared/group_form', f: f
= render 'shared/group_form_description', f: f = render 'shared/group_form_description', f: f
= render 'shared/admin/admin_note_form', f: f
= render_if_exists 'shared/old_repository_size_limit_setting', form: f, type: :group = render_if_exists 'shared/old_repository_size_limit_setting', form: f, type: :group
= render_if_exists 'admin/namespace_plan', f: f = render_if_exists 'admin/namespace_plan', f: f
......
...@@ -103,6 +103,8 @@ ...@@ -103,6 +103,8 @@
%span.monospace= project.full_path + '.git' %span.monospace= project.full_path + '.git'
.col-md-6 .col-md-6
= render 'shared/admin/admin_note'
- if can?(current_user, :admin_group_member, @group) - if can?(current_user, :admin_group_member, @group)
.card .card
.card-header .card-header
......
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
%legend= _('Admin notes') %legend= _('Admin notes')
.form-group.row .form-group.row
.col-sm-2.col-form-label .col-sm-2.col-form-label
= f.label :note, s_('AdminNote|Note') = f.label :note, s_('Admin|Note')
.col-sm-10 .col-sm-10
= f.text_area :note, class: 'form-control gl-form-input gl-form-textarea' = f.text_area :note, class: 'form-control gl-form-input gl-form-textarea'
- if @group.admin_note.present?
- text = @group.admin_note.note
.card.border-info
.card-header.bg-info.gl-text-white
= s_('Admin|Admin notes')
.card-body
%p= text
.form-group.row
= f.fields_for :admin_note do |an|
.col-sm-2.col-form-label.gl-text-right
= an.label :note, s_('Admin|Admin notes')
.col-sm-10
= an.text_area :note, class: 'form-control'
---
title: Allow admin users to define admin notes on groups.
merge_request: 47825
author:
type: added
# frozen_string_literal: true
class CreateAdminNotes < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
create_table_with_constraints :namespace_admin_notes do |t|
t.timestamps_with_timezone
t.references :namespace, null: false, foreign_key: { on_delete: :cascade }
t.text :note
t.text_limit :note, 1000
end
end
def down
drop_table :namespace_admin_notes
end
end
7c33bd30af66ebb9a837c72e2ced107f015d4a22c7b6393554a9299bf3907cc0
\ No newline at end of file
...@@ -14639,6 +14639,24 @@ CREATE SEQUENCE milestones_id_seq ...@@ -14639,6 +14639,24 @@ CREATE SEQUENCE milestones_id_seq
ALTER SEQUENCE milestones_id_seq OWNED BY milestones.id; ALTER SEQUENCE milestones_id_seq OWNED BY milestones.id;
CREATE TABLE namespace_admin_notes (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
namespace_id bigint NOT NULL,
note text,
CONSTRAINT check_e9d2e71b5d CHECK ((char_length(note) <= 1000))
);
CREATE SEQUENCE namespace_admin_notes_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE namespace_admin_notes_id_seq OWNED BY namespace_admin_notes.id;
CREATE TABLE namespace_aggregation_schedules ( CREATE TABLE namespace_aggregation_schedules (
namespace_id integer NOT NULL namespace_id integer NOT NULL
); );
...@@ -19508,6 +19526,8 @@ ALTER TABLE ONLY metrics_users_starred_dashboards ALTER COLUMN id SET DEFAULT ne ...@@ -19508,6 +19526,8 @@ ALTER TABLE ONLY metrics_users_starred_dashboards ALTER COLUMN id SET DEFAULT ne
ALTER TABLE ONLY milestones ALTER COLUMN id SET DEFAULT nextval('milestones_id_seq'::regclass); ALTER TABLE ONLY milestones ALTER COLUMN id SET DEFAULT nextval('milestones_id_seq'::regclass);
ALTER TABLE ONLY namespace_admin_notes ALTER COLUMN id SET DEFAULT nextval('namespace_admin_notes_id_seq'::regclass);
ALTER TABLE ONLY namespace_statistics ALTER COLUMN id SET DEFAULT nextval('namespace_statistics_id_seq'::regclass); ALTER TABLE ONLY namespace_statistics ALTER COLUMN id SET DEFAULT nextval('namespace_statistics_id_seq'::regclass);
ALTER TABLE ONLY namespaces ALTER COLUMN id SET DEFAULT nextval('namespaces_id_seq'::regclass); ALTER TABLE ONLY namespaces ALTER COLUMN id SET DEFAULT nextval('namespaces_id_seq'::regclass);
...@@ -20896,6 +20916,9 @@ ALTER TABLE ONLY milestone_releases ...@@ -20896,6 +20916,9 @@ ALTER TABLE ONLY milestone_releases
ALTER TABLE ONLY milestones ALTER TABLE ONLY milestones
ADD CONSTRAINT milestones_pkey PRIMARY KEY (id); ADD CONSTRAINT milestones_pkey PRIMARY KEY (id);
ALTER TABLE ONLY namespace_admin_notes
ADD CONSTRAINT namespace_admin_notes_pkey PRIMARY KEY (id);
ALTER TABLE ONLY namespace_aggregation_schedules ALTER TABLE ONLY namespace_aggregation_schedules
ADD CONSTRAINT namespace_aggregation_schedules_pkey PRIMARY KEY (namespace_id); ADD CONSTRAINT namespace_aggregation_schedules_pkey PRIMARY KEY (namespace_id);
...@@ -23123,6 +23146,8 @@ CREATE INDEX index_mr_metrics_on_target_project_id_merged_at_nulls_last ON merge ...@@ -23123,6 +23146,8 @@ CREATE INDEX index_mr_metrics_on_target_project_id_merged_at_nulls_last ON merge
CREATE INDEX index_mr_metrics_on_target_project_id_merged_at_time_to_merge ON merge_request_metrics USING btree (target_project_id, merged_at, created_at) WHERE (merged_at > created_at); CREATE INDEX index_mr_metrics_on_target_project_id_merged_at_time_to_merge ON merge_request_metrics USING btree (target_project_id, merged_at, created_at) WHERE (merged_at > created_at);
CREATE INDEX index_namespace_admin_notes_on_namespace_id ON namespace_admin_notes USING btree (namespace_id);
CREATE UNIQUE INDEX index_namespace_aggregation_schedules_on_namespace_id ON namespace_aggregation_schedules USING btree (namespace_id); CREATE UNIQUE INDEX index_namespace_aggregation_schedules_on_namespace_id ON namespace_aggregation_schedules USING btree (namespace_id);
CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON namespace_root_storage_statistics USING btree (namespace_id); CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON namespace_root_storage_statistics USING btree (namespace_id);
...@@ -25965,6 +25990,9 @@ ALTER TABLE ONLY approval_merge_request_rules_approved_approvers ...@@ -25965,6 +25990,9 @@ ALTER TABLE ONLY approval_merge_request_rules_approved_approvers
ALTER TABLE ONLY operations_feature_flags_clients ALTER TABLE ONLY operations_feature_flags_clients
ADD CONSTRAINT fk_rails_6650ed902c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_6650ed902c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY namespace_admin_notes
ADD CONSTRAINT fk_rails_666166ea7b FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY web_hook_logs ALTER TABLE ONLY web_hook_logs
ADD CONSTRAINT fk_rails_666826e111 FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_666826e111 FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE;
...@@ -2246,9 +2246,6 @@ msgstr "" ...@@ -2246,9 +2246,6 @@ msgstr ""
msgid "AdminDashboard|Error loading the statistics. Please try again" msgid "AdminDashboard|Error loading the statistics. Please try again"
msgstr "" msgstr ""
msgid "AdminNote|Note"
msgstr ""
msgid "AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered." msgid "AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered."
msgstr "" msgstr ""
...@@ -2684,6 +2681,12 @@ msgstr "" ...@@ -2684,6 +2681,12 @@ msgstr ""
msgid "Admin|Additional users must be reviewed and approved by a system administrator. Learn more about %{help_link_start}usage caps%{help_link_end}." msgid "Admin|Additional users must be reviewed and approved by a system administrator. Learn more about %{help_link_start}usage caps%{help_link_end}."
msgstr "" msgstr ""
msgid "Admin|Admin notes"
msgstr ""
msgid "Admin|Note"
msgstr ""
msgid "Admin|View pending user approvals" msgid "Admin|View pending user approvals"
msgstr "" msgstr ""
......
...@@ -37,6 +37,12 @@ RSpec.describe Admin::GroupsController do ...@@ -37,6 +37,12 @@ RSpec.describe Admin::GroupsController do
post :create, params: { group: { path: 'test', name: 'test' } } post :create, params: { group: { path: 'test', name: 'test' } }
end.to change { NamespaceSetting.count }.by(1) end.to change { NamespaceSetting.count }.by(1)
end end
it 'creates admin_note for group' do
expect do
post :create, params: { group: { path: 'test', name: 'test', admin_note_attributes: { note: 'test' } } }
end.to change { Namespace::AdminNote.count }.by(1)
end
end end
describe 'PUT #members_update' do describe 'PUT #members_update' do
......
...@@ -35,6 +35,7 @@ RSpec.describe 'Admin Groups' do ...@@ -35,6 +35,7 @@ RSpec.describe 'Admin Groups' do
expect(page).to have_field('group_path') expect(page).to have_field('group_path')
expect(page).to have_field('group_visibility_level_0') expect(page).to have_field('group_visibility_level_0')
expect(page).to have_field('description') expect(page).to have_field('description')
expect(page).to have_field('group_admin_note_attributes_note')
end end
end end
...@@ -47,10 +48,12 @@ RSpec.describe 'Admin Groups' do ...@@ -47,10 +48,12 @@ RSpec.describe 'Admin Groups' do
path_component = 'gitlab' path_component = 'gitlab'
group_name = 'GitLab group name' group_name = 'GitLab group name'
group_description = 'Description of group for GitLab' group_description = 'Description of group for GitLab'
group_admin_note = 'A note about this group by an admin'
fill_in 'group_path', with: path_component fill_in 'group_path', with: path_component
fill_in 'group_name', with: group_name fill_in 'group_name', with: group_name
fill_in 'group_description', with: group_description fill_in 'group_description', with: group_description
fill_in 'group_admin_note_attributes_note', with: group_admin_note
click_button "Create group" click_button "Create group"
expect(current_path).to eq admin_group_path(Group.find_by(path: path_component)) expect(current_path).to eq admin_group_path(Group.find_by(path: path_component))
...@@ -61,6 +64,8 @@ RSpec.describe 'Admin Groups' do ...@@ -61,6 +64,8 @@ RSpec.describe 'Admin Groups' do
expect(li_texts).to match group_name expect(li_texts).to match group_name
expect(li_texts).to match path_component expect(li_texts).to match path_component
expect(li_texts).to match group_description expect(li_texts).to match group_description
p_texts = content.all('p').collect(&:text).join('/n')
expect(p_texts).to match group_admin_note
end end
it 'shows the visibility level radio populated with the default value' do it 'shows the visibility level radio populated with the default value' do
...@@ -116,6 +121,16 @@ RSpec.describe 'Admin Groups' do ...@@ -116,6 +121,16 @@ RSpec.describe 'Admin Groups' do
expect(page).to have_link(group.name, href: group_path(group)) expect(page).to have_link(group.name, href: group_path(group))
end end
it 'has a note if one is available' do
group = create(:group, :private)
note_text = 'A group administrator note'
group.update!(admin_note_attributes: { note: note_text })
visit admin_group_path(group)
expect(page).to have_text(note_text)
end
end end
describe 'group edit' do describe 'group edit' do
...@@ -145,6 +160,36 @@ RSpec.describe 'Admin Groups' do ...@@ -145,6 +160,36 @@ RSpec.describe 'Admin Groups' do
expect(name_field.value).to eq original_name expect(name_field.value).to eq original_name
end end
it 'adding an admin note to group without one' do
group = create(:group, :private)
expect(group.admin_note).to be_nil
visit admin_group_edit_path(group)
admin_note_text = 'A note by an administrator'
fill_in 'group_admin_note_attributes_note', with: admin_note_text
click_button 'Save changes'
expect(page).to have_content(admin_note_text)
end
it 'editing an existing group admin note' do
admin_note_text = 'A note by an administrator'
new_admin_note_text = 'A new note by an administrator'
group = create(:group, :private)
group.create_admin_note(note: admin_note_text)
visit admin_group_edit_path(group)
admin_note_field = find('#group_admin_note_attributes_note')
expect(admin_note_field.value).to eq(admin_note_text)
fill_in 'group_admin_note_attributes_note', with: new_admin_note_text
click_button 'Save changes'
expect(page).to have_content(new_admin_note_text)
end
end end
describe 'add user into a group', :js do describe 'add user into a group', :js do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespace::AdminNote, type: :model do
let!(:namespace) { create(:namespace) }
describe 'associations' do
it { is_expected.to belong_to :namespace }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_length_of(:note).is_at_most(1000) }
end
end
...@@ -21,6 +21,7 @@ RSpec.describe Namespace do ...@@ -21,6 +21,7 @@ RSpec.describe Namespace do
it { is_expected.to have_many :custom_emoji } it { is_expected.to have_many :custom_emoji }
it { is_expected.to have_one :package_setting_relation } it { is_expected.to have_one :package_setting_relation }
it { is_expected.to have_one :onboarding_progress } it { is_expected.to have_one :onboarding_progress }
it { is_expected.to have_one :admin_note }
end end
describe 'validations' do describe 'validations' do
......
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