summaryrefslogtreecommitdiffstats
path: root/lib/redmine/nested_set/project_nested_set.rb
blob: e6c8c61b30c36f395a4c3a4ebeab7c61e465f702 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# frozen_string_literal: true

# Redmine - project management software
# Copyright (C) 2006-2022  Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

module Redmine
  module NestedSet
    module ProjectNestedSet
      def self.included(base)
        base.class_eval do
          belongs_to :parent, :class_name => self.name

          before_create :add_to_nested_set
          before_update(
            :move_in_nested_set,
            :if =>
              lambda {|project| project.parent_id_changed? || project.name_changed?}
          )
          before_destroy :destroy_children
        end
        base.extend ClassMethods
        base.send :include, Redmine::NestedSet::Traversing
      end

      private

      def target_lft
        siblings_rgt =
          self.class.where(:parent_id => parent_id).where("name < ?", name).maximum(:rgt)
        if siblings_rgt
          siblings_rgt + 1
        elsif parent_id
          parent_lft = self.class.where(:id => parent_id).pick(:lft)
          unless parent_lft
            raise "Project id=#{id} with parent_id=#{parent_id}: parent missing or without 'lft' value"
          end

          parent_lft + 1
        else
          1
        end
      end

      def add_to_nested_set(lock=true)
        lock_nested_set if lock
        self.lft = target_lft
        self.rgt = lft + 1
        self.class.where("lft >= ? OR rgt >= ?", lft, lft).update_all(
          [
            "lft = CASE WHEN lft >= :lft THEN lft + 2 ELSE lft END, " \
              "rgt = CASE WHEN rgt >= :lft THEN rgt + 2 ELSE rgt END",
            {:lft => lft}
          ]
        )
      end

      def move_in_nested_set
        lock_nested_set
        reload_nested_set_values
        a = lft
        b = rgt
        c = target_lft
        unless c == a
          if c > a
            # Moving to the right
            d = c - (b - a + 1)
            scope =
              self.class.where(
                ["lft BETWEEN :a AND :c - 1 OR rgt BETWEEN :a AND :c - 1",
                 {:a => a, :c => c}]
              )
            scope.update_all(
              [
                "lft = CASE WHEN lft BETWEEN :a AND :b THEN lft + (:d - :a) " \
                  "WHEN lft BETWEEN :b + 1 AND :c - 1 THEN lft - (:b - :a + 1) ELSE lft END, " \
                  "rgt = CASE WHEN rgt BETWEEN :a AND :b THEN rgt + (:d - :a) " \
                  "WHEN rgt BETWEEN :b + 1 AND :c - 1 THEN rgt - (:b - :a + 1) ELSE rgt END",
                {:a => a, :b => b, :c => c, :d => d}
              ]
            )
          elsif c < a
            # Moving to the left
            scope =
              self.class.where(
                "lft BETWEEN :c AND :b OR rgt BETWEEN :c AND :b",
                {:a => a, :b => b, :c => c}
              )
            scope.update_all(
              [
                "lft = CASE WHEN lft BETWEEN :a AND :b THEN lft - (:a - :c) " \
                  "WHEN lft BETWEEN :c AND :a - 1 THEN lft + (:b - :a + 1) ELSE lft END, " \
                  "rgt = CASE WHEN rgt BETWEEN :a AND :b THEN rgt - (:a - :c) " \
                  "WHEN rgt BETWEEN :c AND :a - 1 THEN rgt + (:b - :a + 1) ELSE rgt END",
                {:a => a, :b => b, :c => c, :d => d}
              ]
            )
          end
          reload_nested_set_values
        end
      end

      def destroy_children
        unless @without_nested_set_update
          lock_nested_set
          reload_nested_set_values
        end
        children.each {|c| c.send :destroy_without_nested_set_update}
        unless @without_nested_set_update
          self.class.where("lft > ? OR rgt > ?", lft, lft).update_all(
            [
              "lft = CASE WHEN lft > :lft THEN lft - :shift ELSE lft END, " \
                "rgt = CASE WHEN rgt > :lft THEN rgt - :shift ELSE rgt END",
              {:lft => lft, :shift => rgt - lft + 1}
            ]
          )
        end
      end

      def destroy_without_nested_set_update
        @without_nested_set_update = true
        destroy
      end

      def reload_nested_set_values
        self.lft, self.rgt = Project.where(:id => id).pick(:lft, :rgt)
      end

      def save_nested_set_values
        self.class.where(:id => id).update_all(:lft => lft, :rgt => rgt)
      end

      def move_possible?(project)
        new_record? || !is_or_is_ancestor_of?(project)
      end

      def lock_nested_set
        lock = true
        if /sqlserver/i.match?(self.class.connection.adapter_name)
          lock = "WITH (ROWLOCK HOLDLOCK UPDLOCK)"
        end
        self.class.order(:id).lock(lock).ids
      end

      def nested_set_scope
        self.class.order(:lft)
      end

      def same_nested_set_scope?(project)
        true
      end

      module ClassMethods
        def rebuild_tree!
          transaction do
            reorder(:id).lock.ids
            update_all(:lft => nil, :rgt => nil)
            rebuild_nodes
          end
        end

        private

        def rebuild_nodes(parent_id = nil)
          nodes = Project.where(:parent_id => parent_id).where(:rgt => nil, :lft => nil).reorder(:name)

          nodes.each do |node|
            node.send :add_to_nested_set, false
            node.send :save_nested_set_values
            rebuild_nodes node.id
          end
        end
      end
    end
  end
end