Skip to content

Commit

Permalink
Merge pull request #16776 from opf/bug/57550-custom-field-with-format…
Browse files Browse the repository at this point in the history
…-version-are-ordered-as-strings

Bug/57550 custom field with format version are ordered as strings
  • Loading branch information
ulferts authored Oct 21, 2024
2 parents 846b5b0 + 8f23e53 commit ec2b460
Show file tree
Hide file tree
Showing 8 changed files with 59 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def caption
},
version: {
association: "version",
sortable: [->(table_name = Version.table_name) { Version.semver_sql(table_name) }, "name"],
sortable: "name",
default_order: "ASC",
null_handling: "NULLS LAST",
groupable: "#{WorkPackage.table_name}.version_id"
Expand Down
16 changes: 1 addition & 15 deletions app/models/versions/scopes/order_by_semver_name.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,7 @@ module OrderBySemverName

class_methods do
def order_by_semver_name
reorder semver_sql, :name
end

# Returns an sql for ordering which:
# * Returns a substring from the beginning of the name up until the first alphabetical character e.g. "1.2.3 "
# from "1.2.3 ABC"
# * Replaces all non numerical character groups in that substring by a blank, e.g "1.2.3 " to "1 2 3 "
# * Splits the result into an array of individual number parts, e.g. "{1, 2, 3, ''}" from "1 2 3 "
# * removes all empty array items, e.g. "{1, 2, 3}" from "{1, 2, 3, ''}"
def semver_sql(table_name = Version.table_name)
sql = <<~SQL
array_remove(regexp_split_to_array(regexp_replace(substring(#{table_name}.name from '^[^a-zA-Z]+'), '\\D+', ' ', 'g'), ' '), '')::int[]
SQL

Arel.sql(sql)
order :name
end
end
end
Expand Down
13 changes: 13 additions & 0 deletions db/migrate/20240920152544_set_versions_name_collation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class SetVersionsNameCollation < ActiveRecord::Migration[7.1]
def up
execute <<-SQL.squish
CREATE COLLATION IF NOT EXISTS versions_name (provider = icu, locale = 'en-u-kn-true');
SQL

change_column :versions, :name, :string, collation: "versions_name"
end

def down
change_column :versions, :name, :string
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,10 @@ def project_without_cf_value

let(:projects) do
[
project_with_cf_value(id_by_name.fetch("10.10.10")),
project_with_cf_value(id_by_name.fetch("10.10.2")),
project_with_cf_value(id_by_name.fetch("10.2")),
project_with_cf_value(id_by_name.fetch("9")),
project_with_cf_value(id_by_name.fetch("10.2")),
project_with_cf_value(id_by_name.fetch("10.10.2")),
project_with_cf_value(id_by_name.fetch("10.10.10")),
project # TODO: should be at index 0
]
end
Expand All @@ -332,12 +332,12 @@ def project_without_cf_value

let(:projects) do
[
project_with_cf_value(*id_by_name.fetch_values("10.10.10")), # 10.10.10
project_with_cf_value(*id_by_name.fetch_values("9", "10.10.10")), # 10.10.10, 9
project_with_cf_value(*id_by_name.fetch_values("10.10.10", "9")), # 10.10.10, 9
project_with_cf_value(*id_by_name.fetch_values("10.10.2", "9")), # 9, 10.10.2
project_with_cf_value(*id_by_name.fetch_values("10.10.10", "9")), # 9, 10.10.10
project_with_cf_value(*id_by_name.fetch_values("9", "10.10.10")), # 9, 10.10.10
project_with_cf_value(*id_by_name.fetch_values("10.2", "10.10.2")), # 10.2, 10.10.2
project_with_cf_value(*id_by_name.fetch_values("10.10.2")), # 10.10.2
project_with_cf_value(*id_by_name.fetch_values("10.2", "10.10.2")), # 10.10.2, 10.2
project_with_cf_value(*id_by_name.fetch_values("10.10.2", "9")), # 10.10.2, 9
project_with_cf_value(*id_by_name.fetch_values("10.10.10")), # 10.10.10
project # TODO: should be at index 0
]
end
Expand Down
16 changes: 8 additions & 8 deletions spec/models/query/results_cf_sorting_integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,10 @@ def wp_without_cf_value

let(:work_packages) do
[
wp_with_cf_value(id_by_name.fetch("10.10.10")),
wp_with_cf_value(id_by_name.fetch("10.10.2")),
wp_with_cf_value(id_by_name.fetch("10.2")),
wp_with_cf_value(id_by_name.fetch("9")),
wp_with_cf_value(id_by_name.fetch("10.2")),
wp_with_cf_value(id_by_name.fetch("10.10.2")),
wp_with_cf_value(id_by_name.fetch("10.10.10")),
wp_without_cf_value # TODO: should be at index 0
]
end
Expand All @@ -324,12 +324,12 @@ def wp_without_cf_value

let(:work_packages) do
[
wp_with_cf_value(id_by_name.fetch_values("10.10.10")), # 10.10.10
wp_with_cf_value(id_by_name.fetch_values("9", "10.10.10")), # 10.10.10, 9
wp_with_cf_value(id_by_name.fetch_values("10.10.10", "9")), # 10.10.10, 9
wp_with_cf_value(id_by_name.fetch_values("10.10.2", "9")), # 9, 10.10.2
wp_with_cf_value(id_by_name.fetch_values("10.10.10", "9")), # 9, 10.10.10
wp_with_cf_value(id_by_name.fetch_values("9", "10.10.10")), # 9, 10.10.10
wp_with_cf_value(id_by_name.fetch_values("10.2", "10.10.2")), # 10.2, 10.10.2
wp_with_cf_value(id_by_name.fetch_values("10.10.2")), # 10.10.2
wp_with_cf_value(id_by_name.fetch_values("10.2", "10.10.2")), # 10.10.2, 10.2
wp_with_cf_value(id_by_name.fetch_values("10.10.2", "9")), # 10.10.2, 9
wp_with_cf_value(id_by_name.fetch_values("10.10.10")), # 10.10.10
wp_without_cf_value # TODO: should be at index 0
]
end
Expand Down
18 changes: 8 additions & 10 deletions spec/models/query/results_version_integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,23 @@

let(:old_version) do
create(:version,
name: "1. Old version",
name: "4. Old version",
project:,
start_date: "2019-02-02",
effective_date: "2019-02-03")
end

let(:new_version) do
create(:version,
name: "1.2 New version",
name: "10.2 New version",
project:,
start_date: "2020-02-02",
effective_date: "2020-02-03")
end

let(:no_date_version) do
create(:version,
name: "1.1 No date version",
name: "10.1 No date version",
project:,
start_date: nil,
effective_date: nil)
Expand All @@ -69,13 +69,13 @@
subject: "No version wp",
project:)
end
let!(:newest_version_wp) do
let!(:new_version_wp) do
create(:work_package,
subject: "Newest version wp",
version: new_version,
project:)
end
let!(:oldest_version_wp) do
let!(:old_version_wp) do
create(:work_package,
subject: "Oldest version wp",
version: old_version,
Expand All @@ -101,7 +101,8 @@
q.sort_criteria = sort_criteria
end
end
let(:work_packages_asc) { [oldest_version_wp, no_date_version_wp, newest_version_wp, no_version_wp] }
let(:work_packages_asc) { [old_version_wp, no_date_version_wp, new_version_wp, no_version_wp] }
let(:work_packages_desc) { [new_version_wp, no_date_version_wp, old_version_wp, no_version_wp] }

before do
login_as(user)
Expand Down Expand Up @@ -136,11 +137,8 @@
let(:sort_criteria) { [["version", "desc"]] }

it "returns the correctly sorted result" do
# null values are still sorted last
work_packages_order = [newest_version_wp, no_date_version_wp, oldest_version_wp, no_version_wp]

expect(query_results.work_packages.pluck(:id))
.to match work_packages_order.map(&:id)
.to match work_packages_desc.map(&:id)
end
end
end
6 changes: 1 addition & 5 deletions spec/models/query/sort_criteria_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,8 @@
let(:sort_criteria) { [%w[version desc], %w[start_date asc]] }

it "adds the order handling (and the default order by id)" do
sort_sql = <<~SQL
array_remove(regexp_split_to_array(regexp_replace(substring(versions.name from '^[^a-zA-Z]+'), '\\D+', ' ', 'g'), ' '), '')::int[]
SQL

expect(subject)
.to eq [["#{sort_sql} DESC NULLS LAST", "name DESC NULLS LAST"],
.to eq [["name DESC NULLS LAST"],
["work_packages.start_date NULLS LAST"],
["work_packages.id DESC"]]
end
Expand Down
43 changes: 19 additions & 24 deletions spec/models/versions/scopes/order_by_semver_name_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,27 @@

RSpec.describe Versions::Scopes::OrderBySemverName do
let(:project) { create(:project) }
let!(:version1) do
create(:version, name: "aaaaa 1.", project:)
end
let!(:version2) do
create(:version, name: "aaaaa", project:)
end
let!(:version3) do
create(:version, name: "1.10. aaa", project:)
end
let!(:version4) do
create(:version, name: "1.1. zzz", project:, start_date: Date.today, effective_date: Date.today + 1.day)
end
let!(:version5) do
create(:version, name: "1.2. mmm", project:, start_date: Date.today)
end
let!(:version6) do
create(:version, name: "1. xxxx", project:, start_date: Date.today + 5.days)
end
let!(:version7) do
create(:version, name: "1.1. aaa", project:)
end
let(:names) do
[
"1. xxxx",
"1.1. aaa",
"1.1. zzz",
"1.2. mmm",
"1.10. aaa",
"9",
"10.2",
"10.10.2",
"10.10.10",
"aaaaa",
"aaaaa 1."
]
end
let!(:versions) { names.map { |name| create(:version, name:, project:) } }

subject { Version.order_by_semver_name }
subject { Version.order_by_semver_name.order(id: :desc).to_a }

it "returns the versions in semver order" do
expect(subject.to_a)
.to eql [version6, version7, version4, version5, version3, version2, version1]
expect(subject)
.to eql versions
end
end

0 comments on commit ec2b460

Please sign in to comment.