Commit 09da07e6 authored by Jan Provaznik's avatar Jan Provaznik Committed by Sean McGivern

move users method to public section

this is for EE to be able to call this (elastic search)
parent 7380e48c
......@@ -29,6 +29,7 @@ class SearchController < ApplicationController
@search_objects = search_service.search_objects
render_commits if @scope == 'commits'
eager_load_user_status if @scope == 'users'
check_single_commit_result
end
......@@ -54,6 +55,12 @@ class SearchController < ApplicationController
@search_objects = prepare_commits_for_rendering(@search_objects)
end
def eager_load_user_status
return if Feature.disabled?(:users_search, default_enabled: true)
@search_objects = @search_objects.eager_load(:status) # rubocop:disable CodeReuse/ActiveRecord
end
def check_single_commit_result
if @search_results.single_commit_result?
only_commit = @search_results.objects('commits').first
......
......@@ -366,7 +366,8 @@ module ProjectsHelper
blobs: :download_code,
commits: :download_code,
merge_requests: :read_merge_request,
notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet]
notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet],
members: :read_project_member
)
end
......
......@@ -201,6 +201,16 @@ module SearchHelper
def limited_count(count, limit = 1000)
count > limit ? "#{limit}+" : count
end
def search_tabs?(tab)
return false if Feature.disabled?(:users_search, default_enabled: true)
if @project
project_search_tabs?(:members)
else
can?(current_user, :read_users_list)
end
end
end
SearchHelper.prepend(EE::SearchHelper)
......@@ -23,7 +23,8 @@ module Search
def allowed_scopes
strong_memoize(:allowed_scopes) do
%w[issues merge_requests milestones]
allowed_scopes = %w[issues merge_requests milestones]
allowed_scopes << 'users' if Feature.enabled?(:users_search, default_enabled: true)
end
end
......
......@@ -11,6 +11,12 @@ module Search
@group = group
end
def execute
Gitlab::GroupSearchResults.new(
current_user, projects, group, params[:search], default_project_filter: default_project_filter
)
end
def projects
return Project.none unless group
return @projects if defined? @projects
......
......@@ -16,7 +16,12 @@ module Search
end
def scope
@scope ||= %w[notes issues merge_requests milestones wiki_blobs commits].delete(params[:scope]) { 'blobs' }
@scope ||= begin
allowed_scopes = %w[notes issues merge_requests milestones wiki_blobs commits]
allowed_scopes << 'users' if Feature.enabled?(:users_search, default_enabled: true)
allowed_scopes.delete(params[:scope]) { 'blobs' }
end
end
end
end
......
- users = capture_haml do
- if search_tabs?(:members)
%li{ class: active_when(@scope == 'users') }
= link_to search_filter_path(scope: 'users') do
Users
%span.badge.badge-pill
= limited_count(@search_results.limited_users_count)
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
......@@ -45,6 +53,7 @@
= _("Commits")
%span.badge.badge-pill
= @search_results.commits_count
= users
- elsif @show_snippets
%li{ class: active_when(@scope == 'snippet_blobs') }
......@@ -94,3 +103,4 @@
= _("Wiki")
%span.badge.badge-pill
= limited_count(@search_results.wiki_blobs_count)
= users
%ul.content-list
%li
.avatar-cell.d-none.d-sm-block
= user_avatar(user: user, user_name: user.name, css_class: 'd-none d-sm-inline avatar s40')
.user-info
= link_to user_path(user), class: 'd-none d-sm-inline' do
.item-title
= user.name
= user_status(user)
.cgray= user.to_reference
---
title: Add users search results to global search
merge_request: 21197
author: Alexis Reigel
type: added
......@@ -17,7 +17,7 @@ GET /search
| `scope` | string | yes | The scope to search in |
| `search` | string | yes | The search query |
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs.
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs, users.
If Elasticsearch is enabled additional scopes available are blobs, wiki_blobs and commits. Find more about [the feature](../integration/elasticsearch.md).
......@@ -255,7 +255,7 @@ Example response:
### Scope: snippet_blobs
```bash
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=snippet_blobs&search=test
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=snippet_blos&search=test
```
Example response:
......@@ -375,6 +375,27 @@ Example response:
]
```
### Scope: users
```bash
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=users&search=doe
```
Example response:
```json
[
{
"id": 1,
"name": "John Doe1",
"username": "user1",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
"web_url": "http://localhost/user1"
}
]
```
## Group Search API
Search within the specified group.
......@@ -391,7 +412,7 @@ GET /groups/:id/search
| `scope` | string | yes | The scope to search in |
| `search` | string | yes | The search query |
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones.
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, users.
If Elasticsearch is enabled additional scopes available are blobs, wiki_blobs and commits. Find more about [the feature](../integration/elasticsearch.md).
......@@ -687,6 +708,27 @@ Example response:
]
```
### Scope: users
```bash
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/3/search?scope=users&search=doe
```
Example response:
```json
[
{
"id": 1,
"name": "John Doe1",
"username": "user1",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
"web_url": "http://localhost/user1"
}
]
```
## Project Search API
Search within the specified project.
......@@ -703,7 +745,7 @@ GET /projects/:id/search
| `scope` | string | yes | The scope to search in |
| `search` | string | yes | The search query |
Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs.
Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users.
The response depends on the requested scope.
......@@ -1016,4 +1058,25 @@ Example response:
]
```
### Scope: users
```bash
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/6/search?scope=users&search=doe
```
Example response:
```json
[
{
"id": 1,
"name": "John Doe1",
"username": "user1",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
"web_url": "http://localhost/user1"
}
]
```
[ce-41763]: https://gitlab.com/gitlab-org/gitlab-ce/issues/41763
......@@ -9,7 +9,9 @@ module EE
override :execute
def execute
if ::Gitlab::CurrentSettings.elasticsearch_search?
::Gitlab::Elastic::SearchResults.new(current_user, params[:search], elastic_projects, elastic_global)
::Gitlab::Elastic::SearchResults.new(current_user, params[:search],
elastic_projects, projects,
elastic_global)
else
super
end
......
......@@ -3,6 +3,8 @@
module EE
module Search
module GroupService
extend ::Gitlab::Utils::Override
def elastic_projects
@elastic_projects ||= projects.pluck(:id) # rubocop:disable CodeReuse/ActiveRecord
end
......@@ -10,6 +12,15 @@ module EE
def elastic_global
false
end
override :execute
def execute
return super unless ::Gitlab::CurrentSettings.elasticsearch_search?
::Gitlab::Elastic::GroupSearchResults.new(
current_user, elastic_projects, projects, group, params[:search],
elastic_global, default_project_filter: default_project_filter)
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Elastic
# Always prefer to use the full class namespace when specifying a
# superclass inside a module, because autoloading can occur in a
# different order between execution environments.
class GroupSearchResults < Gitlab::Elastic::SearchResults
delegate :users, to: :generic_search_results
delegate :limited_users_count, to: :generic_search_results
attr_reader :group, :default_project_filter
def initialize(current_user, limit_project_ids, limit_projects, group, query, public_and_internal_projects, default_project_filter: false, per_page: 20)
super(current_user, query, limit_project_ids, limit_projects, public_and_internal_projects)
@default_project_filter = default_project_filter
@group = group
end
def objects(scope, page = nil)
case scope
when 'users'
users.page(page).per(per_page)
else
super
end
end
def generic_search_results
@generic_search_results ||= Gitlab::GroupSearchResults.new(current_user, limit_projects, group, query, default_project_filter: default_project_filter)
end
end
end
end
......@@ -8,6 +8,9 @@ module Gitlab
class ProjectSearchResults < Gitlab::Elastic::SearchResults
attr_reader :project, :repository_ref
delegate :users, to: :generic_search_results
delegate :limited_users_count, to: :generic_search_results
def initialize(current_user, query, project_id, repository_ref = nil)
@current_user = current_user
@project = Project.find(project_id)
......@@ -26,11 +29,17 @@ module Gitlab
wiki_blobs.page(page).per(per_page)
when 'commits'
commits(page: page, per_page: per_page)
when 'users'
users.page(page).per(per_page)
else
super
end
end
def generic_search_results
@generic_search_results ||= Gitlab::ProjectSearchResults.new(current_user, project, query, repository_ref)
end
def blobs_count
@blobs_count ||= blobs.total_count
end
......
......@@ -7,11 +7,15 @@ module Gitlab
# Limit search results by passed project ids
# It allows us to search only for projects user has access to
attr_reader :limit_project_ids
attr_reader :limit_project_ids, :limit_projects
def initialize(current_user, query, limit_project_ids, public_and_internal_projects = true)
delegate :users, to: :generic_search_results
delegate :limited_users_count, to: :generic_search_results
def initialize(current_user, query, limit_project_ids, limit_projects = nil, public_and_internal_projects = true)
@current_user = current_user
@limit_project_ids = limit_project_ids
@limit_projects = limit_projects
@query = query
@public_and_internal_projects = public_and_internal_projects
end
......@@ -32,11 +36,17 @@ module Gitlab
wiki_blobs.page(page).per(per_page)
when 'commits'
commits(page: page, per_page: per_page)
when 'users'
users.page(page).per(per_page)
else
Kaminari.paginate_array([])
end
end
def generic_search_results
@generic_search_results ||= Gitlab::SearchResults.new(current_user, limit_projects, query)
end
def projects_count
@projects_count ||= projects.total_count
end
......
require 'spec_helper'
describe Gitlab::Elastic::GroupSearchResults do
set(:user) { create(:user) }
set(:group) { create(:group) }
set(:guest) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::GUEST) } }
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
context 'user search' do
subject(:results) { described_class.new(user, nil, nil, group, guest.username, nil) }
before do
expect(Gitlab::GroupSearchResults).to receive(:new).and_call_original
end
it { expect(results.objects('users')).to eq([guest]) }
it { expect(results.limited_users_count).to eq(1) }
end
end
......@@ -224,4 +224,15 @@ describe Gitlab::Elastic::ProjectSearchResults do
expect(results.issues_count).to eq 3
end
end
context 'user search' do
subject(:results) { described_class.new(user, project.owner.username, project.id) }
before do
expect(Gitlab::ProjectSearchResults).to receive(:new).and_call_original
end
it { expect(results.objects('users')).to eq([project.owner]) }
it { expect(results.limited_users_count).to eq(1) }
end
end
......@@ -5,17 +5,17 @@ module API
module SearchHelpers
def self.global_search_scopes
# This is a separate method so that EE can redefine it.
%w(projects issues merge_requests milestones snippet_titles snippet_blobs)
%w(projects issues merge_requests milestones snippet_titles snippet_blobs users)
end
def self.group_search_scopes
# This is a separate method so that EE can redefine it.
%w(projects issues merge_requests milestones)
%w(projects issues merge_requests milestones users)
end
def self.project_search_scopes
# This is a separate method so that EE can redefine it.
%w(issues merge_requests milestones notes wiki_blobs commits blobs)
%w(issues merge_requests milestones notes wiki_blobs commits blobs users)
end
end
end
......
......@@ -17,7 +17,8 @@ module API
blobs: Entities::Blob,
wiki_blobs: Entities::Blob,
snippet_titles: Entities::Snippet,
snippet_blobs: Entities::Snippet
snippet_blobs: Entities::Snippet,
users: Entities::UserBasic
}.freeze
def search(additional_params = {})
......@@ -51,6 +52,12 @@ module API
# Defining this method here as a noop allows us to easily extend it in
# EE, without having to modify this file directly.
end
def check_users_search_allowed!
if params[:scope].to_sym == :users && Feature.disabled?(:users_search, default_enabled: true)
render_api_error!({ error: _("Scope not supported with disabled 'users_search' feature!") }, 400)
end
end
end
resource :search do
......@@ -67,6 +74,7 @@ module API
end
get do
verify_search_scope!
check_users_search_allowed!
present search, with: entity
end
......@@ -87,6 +95,7 @@ module API
end
get ':id/(-/)search' do
verify_search_scope!
check_users_search_allowed!
present search(group_id: user_group.id), with: entity
end
......@@ -106,6 +115,8 @@ module API
use :pagination
end
get ':id/(-/)search' do
check_users_search_allowed!
present search(project_id: user_project.id), with: entity
end
end
......
# frozen_string_literal: true
module Gitlab
class GroupSearchResults < SearchResults
def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20)
super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page)
@group = group
end
# rubocop:disable CodeReuse/ActiveRecord
def users
# 1: get all groups the current user has access to
groups = GroupsFinder.new(current_user).execute.joins(:users)
# 2: Get the group's whole hierarchy
group_users = @group.direct_and_indirect_users
# 3: get all users the current user has access to (->
# `SearchResults#users`), which also applies the query.
users = super
# 4: filter for users that belong to the previously selected groups
users
.where(id: group_users.select('id'))
.where(id: groups.select('members.user_id'))
end
# rubocop:enable CodeReuse/ActiveRecord
end
end
......@@ -22,11 +22,17 @@ module Gitlab
paginated_blobs(wiki_blobs, page)
when 'commits'
Kaminari.paginate_array(commits).page(page).per(per_page)
when 'users'
users.page(page).per(per_page)
else
super(scope, page, false)
end
end
def users
super.where(id: @project.team.members) # rubocop:disable CodeReuse/ActiveRecord
end
def blobs_count
@blobs_count ||= blobs.count
end
......
......@@ -32,6 +32,8 @@ module Gitlab
merge_requests.page(page).per(per_page)
when 'milestones'
milestones.page(page).per(per_page)
when 'users'
users.page(page).per(per_page)
else
Kaminari.paginate_array([]).page(page).per(per_page)
end
......@@ -71,6 +73,12 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop:disable CodeReuse/ActiveRecord
def limited_users_count
@limited_users_count ||= users.limit(count_limit).count
end
# rubocop:enable CodeReuse/ActiveRecord
def single_commit_result?
false
end
......@@ -79,6 +87,12 @@ module Gitlab
1001
end
def users
return User.none unless Ability.allowed?(current_user, :read_users_list)
UsersFinder.new(current_user, search: query).execute
end
private
def projects
......
......@@ -8816,6 +8816,9 @@ msgstr ""
msgid "Scope"
msgstr ""
msgid "Scope not supported with disabled 'users_search' feature!"
msgstr ""
msgid "Scoped issue boards"
msgstr ""
......
require 'spec_helper'
describe 'User searches for users' do
context 'when on the dashboard' do
it 'finds the user' do
create(:user, username: 'gob_bluth', name: 'Gob Bluth')
sign_in(create(:user))
visit dashboard_projects_path
fill_in 'search', with: 'gob'
click_button 'Go'
expect(page).to have_content('Users 1')
click_on('Users 1')
expect(page).to have_content('Gob Bluth')
expect(page).to have_content('@gob_bluth')
end
end
context 'when on the project page' do
it 'finds the user belonging to the project' do
project = create(:project)
user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth')
create(:project_member, :developer, user: user1, project: project)
user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth')
create(:project_member, :developer, user: user2, project: project)
create(:user, username: 'gob_2018', name: 'George Oscar Bluth')
sign_in(user1)
visit projects_path(project)
fill_in 'search', with: 'gob'
click_button 'Go'
expect(page).to have_content('Gob Bluth')
expect(page).to have_content('@gob_bluth')
expect(page).not_to have_content('Michael Bluth')
expect(page).not_to have_content('@michael_bluth')
expect(page).not_to have_content('George Oscar Bluth')
expect(page).not_to have_content('@gob_2018')
end
end
context 'when on the group page' do
it 'finds the user belonging to the group' do
group = create(:group)
user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth')
create(:group_member, :developer, user: user1, group: group)
user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth')
create(:group_member, :developer, user: user2, group: group)
create(:user, username: 'gob_2018', name: 'George Oscar Bluth')
sign_in(user1)
visit group_path(group)
fill_in 'search', with: 'gob'
click_button 'Go'
expect(page).to have_content('Gob Bluth')
expect(page).to have_content('@gob_bluth')
expect(page).not_to have_content('Michael Bluth')
expect(page).not_to have_content('@michael_bluth')
expect(page).not_to have_content('George Oscar Bluth')
expect(page).not_to have_content('@gob_2018')
end
end
end
require 'spec_helper'
describe Gitlab::GroupSearchResults do
let(:user) { create(:user) }
describe 'user search' do
let(:group) { create(:group) }
it 'returns the users belonging to the group matching the search query' do
user1 = create(:user, username: 'gob_bluth')
create(:group_member, :developer, user: user1, group: group)
user2 = create(:user, username: 'michael_bluth')
create(:group_member, :developer, user: user2, group: group)
create(:user, username: 'gob_2018')
result = described_class.new(user, anything, group, 'gob').objects('users')
expect(result).to eq [user1]
end
it 'returns the user belonging to the subgroup matching the search query', :nested_groups do
user1 = create(:user, username: 'gob_bluth')
subgroup = create(:group, parent: group)
create(:group_member, :developer, user: user1, group: subgroup)
create(:user, username: 'gob_2018')
result = described_class.new(user, anything, group, 'gob').objects('users')
expect(result).to eq [user1]
end
it 'returns the user belonging to the parent group matching the search query', :nested_groups do
user1 = create(:user, username: 'gob_bluth')
parent_group = create(:group, children: [group])
create(:group_member, :developer, user: user1, group: parent_group)
create(:user, username: 'gob_2018')
result = described_class.new(user, anything, group, 'gob').objects('users')
expect(result).to eq [user1]
end
it 'does not return the user belonging to the private subgroup', :nested_groups do
user1 = create(:user, username: 'gob_bluth')
subgroup = create(:group, :private, parent: group)
create(:group_member, :developer, user: user1, group: subgroup)
create(:user, username: 'gob_2018')
result = described_class.new(user, anything, group, 'gob').objects('users')
expect(result).to eq []
end
it 'does not return the user belonging to an unrelated group' do
user = create(:user, username: 'gob_bluth')
unrelated_group = create(:group)
create(:group_member, :developer, user: user, group: unrelated_group)
result = described_class.new(user, anything, group, 'gob').objects('users')
expect(result).to eq []
end
end
end
......@@ -412,4 +412,36 @@ describe Gitlab::ProjectSearchResults do
end
end
end
describe 'user search' do
it 'returns the user belonging to the project matching the search query' do
project = create(:project)
user1 = create(:user, username: 'gob_bluth')
create(:project_member, :developer, user: user1, project: project)
user2 = create(:user, username: 'michael_bluth')
create(:project_member, :developer, user: user2, project: project)
create(:user, username: 'gob_2018')
result = described_class.new(user, project, 'gob').objects('users')
expect(result).to eq [user1]
end
it 'returns the user belonging to the group matching the search query' do
group = create(:group)
project = create(:project, namespace: group)
user1 = create(:user, username: 'gob_bluth')
create(:group_member, :developer, user: user1, group: group)
create(:user, username: 'gob_2018')
result = described_class.new(user, project, 'gob').objects('users')
expect(result).to eq [user1]
end
end
end
......@@ -121,6 +121,22 @@ describe Gitlab::SearchResults do
results.objects('issues')
end
end
describe '#users' do
it 'does not call the UsersFinder when the current_user is not allowed to read users list' do
allow(Ability).to receive(:allowed?).and_return(false)
expect(UsersFinder).not_to receive(:new).with(user, search: 'foo').and_call_original
results.objects('users')
end
it 'calls the UsersFinder' do
expect(UsersFinder).to receive(:new).with(user, search: 'foo').and_call_original
results.objects('users')
end
end
end
it 'does not list issues on private projects' do
......
......@@ -77,6 +77,28 @@ describe API::Search do
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
context 'for users scope' do
before do
create(:user, name: 'billy')
get api('/search', user), params: { scope: 'users', search: 'billy' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
context 'when users search feature is disabled' do
before do
allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true)
get api('/search', user), params: { scope: 'users', search: 'billy' }
end
it 'returns 400 error' do
expect(response).to have_gitlab_http_status(400)
end
end
end
context 'for snippet_titles scope' do
before do
create(:snippet, :public, title: 'awesome snippet', content: 'snippet content')
......@@ -192,6 +214,40 @@ describe API::Search do
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
context 'for users scope' do
before do
user = create(:user, name: 'billy')
create(:group_member, :developer, user: user, group: group)
get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
context 'when users search feature is disabled' do
before do
allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true)
get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' }
end
it 'returns 400 error' do
expect(response).to have_gitlab_http_status(400)
end
end
end
context 'for users scope with group path as id' do
before do
user1 = create(:user, name: 'billy')
create(:group_member, :developer, user: user1, group: group)
get api("/groups/#{CGI.escape(group.full_path)}/search", user), params: { scope: 'users', search: 'billy' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
end
end
end
......@@ -269,6 +325,29 @@ describe API::Search do
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
context 'for users scope' do
before do
user1 = create(:user, name: 'billy')
create(:project_member, :developer, user: user1, project: project)
get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
context 'when users search feature is disabled' do
before do
allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true)
get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' }
end
it 'returns 400 error' do
expect(response).to have_gitlab_http_status(400)
end
end
end
context 'for notes scope' do
before do
create(:note_on_merge_request, project: project, note: 'awesome note')
......
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