Commit 1ecfca53 authored by Dylan Griffith's avatar Dylan Griffith

Add ability to sort to search API

Sorting already exists for search results from the web. Most of this
code for searching is shared. Unfortunately in the web search results we
had to implement this by using a single parameter. In order for our API
to be in line with other APIs it seemed preferable to implement sorting
as 2 params (field, direction).

To make matters worse the web sorting does things like `created_asc` for
sorting by `created_at asc`. So it wasn't even perfectly constructed by
concatenating field name and direction. Unfortunately the simplest
option here seemed to be to push the understanding of how to interpret
these different params into the searching code so it needs to handle 2
different meanings for the `sort` param.

I considered changing the web code to do a translation of params before
passing down to the `SearchService` but this was tricky due `params`
being passed in and also `@sort` is used by the view. We may in future
be able to refactor this to use 2 params but it seemed tricky to
introduce now with a single form field.
parent b1bfa935
...@@ -16,6 +16,7 @@ module Search ...@@ -16,6 +16,7 @@ module Search
Gitlab::SearchResults.new(current_user, Gitlab::SearchResults.new(current_user,
params[:search], params[:search],
projects, projects,
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] }) filters: { state: params[:state], confidential: params[:confidential] })
end end
......
...@@ -16,6 +16,7 @@ module Search ...@@ -16,6 +16,7 @@ module Search
params[:search], params[:search],
projects, projects,
group: group, group: group,
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] } filters: { state: params[:state], confidential: params[:confidential] }
) )
......
...@@ -17,6 +17,7 @@ module Search ...@@ -17,6 +17,7 @@ module Search
params[:search], params[:search],
project: project, project: project,
repository_ref: params[:repository_ref], repository_ref: params[:repository_ref],
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] } filters: { confidential: params[:confidential], state: params[:state] }
) )
......
---
title: Add ability to sort to search API
merge_request: 46646
author:
type: added
...@@ -26,6 +26,8 @@ GET /search ...@@ -26,6 +26,8 @@ GET /search
| `search` | string | yes | The search query | | `search` | string | yes | The search query |
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. | | `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. | | `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, users. Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, users.
...@@ -436,6 +438,8 @@ GET /groups/:id/search ...@@ -436,6 +438,8 @@ GET /groups/:id/search
| `search` | string | yes | The search query | | `search` | string | yes | The search query |
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. | | `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. | | `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, users. Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, users.
...@@ -816,6 +820,8 @@ GET /projects/:id/search ...@@ -816,6 +820,8 @@ GET /projects/:id/search
| `ref` | string | no | The name of a repository branch or tag to search on. The project's default branch is used by default. This is only applicable for scopes: commits, blobs, and wiki_blobs. | | `ref` | string | no | The name of a repository branch or tag to search on. The project's default branch is used by default. This is only applicable for scopes: commits, blobs, and wiki_blobs. |
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. | | `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. | | `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users. Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users.
......
...@@ -16,6 +16,7 @@ module EE ...@@ -16,6 +16,7 @@ module EE
params[:search], params[:search],
elastic_projects, elastic_projects,
public_and_internal_projects: elastic_global, public_and_internal_projects: elastic_global,
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] } filters: { confidential: params[:confidential], state: params[:state] }
) )
......
...@@ -30,6 +30,7 @@ module EE ...@@ -30,6 +30,7 @@ module EE
elastic_projects, elastic_projects,
group: group, group: group,
public_and_internal_projects: elastic_global, public_and_internal_projects: elastic_global,
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] } filters: { confidential: params[:confidential], state: params[:state] }
) )
......
...@@ -15,6 +15,7 @@ module EE ...@@ -15,6 +15,7 @@ module EE
params[:search], params[:search],
project: project, project: project,
repository_ref: repository_ref, repository_ref: repository_ref,
order_by: params[:order_by],
sort: params[:sort], sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] } filters: { confidential: params[:confidential], state: params[:state] }
) )
......
...@@ -137,14 +137,16 @@ module Elastic ...@@ -137,14 +137,16 @@ module Elastic
end end
def apply_sort(query_hash, options) def apply_sort(query_hash, options)
case options[:sort] # Due to different uses of sort param we prefer order_by when
when 'created_asc' # present
case [options[:order_by], options[:sort]]
when %w[created_at asc], [nil, 'created_asc']
query_hash.merge(sort: { query_hash.merge(sort: {
created_at: { created_at: {
order: 'asc' order: 'asc'
} }
}) })
when 'created_desc' when %w[created_at desc], [nil, 'created_desc']
query_hash.merge(sort: { query_hash.merge(sort: {
created_at: { created_at: {
order: 'desc' order: 'desc'
......
...@@ -8,13 +8,15 @@ module Gitlab ...@@ -8,13 +8,15 @@ module Gitlab
class GroupSearchResults < Gitlab::Elastic::SearchResults class GroupSearchResults < Gitlab::Elastic::SearchResults
attr_reader :group, :default_project_filter, :filters attr_reader :group, :default_project_filter, :filters
def initialize(current_user, query, limit_project_ids = nil, group:, public_and_internal_projects: false, default_project_filter: false, sort: nil, filters: {}) # rubocop:disable Metrics/ParameterLists
def initialize(current_user, query, limit_project_ids = nil, group:, public_and_internal_projects: false, default_project_filter: false, order_by: nil, sort: nil, filters: {})
@group = group @group = group
@default_project_filter = default_project_filter @default_project_filter = default_project_filter
@filters = filters @filters = filters
super(current_user, query, limit_project_ids, public_and_internal_projects: public_and_internal_projects, sort: sort, filters: filters) super(current_user, query, limit_project_ids, public_and_internal_projects: public_and_internal_projects, order_by: order_by, sort: sort, filters: filters)
end end
# rubocop:enable Metrics/ParameterLists
end end
end end
end end
...@@ -8,11 +8,11 @@ module Gitlab ...@@ -8,11 +8,11 @@ module Gitlab
class ProjectSearchResults < Gitlab::Elastic::SearchResults class ProjectSearchResults < Gitlab::Elastic::SearchResults
attr_reader :project, :repository_ref, :filters attr_reader :project, :repository_ref, :filters
def initialize(current_user, query, project:, repository_ref: nil, sort: nil, filters: {}) def initialize(current_user, query, project:, repository_ref: nil, order_by: nil, sort: nil, filters: {})
@project = project @project = project
@repository_ref = repository_ref.presence || project.default_branch @repository_ref = repository_ref.presence || project.default_branch
super(current_user, query, [project.id], public_and_internal_projects: false, sort: sort, filters: filters) super(current_user, query, [project.id], public_and_internal_projects: false, order_by: order_by, sort: sort, filters: filters)
end end
private private
......
...@@ -7,17 +7,18 @@ module Gitlab ...@@ -7,17 +7,18 @@ module Gitlab
DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE
attr_reader :current_user, :query, :public_and_internal_projects, :sort, :filters attr_reader :current_user, :query, :public_and_internal_projects, :order_by, :sort, :filters
# Limit search results by passed projects # Limit search results by passed projects
# It allows us to search only for projects user has access to # It allows us to search only for projects user has access to
attr_reader :limit_project_ids attr_reader :limit_project_ids
def initialize(current_user, query, limit_project_ids = nil, public_and_internal_projects: true, sort: nil, filters: {}) def initialize(current_user, query, limit_project_ids = nil, public_and_internal_projects: true, order_by: nil, sort: nil, filters: {})
@current_user = current_user @current_user = current_user
@query = query @query = query
@limit_project_ids = limit_project_ids @limit_project_ids = limit_project_ids
@public_and_internal_projects = public_and_internal_projects @public_and_internal_projects = public_and_internal_projects
@order_by = order_by
@sort = sort @sort = sort
@filters = filters @filters = filters
end end
...@@ -202,6 +203,7 @@ module Gitlab ...@@ -202,6 +203,7 @@ module Gitlab
current_user: current_user, current_user: current_user,
project_ids: limit_project_ids, project_ids: limit_project_ids,
public_and_internal_projects: public_and_internal_projects, public_and_internal_projects: public_and_internal_projects,
order_by: order_by,
sort: sort sort: sort
} }
end end
...@@ -214,7 +216,7 @@ module Gitlab ...@@ -214,7 +216,7 @@ module Gitlab
def issues def issues
strong_memoize(:issues) do strong_memoize(:issues) do
options = base_options.merge(filters.slice(:sort, :confidential, :state)) options = base_options.merge(filters.slice(:order_by, :sort, :confidential, :state))
Issue.elastic_search(query, options: options) Issue.elastic_search(query, options: options)
end end
...@@ -235,7 +237,7 @@ module Gitlab ...@@ -235,7 +237,7 @@ module Gitlab
def merge_requests def merge_requests
strong_memoize(:merge_requests) do strong_memoize(:merge_requests) do
options = base_options.merge(filters.slice(:sort, :state)) options = base_options.merge(filters.slice(:order_by, :sort, :state))
MergeRequest.elastic_search(query, options: options) MergeRequest.elastic_search(query, options: options)
end end
......
...@@ -22,6 +22,7 @@ RSpec.describe API::Search, factory_default: :keep do ...@@ -22,6 +22,7 @@ RSpec.describe API::Search, factory_default: :keep do
it 'returns a different result for each page' do it 'returns a different result for each page' do
get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 } get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to eq(1) expect(json_response.count).to eq(1)
first = json_response.first first = json_response.first
...@@ -37,6 +38,30 @@ RSpec.describe API::Search, factory_default: :keep do ...@@ -37,6 +38,30 @@ RSpec.describe API::Search, factory_default: :keep do
end end
end end
shared_examples 'orderable by created_at' do |scope:|
it 'allows ordering results by created_at asc' do
get api(endpoint, user), params: { scope: scope, search: '*', order_by: 'created_at', sort: 'asc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats).to eq(created_ats.sort)
end
it 'allows ordering results by created_at desc' do
get api(endpoint, user), params: { scope: scope, search: '*', order_by: 'created_at', sort: 'desc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats).to eq(created_ats.sort.reverse)
end
end
shared_examples 'elasticsearch disabled' do shared_examples 'elasticsearch disabled' do
it 'returns 400 error for wiki_blobs, blobs and commits scope' do it 'returns 400 error for wiki_blobs, blobs and commits scope' do
get api(endpoint, user), params: { scope: 'wiki_blobs', search: 'awesome' } get api(endpoint, user), params: { scope: 'wiki_blobs', search: 'awesome' }
...@@ -61,6 +86,7 @@ RSpec.describe API::Search, factory_default: :keep do ...@@ -61,6 +86,7 @@ RSpec.describe API::Search, factory_default: :keep do
end end
it_behaves_like 'pagination', scope: 'merge_requests' it_behaves_like 'pagination', scope: 'merge_requests'
it_behaves_like 'orderable by created_at', scope: 'merge_requests'
it 'avoids N+1 queries' do it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new { get api(endpoint, user), params: { scope: 'merge_requests', search: '*' } } control = ActiveRecord::QueryRecorder.new { get api(endpoint, user), params: { scope: 'merge_requests', search: '*' } }
...@@ -213,6 +239,7 @@ RSpec.describe API::Search, factory_default: :keep do ...@@ -213,6 +239,7 @@ RSpec.describe API::Search, factory_default: :keep do
end end
it_behaves_like 'pagination', scope: 'issues' it_behaves_like 'pagination', scope: 'issues'
it_behaves_like 'orderable by created_at', scope: 'issues'
end end
unless level == :project unless level == :project
......
...@@ -39,7 +39,9 @@ module API ...@@ -39,7 +39,9 @@ module API
snippets: snippets?, snippets: snippets?,
basic_search: params[:basic_search], basic_search: params[:basic_search],
page: params[:page], page: params[:page],
per_page: params[:per_page] per_page: params[:per_page],
order_by: params[:order_by],
sort: params[:sort]
}.merge(additional_params) }.merge(additional_params)
results = SearchService.new(current_user, search_params).search_objects(preload_method) results = SearchService.new(current_user, search_params).search_objects(preload_method)
......
...@@ -4,10 +4,10 @@ module Gitlab ...@@ -4,10 +4,10 @@ module Gitlab
class GroupSearchResults < SearchResults class GroupSearchResults < SearchResults
attr_reader :group attr_reader :group
def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, sort: nil, filters: {}) def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, order_by: nil, sort: nil, filters: {})
@group = group @group = group
super(current_user, query, limit_projects, default_project_filter: default_project_filter, sort: sort, filters: filters) super(current_user, query, limit_projects, default_project_filter: default_project_filter, order_by: order_by, sort: sort, filters: filters)
end end
# rubocop:disable CodeReuse/ActiveRecord # rubocop:disable CodeReuse/ActiveRecord
......
...@@ -4,11 +4,11 @@ module Gitlab ...@@ -4,11 +4,11 @@ module Gitlab
class ProjectSearchResults < SearchResults class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref attr_reader :project, :repository_ref
def initialize(current_user, query, project:, repository_ref: nil, sort: nil, filters: {}) def initialize(current_user, query, project:, repository_ref: nil, order_by: nil, sort: nil, filters: {})
@project = project @project = project
@repository_ref = repository_ref.presence @repository_ref = repository_ref.presence
super(current_user, query, [project], sort: sort, filters: filters) super(current_user, query, [project], order_by: order_by, sort: sort, filters: filters)
end end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil) def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
......
...@@ -7,7 +7,7 @@ module Gitlab ...@@ -7,7 +7,7 @@ module Gitlab
DEFAULT_PAGE = 1 DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 20 DEFAULT_PER_PAGE = 20
attr_reader :current_user, :query, :sort, :filters attr_reader :current_user, :query, :order_by, :sort, :filters
# Limit search results by passed projects # Limit search results by passed projects
# It allows us to search only for projects user has access to # It allows us to search only for projects user has access to
...@@ -19,11 +19,12 @@ module Gitlab ...@@ -19,11 +19,12 @@ module Gitlab
# query # query
attr_reader :default_project_filter attr_reader :default_project_filter
def initialize(current_user, query, limit_projects = nil, sort: nil, default_project_filter: false, filters: {}) def initialize(current_user, query, limit_projects = nil, order_by: nil, sort: nil, default_project_filter: false, filters: {})
@current_user = current_user @current_user = current_user
@query = query @query = query
@limit_projects = limit_projects || Project.all @limit_projects = limit_projects || Project.all
@default_project_filter = default_project_filter @default_project_filter = default_project_filter
@order_by = order_by
@sort = sort @sort = sort
@filters = filters @filters = filters
end end
...@@ -128,10 +129,12 @@ module Gitlab ...@@ -128,10 +129,12 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def apply_sort(scope) def apply_sort(scope)
case sort # Due to different uses of sort param we prefer order_by when
when 'created_asc' # present
case [order_by, sort]
when %w[created_at asc], [nil, 'created_asc']
scope.reorder('created_at ASC') scope.reorder('created_at ASC')
when 'created_desc' when %w[created_at desc], [nil, 'created_desc']
scope.reorder('created_at DESC') scope.reorder('created_at DESC')
else else
scope.reorder('created_at DESC') scope.reorder('created_at DESC')
......
...@@ -23,6 +23,48 @@ RSpec.describe API::Search do ...@@ -23,6 +23,48 @@ RSpec.describe API::Search do
end end
end end
shared_examples 'orderable by created_at' do |scope:|
it 'allows ordering results by created_at asc' do
get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'asc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats.uniq.count).to be > 1
expect(created_ats).to eq(created_ats.sort)
end
it 'allows ordering results by created_at desc' do
get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'desc' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.count).to be > 1
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
expect(created_ats.uniq.count).to be > 1
expect(created_ats).to eq(created_ats.sort.reverse)
end
end
shared_examples 'issues orderable by created_at' do
before do
create_list(:issue, 3, title: 'sortable item', project: project)
end
it_behaves_like 'orderable by created_at', scope: :issues
end
shared_examples 'merge_requests orderable by created_at' do
before do
create_list(:merge_request, 3, :unique_branches, title: 'sortable item', target_project: repo_project, source_project: repo_project)
end
it_behaves_like 'orderable by created_at', scope: :merge_requests
end
shared_examples 'pagination' do |scope:, search: ''| shared_examples 'pagination' do |scope:, search: ''|
it 'returns a different result for each page' do it 'returns a different result for each page' do
get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 } get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 }
...@@ -121,6 +163,8 @@ RSpec.describe API::Search do ...@@ -121,6 +163,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:issue, project: project, title: 'another issue') create(:issue, project: project, title: 'another issue')
...@@ -181,6 +225,8 @@ RSpec.describe API::Search do ...@@ -181,6 +225,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
...@@ -354,6 +400,8 @@ RSpec.describe API::Search do ...@@ -354,6 +400,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:issue, project: project, title: 'another issue') create(:issue, project: project, title: 'another issue')
...@@ -374,6 +422,8 @@ RSpec.describe API::Search do ...@@ -374,6 +422,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
...@@ -506,6 +556,8 @@ RSpec.describe API::Search do ...@@ -506,6 +556,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues it_behaves_like 'ping counters', scope: :issues
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:issue, project: project, title: 'another issue') create(:issue, project: project, title: 'another issue')
...@@ -536,6 +588,8 @@ RSpec.describe API::Search do ...@@ -536,6 +588,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests it_behaves_like 'ping counters', scope: :merge_requests
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do describe 'pagination' do
before do before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
......
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