##// END OF EJS Templates
Replaces acts_as_list with an implementation that handles #position= (#12909)....
Jean-Philippe Lang -
r14953:64afa24a7f72
parent child
Show More
@@ -0,0 +1,13
1 class RemovePositionDefaults < ActiveRecord::Migration
2 def up
3 [Board, CustomField, Enumeration, IssueStatus, Role, Tracker].each do |klass|
4 change_column klass.table_name, :position, :integer, :default => nil
5 end
6 end
7
8 def down
9 [Board, CustomField, Enumeration, IssueStatus, Role, Tracker].each do |klass|
10 change_column klass.table_name, :position, :integer, :default => 1
11 end
12 end
13 end
@@ -0,0 +1,134
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module Acts
20 module Positioned
21 def self.included(base)
22 base.extend ClassMethods
23 end
24
25 # This extension provides the capabilities for reordering objects in a list.
26 # The class needs to have a +position+ column defined as an integer on the
27 # mapped database table.
28 module ClassMethods
29 # Configuration options are:
30 #
31 # * +scope+ - restricts what is to be considered a list. Must be a symbol
32 # or an array of symbols
33 def acts_as_positioned(options = {})
34 class_attribute :positioned_options
35 self.positioned_options = {:scope => Array(options[:scope])}
36
37 send :include, Redmine::Acts::Positioned::InstanceMethods
38
39 before_save :set_default_position
40 after_save :update_position
41 after_destroy :remove_position
42 end
43 end
44
45 module InstanceMethods
46 def self.included(base)
47 base.extend ClassMethods
48 end
49
50 # Move to the given position
51 # For compatibility with the previous way of sorting items
52 def move_to=(pos)
53 case pos.to_s
54 when 'highest'
55 self.position = 1
56 when 'higher'
57 self.position -= 1 if position > 1
58 when 'lower'
59 self.position += 1
60 when 'lowest'
61 self.position = nil
62 set_default_position
63 end
64 end
65
66 private
67
68 def position_scope
69 build_position_scope {|c| send(c)}
70 end
71
72 def position_scope_was
73 build_position_scope {|c| send("#{c}_was")}
74 end
75
76 def build_position_scope
77 condition_hash = self.class.positioned_options[:scope].inject({}) do |h, column|
78 h[column] = yield(column)
79 h
80 end
81 self.class.where(condition_hash)
82 end
83
84 def set_default_position
85 if position.nil?
86 self.position = position_scope.maximum(:position).to_i + (new_record? ? 1 : 0)
87 end
88 end
89
90 def update_position
91 if !new_record? && position_scope_changed?
92 remove_position
93 insert_position
94 elsif position_changed?
95 if position_was.nil?
96 insert_position
97 else
98 shift_positions
99 end
100 end
101 end
102
103 def insert_position
104 position_scope.where("position >= ? AND id <> ?", position, id).update_all("position = position + 1")
105 end
106
107 def remove_position
108 position_scope_was.where("position >= ? AND id <> ?", position_was, id).update_all("position = position - 1")
109 end
110
111 def position_scope_changed?
112 (changed & self.class.positioned_options[:scope].map(&:to_s)).any?
113 end
114
115 def shift_positions
116 offset = position_was <=> position
117 min, max = [position, position_was].sort
118 r = position_scope.where("id <> ? AND position BETWEEN ? AND ?", id, min, max).update_all("position = position + #{offset}")
119 if r != max - min
120 reset_positions_in_list
121 end
122 end
123
124 def reset_positions_in_list
125 position_scope.reorder(:position, :id).pluck(:id).each_with_index do |record_id, p|
126 self.class.where(:id => record_id).update_all(:position => p+1)
127 end
128 end
129 end
130 end
131 end
132 end
133
134 ActiveRecord::Base.send :include, Redmine::Acts::Positioned
@@ -0,0 +1,53
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../../../../test_helper', __FILE__)
19
20 class Redmine::Acts::PositionedWithScopeTest < ActiveSupport::TestCase
21 fixtures :projects, :boards
22
23 def test_create_should_default_to_last_position
24 b = Board.generate!(:project_id => 1)
25 assert_equal 3, b.reload.position
26
27 b = Board.generate!(:project_id => 3)
28 assert_equal 1, b.reload.position
29 end
30
31 def test_create_should_insert_at_given_position
32 b = Board.generate!(:project_id => 1, :position => 2)
33
34 assert_equal 2, b.reload.position
35 assert_equal [1, 3, 1, 2], Board.order(:id).pluck(:position)
36 end
37
38 def test_destroy_should_remove_position
39 b = Board.generate!(:project_id => 1, :position => 2)
40 b.destroy
41
42 assert_equal [1, 2, 1], Board.order(:id).pluck(:position)
43 end
44
45 def test_update_should_update_positions
46 b = Board.generate!(:project_id => 1)
47 assert_equal 3, b.position
48
49 b.position = 2
50 b.save!
51 assert_equal [1, 3, 1, 2], Board.order(:id).pluck(:position)
52 end
53 end
@@ -0,0 +1,55
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../../../../test_helper', __FILE__)
19
20 class Redmine::Acts::PositionedWithoutScopeTest < ActiveSupport::TestCase
21 fixtures :trackers, :issue_statuses
22
23 def test_create_should_default_to_last_position
24 t = Tracker.generate
25 t.save!
26
27 assert_equal 4, t.reload.position
28 end
29
30 def test_create_should_insert_at_given_position
31 t = Tracker.generate
32 t.position = 2
33 t.save!
34
35 assert_equal 2, t.reload.position
36 assert_equal [1, 3, 4, 2], Tracker.order(:id).pluck(:position)
37 end
38
39 def test_destroy_should_remove_position
40 t = Tracker.generate!
41 Tracker.generate!
42 t.destroy
43
44 assert_equal [1, 2, 3, 4], Tracker.order(:id).pluck(:position)
45 end
46
47 def test_update_should_update_positions
48 t = Tracker.generate!
49 assert_equal 4, t.position
50
51 t.position = 2
52 t.save!
53 assert_equal [1, 3, 4, 2], Tracker.order(:id).pluck(:position)
54 end
55 end
@@ -21,7 +21,7 class Board < ActiveRecord::Base
21 has_many :messages, lambda {order("#{Message.table_name}.created_on DESC")}, :dependent => :destroy
21 has_many :messages, lambda {order("#{Message.table_name}.created_on DESC")}, :dependent => :destroy
22 belongs_to :last_message, :class_name => 'Message'
22 belongs_to :last_message, :class_name => 'Message'
23 acts_as_tree :dependent => :nullify
23 acts_as_tree :dependent => :nullify
24 acts_as_list :scope => '(project_id = #{project_id} AND parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"})'
24 acts_as_positioned :scope => [:project_id, :parent_id]
25 acts_as_watchable
25 acts_as_watchable
26
26
27 validates_presence_of :name, :description
27 validates_presence_of :name, :description
@@ -24,7 +24,7 class CustomField < ActiveRecord::Base
24 :dependent => :delete_all
24 :dependent => :delete_all
25 has_many :custom_values, :dependent => :delete_all
25 has_many :custom_values, :dependent => :delete_all
26 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
26 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
27 acts_as_list :scope => 'type = \'#{self.class}\''
27 acts_as_positioned
28 serialize :possible_values
28 serialize :possible_values
29 store :format_store
29 store :format_store
30
30
@@ -22,7 +22,7 class Enumeration < ActiveRecord::Base
22
22
23 belongs_to :project
23 belongs_to :project
24
24
25 acts_as_list :scope => 'type = \'#{type}\' AND #{parent_id ? "parent_id = #{parent_id}" : "parent_id IS NULL"}'
25 acts_as_positioned :scope => :parent_id
26 acts_as_customizable
26 acts_as_customizable
27 acts_as_tree
27 acts_as_tree
28
28
@@ -129,33 +129,38 class Enumeration < ActiveRecord::Base
129 return new == previous
129 return new == previous
130 end
130 end
131
131
132 # Overrides acts_as_list reset_positions_in_list so that enumeration overrides
132 private
133 # get the same position as the overriden enumeration
134 def reset_positions_in_list
135 acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i|
136 acts_as_list_class.where("id = :id OR parent_id = :id", :id => item.id).
137 update_all({position_column => (i + 1)})
138 end
139 end
140
133
141 private
142 def check_integrity
134 def check_integrity
143 raise "Cannot delete enumeration" if self.in_use?
135 raise "Cannot delete enumeration" if self.in_use?
144 end
136 end
145
137
146 # Overrides acts_as_list add_to_list_bottom so that enumeration overrides
138 # Overrides Redmine::Acts::Positioned#set_default_position so that enumeration overrides
147 # get the same position as the overriden enumeration
139 # get the same position as the overriden enumeration
148 def add_to_list_bottom
140 def set_default_position
149 if parent
141 if position.nil? && parent
150 self[position_column] = parent.position
142 self.position = parent.position
151 else
143 end
152 super
144 super
145 end
146
147 # Overrides Redmine::Acts::Positioned#update_position so that overrides get the same
148 # position as the overriden enumeration
149 def update_position
150 super
151 if position_changed?
152 self.class.where.not(:parent_id => nil).update_all(
153 "position = coalesce((
154 select position
155 from (select id, position from enumerations) as parent
156 where parent_id = parent.id), 1)"
157 )
153 end
158 end
154 end
159 end
155
160
156 # Overrides acts_as_list remove_from_list so that enumeration overrides
161 # Overrides Redmine::Acts::Positioned#remove_position so that enumeration overrides
157 # get the same position as the overriden enumeration
162 # get the same position as the overriden enumeration
158 def remove_from_list
163 def remove_position
159 if parent_id.blank?
164 if parent_id.blank?
160 super
165 super
161 end
166 end
@@ -19,7 +19,7 class IssueStatus < ActiveRecord::Base
19 before_destroy :check_integrity
19 before_destroy :check_integrity
20 has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id"
20 has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id"
21 has_many :workflow_transitions_as_new_status, :class_name => 'WorkflowTransition', :foreign_key => "new_status_id"
21 has_many :workflow_transitions_as_new_status, :class_name => 'WorkflowTransition', :foreign_key => "new_status_id"
22 acts_as_list
22 acts_as_positioned
23
23
24 after_update :handle_is_closed_change
24 after_update :handle_is_closed_change
25 before_destroy :delete_workflow_rules
25 before_destroy :delete_workflow_rules
@@ -70,7 +70,7 class Role < ActiveRecord::Base
70
70
71 has_many :member_roles, :dependent => :destroy
71 has_many :member_roles, :dependent => :destroy
72 has_many :members, :through => :member_roles
72 has_many :members, :through => :member_roles
73 acts_as_list
73 acts_as_positioned :scope => :builtin
74
74
75 serialize :permissions, ::Role::PermissionsAttributeCoder
75 serialize :permissions, ::Role::PermissionsAttributeCoder
76 attr_protected :builtin
76 attr_protected :builtin
@@ -223,10 +223,10 private
223 def self.find_or_create_system_role(builtin, name)
223 def self.find_or_create_system_role(builtin, name)
224 role = where(:builtin => builtin).first
224 role = where(:builtin => builtin).first
225 if role.nil?
225 if role.nil?
226 role = create(:name => name, :position => 0) do |r|
226 role = create(:name => name) do |r|
227 r.builtin = builtin
227 r.builtin = builtin
228 end
228 end
229 raise "Unable to create the #{name} role." if role.new_record?
229 raise "Unable to create the #{name} role (#{role.errors.full_messages.join(',')})." if role.new_record?
230 end
230 end
231 role
231 role
232 end
232 end
@@ -34,7 +34,7 class Tracker < ActiveRecord::Base
34
34
35 has_and_belongs_to_many :projects
35 has_and_belongs_to_many :projects
36 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
36 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
37 acts_as_list
37 acts_as_positioned
38
38
39 attr_protected :fields_bits
39 attr_protected :fields_bits
40
40
@@ -28,6 +28,8 rescue LoadError
28 # Redcarpet is not available
28 # Redcarpet is not available
29 end
29 end
30
30
31 require 'redmine/acts/positioned'
32
31 require 'redmine/scm/base'
33 require 'redmine/scm/base'
32 require 'redmine/access_control'
34 require 'redmine/access_control'
33 require 'redmine/access_keys'
35 require 'redmine/access_keys'
@@ -145,3 +145,4 custom_fields_011:
145 SGXDqWzDp2prc2Tigqw2NTTDuQ==
145 SGXDqWzDp2prc2Tigqw2NTTDuQ==
146 - Other value
146 - Other value
147 field_format: list
147 field_format: list
148 position: 1
@@ -78,11 +78,13 enumerations_012:
78 type: Enumeration
78 type: Enumeration
79 is_default: true
79 is_default: true
80 active: true
80 active: true
81 position: 1
81 enumerations_013:
82 enumerations_013:
82 name: Another Enumeration
83 name: Another Enumeration
83 id: 13
84 id: 13
84 type: Enumeration
85 type: Enumeration
85 active: true
86 active: true
87 position: 2
86 enumerations_014:
88 enumerations_014:
87 name: Inactive Activity
89 name: Inactive Activity
88 id: 14
90 id: 14
@@ -182,7 +182,7 roles_004:
182 - :browse_repository
182 - :browse_repository
183 - :view_changesets
183 - :view_changesets
184
184
185 position: 4
185 position: 1
186 roles_005:
186 roles_005:
187 name: Anonymous
187 name: Anonymous
188 id: 5
188 id: 5
@@ -203,5 +203,5 roles_005:
203 - :browse_repository
203 - :browse_repository
204 - :view_changesets
204 - :view_changesets
205
205
206 position: 5
206 position: 1
207
207
@@ -203,6 +203,6 class RolesControllerTest < ActionController::TestCase
203 def test_move_lowest
203 def test_move_lowest
204 put :update, :id => 2, :role => {:move_to => 'lowest'}
204 put :update, :id => 2, :role => {:move_to => 'lowest'}
205 assert_redirected_to '/roles'
205 assert_redirected_to '/roles'
206 assert_equal Role.count, Role.find(2).position
206 assert_equal Role.givable.count, Role.find(2).position
207 end
207 end
208 end
208 end
@@ -60,13 +60,18 module ObjectHelpers
60 status
60 status
61 end
61 end
62
62
63 def Tracker.generate!(attributes={})
63 def Tracker.generate(attributes={})
64 @generated_tracker_name ||= 'Tracker 0'
64 @generated_tracker_name ||= 'Tracker 0'
65 @generated_tracker_name.succ!
65 @generated_tracker_name.succ!
66 tracker = Tracker.new(attributes)
66 tracker = Tracker.new(attributes)
67 tracker.name = @generated_tracker_name.dup if tracker.name.blank?
67 tracker.name = @generated_tracker_name.dup if tracker.name.blank?
68 tracker.default_status ||= IssueStatus.order('position').first || IssueStatus.generate!
68 tracker.default_status ||= IssueStatus.order('position').first || IssueStatus.generate!
69 yield tracker if block_given?
69 yield tracker if block_given?
70 tracker
71 end
72
73 def Tracker.generate!(attributes={}, &block)
74 tracker = Tracker.generate(attributes, &block)
70 tracker.save!
75 tracker.save!
71 tracker
76 tracker
72 end
77 end
@@ -155,6 +155,7 class EnumerationTest < ActiveSupport::TestCase
155 b = IssuePriority.create!(:name => 'B')
155 b = IssuePriority.create!(:name => 'B')
156 override = IssuePriority.create!(:name => 'BB', :parent_id => b.id)
156 override = IssuePriority.create!(:name => 'BB', :parent_id => b.id)
157 b.move_to = 'higher'
157 b.move_to = 'higher'
158 b.save!
158
159
159 assert_equal [2, 1, 1], [a, b, override].map(&:reload).map(&:position)
160 assert_equal [2, 1, 1], [a, b, override].map(&:reload).map(&:position)
160 end
161 end
@@ -55,25 +55,6 class IssuePriorityTest < ActiveSupport::TestCase
55 assert_equal [1, 2, 3], priorities.map(&:position)
55 assert_equal [1, 2, 3], priorities.map(&:position)
56 end
56 end
57
57
58 def test_reset_positions_in_list_should_set_sequential_positions
59 IssuePriority.delete_all
60
61 priorities = [1, 2, 3].map {|i| IssuePriority.create!(:name => "P#{i}")}
62 priorities[0].update_attribute :position, 4
63 priorities[1].update_attribute :position, 2
64 priorities[2].update_attribute :position, 7
65 assert_equal [4, 2, 7], priorities.map(&:reload).map(&:position)
66
67 priorities[0].reset_positions_in_list
68 assert_equal [2, 1, 3], priorities.map(&:reload).map(&:position)
69 end
70
71 def test_moving_in_list_should_reset_positions
72 priority = IssuePriority.first
73 priority.expects(:reset_positions_in_list).once
74 priority.move_to = 'higher'
75 end
76
77 def test_clear_position_names_should_set_position_names_to_nil
58 def test_clear_position_names_should_set_position_names_to_nil
78 IssuePriority.clear_position_names
59 IssuePriority.clear_position_names
79 assert IssuePriority.all.all? {|priority| priority.position_name.nil?}
60 assert IssuePriority.all.all? {|priority| priority.position_name.nil?}
@@ -102,6 +83,7 class IssuePriorityTest < ActiveSupport::TestCase
102 def test_moving_a_priority_should_update_position_names
83 def test_moving_a_priority_should_update_position_names
103 prio = IssuePriority.first
84 prio = IssuePriority.first
104 prio.move_to = 'lowest'
85 prio.move_to = 'lowest'
86 prio.save!
105 prio.reload
87 prio.reload
106 assert_equal 'highest', prio.position_name
88 assert_equal 'highest', prio.position_name
107 end
89 end
General Comments 0
You need to be logged in to leave comments. Login now