@@ -0,0 +1,195 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2014 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 NestedSet | |||
|
20 | module IssueNestedSet | |||
|
21 | def self.included(base) | |||
|
22 | base.class_eval do | |||
|
23 | belongs_to :parent, :class_name => self.name | |||
|
24 | ||||
|
25 | before_create :add_to_nested_set, :if => lambda {|issue| issue.parent.present?} | |||
|
26 | after_create :add_as_root, :if => lambda {|issue| issue.parent.blank?} | |||
|
27 | before_update :handle_parent_change, :if => lambda {|issue| issue.parent_id_changed?} | |||
|
28 | before_destroy :destroy_children | |||
|
29 | end | |||
|
30 | base.extend ClassMethods | |||
|
31 | base.send :include, Redmine::NestedSet::Traversing | |||
|
32 | end | |||
|
33 | ||||
|
34 | private | |||
|
35 | ||||
|
36 | def target_lft | |||
|
37 | scope_for_max_rgt = self.class.where(:root_id => root_id).where(:parent_id => parent_id) | |||
|
38 | if id | |||
|
39 | #scope_for_max_rgt = scope_for_max_rgt.where("id < ?", id) | |||
|
40 | end | |||
|
41 | max_rgt = scope_for_max_rgt.maximum(:rgt) | |||
|
42 | if max_rgt | |||
|
43 | max_rgt + 1 | |||
|
44 | elsif parent | |||
|
45 | parent.lft + 1 | |||
|
46 | else | |||
|
47 | 1 | |||
|
48 | end | |||
|
49 | end | |||
|
50 | ||||
|
51 | def add_to_nested_set(lock=true) | |||
|
52 | lock_nested_set if lock | |||
|
53 | parent.send :reload_nested_set_values | |||
|
54 | self.root_id = parent.root_id | |||
|
55 | self.lft = target_lft | |||
|
56 | self.rgt = lft + 1 | |||
|
57 | self.class.where(:root_id => root_id).where("lft >= ? OR rgt >= ?", lft, lft).update_all([ | |||
|
58 | "lft = CASE WHEN lft >= :lft THEN lft + 2 ELSE lft END, " + | |||
|
59 | "rgt = CASE WHEN rgt >= :lft THEN rgt + 2 ELSE rgt END", | |||
|
60 | {:lft => lft} | |||
|
61 | ]) | |||
|
62 | end | |||
|
63 | ||||
|
64 | def add_as_root | |||
|
65 | self.root_id = id | |||
|
66 | self.lft = 1 | |||
|
67 | self.rgt = 2 | |||
|
68 | self.class.where(:id => id).update_all(:root_id => root_id, :lft => lft, :rgt => rgt) | |||
|
69 | end | |||
|
70 | ||||
|
71 | def handle_parent_change | |||
|
72 | lock_nested_set | |||
|
73 | reload_nested_set_values | |||
|
74 | if parent_id_was | |||
|
75 | remove_from_nested_set | |||
|
76 | end | |||
|
77 | if parent | |||
|
78 | move_to_nested_set | |||
|
79 | end | |||
|
80 | reload_nested_set_values | |||
|
81 | end | |||
|
82 | ||||
|
83 | def move_to_nested_set | |||
|
84 | if parent | |||
|
85 | previous_root_id = root_id | |||
|
86 | self.root_id = parent.root_id | |||
|
87 | ||||
|
88 | lft_after_move = target_lft | |||
|
89 | self.class.where(:root_id => parent.root_id).update_all([ | |||
|
90 | "lft = CASE WHEN lft >= :lft THEN lft + :shift ELSE lft END, " + | |||
|
91 | "rgt = CASE WHEN rgt >= :lft THEN rgt + :shift ELSE rgt END", | |||
|
92 | {:lft => lft_after_move, :shift => (rgt - lft + 1)} | |||
|
93 | ]) | |||
|
94 | ||||
|
95 | self.class.where(:root_id => previous_root_id).update_all([ | |||
|
96 | "root_id = :root_id, lft = lft + :shift, rgt = rgt + :shift", | |||
|
97 | {:root_id => parent.root_id, :shift => lft_after_move - lft} | |||
|
98 | ]) | |||
|
99 | ||||
|
100 | self.lft, self.rgt = lft_after_move, (rgt - lft + lft_after_move) | |||
|
101 | parent.send :reload_nested_set_values | |||
|
102 | end | |||
|
103 | end | |||
|
104 | ||||
|
105 | def remove_from_nested_set | |||
|
106 | self.class.where(:root_id => root_id).where("lft >= ? AND rgt <= ?", lft, rgt). | |||
|
107 | update_all(["root_id = :id, lft = lft - :shift, rgt = rgt - :shift", {:id => id, :shift => lft - 1}]) | |||
|
108 | ||||
|
109 | self.class.where(:root_id => root_id).update_all([ | |||
|
110 | "lft = CASE WHEN lft >= :lft THEN lft - :shift ELSE lft END, " + | |||
|
111 | "rgt = CASE WHEN rgt >= :lft THEN rgt - :shift ELSE rgt END", | |||
|
112 | {:lft => lft, :shift => rgt - lft + 1} | |||
|
113 | ]) | |||
|
114 | self.root_id = id | |||
|
115 | self.lft, self.rgt = 1, (rgt - lft + 1) | |||
|
116 | end | |||
|
117 | ||||
|
118 | def destroy_children | |||
|
119 | unless @without_nested_set_update | |||
|
120 | lock_nested_set | |||
|
121 | reload_nested_set_values | |||
|
122 | end | |||
|
123 | children.each {|c| c.send :destroy_without_nested_set_update} | |||
|
124 | reload | |||
|
125 | unless @without_nested_set_update | |||
|
126 | self.class.where(:root_id => root_id).where("lft > ? OR rgt > ?", lft, lft).update_all([ | |||
|
127 | "lft = CASE WHEN lft > :lft THEN lft - :shift ELSE lft END, " + | |||
|
128 | "rgt = CASE WHEN rgt > :lft THEN rgt - :shift ELSE rgt END", | |||
|
129 | {:lft => lft, :shift => rgt - lft + 1} | |||
|
130 | ]) | |||
|
131 | end | |||
|
132 | end | |||
|
133 | ||||
|
134 | def destroy_without_nested_set_update | |||
|
135 | @without_nested_set_update = true | |||
|
136 | destroy | |||
|
137 | end | |||
|
138 | ||||
|
139 | def reload_nested_set_values | |||
|
140 | self.root_id, self.lft, self.rgt = self.class.where(:id => id).pluck(:root_id, :lft, :rgt).first | |||
|
141 | end | |||
|
142 | ||||
|
143 | def save_nested_set_values | |||
|
144 | self.class.where(:id => id).update_all(:root_id => root_id, :lft => lft, :rgt => rgt) | |||
|
145 | end | |||
|
146 | ||||
|
147 | def move_possible?(issue) | |||
|
148 | !is_or_is_ancestor_of?(issue) | |||
|
149 | end | |||
|
150 | ||||
|
151 | def lock_nested_set | |||
|
152 | lock = true | |||
|
153 | if self.class.connection.adapter_name =~ /sqlserver/i | |||
|
154 | lock = "WITH (ROWLOCK HOLDLOCK UPDLOCK)" | |||
|
155 | end | |||
|
156 | sets_to_lock = [id, parent_id].compact | |||
|
157 | self.class.reorder(:id).where("root_id IN (SELECT root_id FROM #{self.class.table_name} WHERE id IN (?))", sets_to_lock).lock(lock).ids | |||
|
158 | end | |||
|
159 | ||||
|
160 | def nested_set_scope | |||
|
161 | self.class.order(:lft).where(:root_id => root_id) | |||
|
162 | end | |||
|
163 | ||||
|
164 | def same_nested_set_scope?(issue) | |||
|
165 | root_id == issue.root_id | |||
|
166 | end | |||
|
167 | ||||
|
168 | module ClassMethods | |||
|
169 | def rebuild_tree! | |||
|
170 | transaction do | |||
|
171 | reorder(:id).lock.ids | |||
|
172 | update_all(:root_id => nil, :lft => nil, :rgt => nil) | |||
|
173 | where(:parent_id => nil).update_all(["root_id = id, lft = ?, rgt = ?", 1, 2]) | |||
|
174 | roots_with_children = joins("JOIN #{table_name} parent ON parent.id = #{table_name}.parent_id AND parent.id = parent.root_id").uniq.pluck("parent.id") | |||
|
175 | roots_with_children.each do |root_id| | |||
|
176 | rebuild_nodes(root_id) | |||
|
177 | end | |||
|
178 | end | |||
|
179 | end | |||
|
180 | ||||
|
181 | private | |||
|
182 | ||||
|
183 | def rebuild_nodes(parent_id = nil) | |||
|
184 | nodes = where(:parent_id => parent_id, :rgt => nil, :lft => nil).order(:id).to_a | |||
|
185 | ||||
|
186 | nodes.each do |node| | |||
|
187 | node.send :add_to_nested_set, false | |||
|
188 | node.send :save_nested_set_values | |||
|
189 | rebuild_nodes node.id | |||
|
190 | end | |||
|
191 | end | |||
|
192 | end | |||
|
193 | end | |||
|
194 | end | |||
|
195 | end |
@@ -0,0 +1,159 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2014 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 NestedSet | |||
|
20 | module ProjectNestedSet | |||
|
21 | def self.included(base) | |||
|
22 | base.class_eval do | |||
|
23 | belongs_to :parent, :class_name => self.name | |||
|
24 | ||||
|
25 | before_create :add_to_nested_set | |||
|
26 | before_update :move_in_nested_set, :if => lambda {|project| project.parent_id_changed? || project.name_changed?} | |||
|
27 | before_destroy :destroy_children | |||
|
28 | end | |||
|
29 | base.extend ClassMethods | |||
|
30 | base.send :include, Redmine::NestedSet::Traversing | |||
|
31 | end | |||
|
32 | ||||
|
33 | private | |||
|
34 | ||||
|
35 | def target_lft | |||
|
36 | siblings_rgt = self.class.where(:parent_id => parent_id).where("name < ?", name).maximum(:rgt) | |||
|
37 | if siblings_rgt | |||
|
38 | siblings_rgt + 1 | |||
|
39 | elsif parent_id | |||
|
40 | parent_lft = self.class.where(:id => parent_id).pluck(:lft).first | |||
|
41 | raise "Project id=#{id} with parent_id=#{parent_id}: parent missing or without 'lft' value" unless parent_lft | |||
|
42 | parent_lft + 1 | |||
|
43 | else | |||
|
44 | 1 | |||
|
45 | end | |||
|
46 | end | |||
|
47 | ||||
|
48 | def add_to_nested_set(lock=true) | |||
|
49 | lock_nested_set if lock | |||
|
50 | self.lft = target_lft | |||
|
51 | self.rgt = lft + 1 | |||
|
52 | self.class.where("lft >= ? OR rgt >= ?", lft, lft).update_all([ | |||
|
53 | "lft = CASE WHEN lft >= :lft THEN lft + 2 ELSE lft END, " + | |||
|
54 | "rgt = CASE WHEN rgt >= :lft THEN rgt + 2 ELSE rgt END", | |||
|
55 | {:lft => lft} | |||
|
56 | ]) | |||
|
57 | end | |||
|
58 | ||||
|
59 | def move_in_nested_set | |||
|
60 | lock_nested_set | |||
|
61 | reload_nested_set_values | |||
|
62 | a = lft | |||
|
63 | b = rgt | |||
|
64 | c = target_lft | |||
|
65 | unless c == a | |||
|
66 | if c > a | |||
|
67 | # Moving to the right | |||
|
68 | d = c - (b - a + 1) | |||
|
69 | scope = self.class.where(["lft BETWEEN :a AND :c - 1 OR rgt BETWEEN :a AND :c - 1", {:a => a, :c => c}]) | |||
|
70 | scope.update_all([ | |||
|
71 | "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, " + | |||
|
72 | "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", | |||
|
73 | {:a => a, :b => b, :c => c, :d => d} | |||
|
74 | ]) | |||
|
75 | elsif c < a | |||
|
76 | # Moving to the left | |||
|
77 | scope = self.class.where("lft BETWEEN :c AND :b OR rgt BETWEEN :c AND :b", {:a => a, :b => b, :c => c}) | |||
|
78 | scope.update_all([ | |||
|
79 | "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, " + | |||
|
80 | "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", | |||
|
81 | {:a => a, :b => b, :c => c, :d => d} | |||
|
82 | ]) | |||
|
83 | end | |||
|
84 | reload_nested_set_values | |||
|
85 | end | |||
|
86 | end | |||
|
87 | ||||
|
88 | def destroy_children | |||
|
89 | unless @without_nested_set_update | |||
|
90 | lock_nested_set | |||
|
91 | reload_nested_set_values | |||
|
92 | end | |||
|
93 | children.each {|c| c.send :destroy_without_nested_set_update} | |||
|
94 | unless @without_nested_set_update | |||
|
95 | self.class.where("lft > ? OR rgt > ?", lft, lft).update_all([ | |||
|
96 | "lft = CASE WHEN lft > :lft THEN lft - :shift ELSE lft END, " + | |||
|
97 | "rgt = CASE WHEN rgt > :lft THEN rgt - :shift ELSE rgt END", | |||
|
98 | {:lft => lft, :shift => rgt - lft + 1} | |||
|
99 | ]) | |||
|
100 | end | |||
|
101 | end | |||
|
102 | ||||
|
103 | def destroy_without_nested_set_update | |||
|
104 | @without_nested_set_update = true | |||
|
105 | destroy | |||
|
106 | end | |||
|
107 | ||||
|
108 | def reload_nested_set_values | |||
|
109 | self.lft, self.rgt = Project.where(:id => id).pluck(:lft, :rgt).first | |||
|
110 | end | |||
|
111 | ||||
|
112 | def save_nested_set_values | |||
|
113 | self.class.where(:id => id).update_all(:lft => lft, :rgt => rgt) | |||
|
114 | end | |||
|
115 | ||||
|
116 | def move_possible?(project) | |||
|
117 | !is_or_is_ancestor_of?(project) | |||
|
118 | end | |||
|
119 | ||||
|
120 | def lock_nested_set | |||
|
121 | lock = true | |||
|
122 | if self.class.connection.adapter_name =~ /sqlserver/i | |||
|
123 | lock = "WITH (ROWLOCK HOLDLOCK UPDLOCK)" | |||
|
124 | end | |||
|
125 | self.class.order(:id).lock(lock).ids | |||
|
126 | end | |||
|
127 | ||||
|
128 | def nested_set_scope | |||
|
129 | self.class.order(:lft) | |||
|
130 | end | |||
|
131 | ||||
|
132 | def same_nested_set_scope?(project) | |||
|
133 | true | |||
|
134 | end | |||
|
135 | ||||
|
136 | module ClassMethods | |||
|
137 | def rebuild_tree! | |||
|
138 | transaction do | |||
|
139 | reorder(:id).lock.ids | |||
|
140 | update_all(:lft => nil, :rgt => nil) | |||
|
141 | rebuild_nodes | |||
|
142 | end | |||
|
143 | end | |||
|
144 | ||||
|
145 | private | |||
|
146 | ||||
|
147 | def rebuild_nodes(parent_id = nil) | |||
|
148 | nodes = Project.where(:parent_id => parent_id).where(:rgt => nil, :lft => nil).reorder(:name) | |||
|
149 | ||||
|
150 | nodes.each do |node| | |||
|
151 | node.send :add_to_nested_set, false | |||
|
152 | node.send :save_nested_set_values | |||
|
153 | rebuild_nodes node.id | |||
|
154 | end | |||
|
155 | end | |||
|
156 | end | |||
|
157 | end | |||
|
158 | end | |||
|
159 | end |
@@ -0,0 +1,116 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2014 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 NestedSet | |||
|
20 | module Traversing | |||
|
21 | def self.included(base) | |||
|
22 | base.class_eval do | |||
|
23 | scope :roots, lambda {where :parent_id => nil} | |||
|
24 | scope :leaves, lambda {where "#{table_name}.rgt - #{table_name}.lft = ?", 1} | |||
|
25 | end | |||
|
26 | end | |||
|
27 | ||||
|
28 | # Returns true if the element has no parent | |||
|
29 | def root? | |||
|
30 | parent_id.nil? | |||
|
31 | end | |||
|
32 | ||||
|
33 | # Returns true if the element has a parent | |||
|
34 | def child? | |||
|
35 | !root? | |||
|
36 | end | |||
|
37 | ||||
|
38 | # Returns true if the element has no children | |||
|
39 | def leaf? | |||
|
40 | new_record? || (rgt - lft == 1) | |||
|
41 | end | |||
|
42 | ||||
|
43 | # Returns the root element (ancestor with no parent) | |||
|
44 | def root | |||
|
45 | self_and_ancestors.first | |||
|
46 | end | |||
|
47 | ||||
|
48 | # Returns the children | |||
|
49 | def children | |||
|
50 | if id.nil? | |||
|
51 | nested_set_scope.none | |||
|
52 | else | |||
|
53 | self.class.order(:lft).where(:parent_id => id) | |||
|
54 | end | |||
|
55 | end | |||
|
56 | ||||
|
57 | # Returns the descendants that have no children | |||
|
58 | def leaves | |||
|
59 | descendants.where("#{self.class.table_name}.rgt - #{self.class.table_name}.lft = ?", 1) | |||
|
60 | end | |||
|
61 | ||||
|
62 | # Returns the siblings | |||
|
63 | def siblings | |||
|
64 | nested_set_scope.where(:parent_id => parent_id).where("id <> ?", id) | |||
|
65 | end | |||
|
66 | ||||
|
67 | # Returns the ancestors | |||
|
68 | def ancestors | |||
|
69 | if root? | |||
|
70 | nested_set_scope.none | |||
|
71 | else | |||
|
72 | nested_set_scope.where("#{self.class.table_name}.lft < ? AND #{self.class.table_name}.rgt > ?", lft, rgt) | |||
|
73 | end | |||
|
74 | end | |||
|
75 | ||||
|
76 | # Returns the element and its ancestors | |||
|
77 | def self_and_ancestors | |||
|
78 | nested_set_scope.where("#{self.class.table_name}.lft <= ? AND #{self.class.table_name}.rgt >= ?", lft, rgt) | |||
|
79 | end | |||
|
80 | ||||
|
81 | # Returns true if the element is an ancestor of other | |||
|
82 | def is_ancestor_of?(other) | |||
|
83 | same_nested_set_scope?(other) && other.lft > lft && other.rgt < rgt | |||
|
84 | end | |||
|
85 | ||||
|
86 | # Returns true if the element equals other or is an ancestor of other | |||
|
87 | def is_or_is_ancestor_of?(other) | |||
|
88 | other == self || is_ancestor_of?(other) | |||
|
89 | end | |||
|
90 | ||||
|
91 | # Returns the descendants | |||
|
92 | def descendants | |||
|
93 | if leaf? | |||
|
94 | nested_set_scope.none | |||
|
95 | else | |||
|
96 | nested_set_scope.where("#{self.class.table_name}.lft > ? AND #{self.class.table_name}.rgt < ?", lft, rgt) | |||
|
97 | end | |||
|
98 | end | |||
|
99 | ||||
|
100 | # Returns the element and its descendants | |||
|
101 | def self_and_descendants | |||
|
102 | nested_set_scope.where("#{self.class.table_name}.lft >= ? AND #{self.class.table_name}.rgt <= ?", lft, rgt) | |||
|
103 | end | |||
|
104 | ||||
|
105 | # Returns true if the element is a descendant of other | |||
|
106 | def is_descendant_of?(other) | |||
|
107 | same_nested_set_scope?(other) && other.lft < lft && other.rgt > rgt | |||
|
108 | end | |||
|
109 | ||||
|
110 | # Returns true if the element equals other or is a descendant of other | |||
|
111 | def is_or_is_descendant_of?(other) | |||
|
112 | other == self || is_descendant_of?(other) | |||
|
113 | end | |||
|
114 | end | |||
|
115 | end | |||
|
116 | end |
@@ -0,0 +1,73 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2014 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 IssueNestedSetConcurrencyTest < ActiveSupport::TestCase | |||
|
21 | fixtures :projects, :users, | |||
|
22 | :trackers, :projects_trackers, | |||
|
23 | :enabled_modules, | |||
|
24 | :issue_statuses, | |||
|
25 | :enumerations | |||
|
26 | ||||
|
27 | self.use_transactional_fixtures = false | |||
|
28 | ||||
|
29 | def setup | |||
|
30 | CustomField.delete_all | |||
|
31 | end | |||
|
32 | ||||
|
33 | def teardown | |||
|
34 | Issue.delete_all | |||
|
35 | end | |||
|
36 | ||||
|
37 | def test_concurrency | |||
|
38 | skip if sqlite? | |||
|
39 | with_settings :notified_events => [] do | |||
|
40 | # Generates an issue and destroys it in order | |||
|
41 | # to load all needed classes before starting threads | |||
|
42 | i = Issue.generate! | |||
|
43 | i.destroy | |||
|
44 | ||||
|
45 | root = Issue.generate! | |||
|
46 | assert_difference 'Issue.count', 60 do | |||
|
47 | threads = [] | |||
|
48 | 3.times do |i| | |||
|
49 | threads << Thread.new(i) do | |||
|
50 | ActiveRecord::Base.connection_pool.with_connection do | |||
|
51 | begin | |||
|
52 | 10.times do | |||
|
53 | i = Issue.generate! :parent_issue_id => root.id | |||
|
54 | c1 = Issue.generate! :parent_issue_id => i.id | |||
|
55 | c2 = Issue.generate! :parent_issue_id => i.id | |||
|
56 | c3 = Issue.generate! :parent_issue_id => i.id | |||
|
57 | c2.reload.destroy | |||
|
58 | c1.reload.destroy | |||
|
59 | end | |||
|
60 | rescue Exception => e | |||
|
61 | Thread.current[:exception] = e.message | |||
|
62 | end | |||
|
63 | end | |||
|
64 | end | |||
|
65 | end | |||
|
66 | threads.each do |thread| | |||
|
67 | thread.join | |||
|
68 | assert_nil thread[:exception] | |||
|
69 | end | |||
|
70 | end | |||
|
71 | end | |||
|
72 | end | |||
|
73 | end |
@@ -0,0 +1,76 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2014 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 ProjectNestedSetConcurrencyTest < ActiveSupport::TestCase | |||
|
21 | self.use_transactional_fixtures = false | |||
|
22 | ||||
|
23 | def setup | |||
|
24 | CustomField.delete_all | |||
|
25 | end | |||
|
26 | ||||
|
27 | def teardown | |||
|
28 | Project.delete_all | |||
|
29 | end | |||
|
30 | ||||
|
31 | def test_concurrency | |||
|
32 | skip if sqlite? | |||
|
33 | # Generates a project and destroys it in order | |||
|
34 | # to load all needed classes before starting threads | |||
|
35 | p = generate_project! | |||
|
36 | p.destroy | |||
|
37 | ||||
|
38 | assert_difference 'Project.count', 60 do | |||
|
39 | threads = [] | |||
|
40 | 3.times do |i| | |||
|
41 | threads << Thread.new(i) do | |||
|
42 | ActiveRecord::Base.connection_pool.with_connection do | |||
|
43 | begin | |||
|
44 | 10.times do | |||
|
45 | p = generate_project! | |||
|
46 | c1 = generate_project! :parent_id => p.id | |||
|
47 | c2 = generate_project! :parent_id => p.id | |||
|
48 | c3 = generate_project! :parent_id => p.id | |||
|
49 | c2.reload.destroy | |||
|
50 | c1.reload.destroy | |||
|
51 | end | |||
|
52 | rescue Exception => e | |||
|
53 | Thread.current[:exception] = e.message | |||
|
54 | end | |||
|
55 | end | |||
|
56 | end | |||
|
57 | end | |||
|
58 | threads.each do |thread| | |||
|
59 | thread.join | |||
|
60 | assert_nil thread[:exception] | |||
|
61 | end | |||
|
62 | end | |||
|
63 | end | |||
|
64 | ||||
|
65 | # Generates a bare project with random name | |||
|
66 | # and identifier | |||
|
67 | def generate_project!(attributes={}) | |||
|
68 | identifier = "a"+Redmine::Utils.random_hex(6) | |||
|
69 | Project.generate!({ | |||
|
70 | :identifier => identifier, | |||
|
71 | :name => identifier, | |||
|
72 | :tracker_ids => [], | |||
|
73 | :enabled_module_names => [] | |||
|
74 | }.merge(attributes)) | |||
|
75 | end | |||
|
76 | end |
@@ -6,7 +6,6 gem "coderay", "~> 1.1.0" | |||||
6 | gem "builder", ">= 3.0.4" |
|
6 | gem "builder", ">= 3.0.4" | |
7 | gem "request_store", "1.0.5" |
|
7 | gem "request_store", "1.0.5" | |
8 | gem "mime-types" |
|
8 | gem "mime-types" | |
9 | gem "awesome_nested_set", "3.0.0" |
|
|||
10 | gem "protected_attributes" |
|
9 | gem "protected_attributes" | |
11 | gem "actionpack-action_caching" |
|
10 | gem "actionpack-action_caching" | |
12 | gem "actionpack-xml_parser" |
|
11 | gem "actionpack-xml_parser" |
@@ -19,6 +19,8 class Issue < ActiveRecord::Base | |||||
19 | include Redmine::SafeAttributes |
|
19 | include Redmine::SafeAttributes | |
20 | include Redmine::Utils::DateCalculation |
|
20 | include Redmine::Utils::DateCalculation | |
21 | include Redmine::I18n |
|
21 | include Redmine::I18n | |
|
22 | before_save :set_parent_id | |||
|
23 | include Redmine::NestedSet::IssueNestedSet | |||
22 |
|
24 | |||
23 | belongs_to :project |
|
25 | belongs_to :project | |
24 | belongs_to :tracker |
|
26 | belongs_to :tracker | |
@@ -41,7 +43,6 class Issue < ActiveRecord::Base | |||||
41 | has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all |
|
43 | has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all | |
42 | has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all |
|
44 | has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all | |
43 |
|
45 | |||
44 | acts_as_nested_set :scope => 'root_id', :dependent => :destroy |
|
|||
45 | acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed |
|
46 | acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed | |
46 | acts_as_customizable |
|
47 | acts_as_customizable | |
47 | acts_as_watchable |
|
48 | acts_as_watchable | |
@@ -185,7 +186,7 class Issue < ActiveRecord::Base | |||||
185 | # the lock_version condition should not be an issue but we handle it. |
|
186 | # the lock_version condition should not be an issue but we handle it. | |
186 | def destroy |
|
187 | def destroy | |
187 | super |
|
188 | super | |
188 | rescue ActiveRecord::RecordNotFound |
|
189 | rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound | |
189 | # Stale or already deleted |
|
190 | # Stale or already deleted | |
190 | begin |
|
191 | begin | |
191 | reload |
|
192 | reload | |
@@ -615,10 +616,8 class Issue < ActiveRecord::Base | |||||
615 | errors.add :parent_issue_id, :invalid |
|
616 | errors.add :parent_issue_id, :invalid | |
616 | elsif !new_record? |
|
617 | elsif !new_record? | |
617 | # moving an existing issue |
|
618 | # moving an existing issue | |
618 | if @parent_issue.root_id != root_id |
|
619 | if move_possible?(@parent_issue) | |
619 | # we can always move to another tree |
|
620 | # move accepted | |
620 | elsif move_possible?(@parent_issue) |
|
|||
621 | # move accepted inside tree |
|
|||
622 | else |
|
621 | else | |
623 | errors.add :parent_issue_id, :invalid |
|
622 | errors.add :parent_issue_id, :invalid | |
624 | end |
|
623 | end | |
@@ -1184,6 +1183,10 class Issue < ActiveRecord::Base | |||||
1184 | end |
|
1183 | end | |
1185 | end |
|
1184 | end | |
1186 |
|
1185 | |||
|
1186 | def set_parent_id | |||
|
1187 | self.parent_id = parent_issue_id | |||
|
1188 | end | |||
|
1189 | ||||
1187 | # Returns true if issue's project is a valid |
|
1190 | # Returns true if issue's project is a valid | |
1188 | # parent issue project |
|
1191 | # parent issue project | |
1189 | def valid_parent_project?(issue=parent) |
|
1192 | def valid_parent_project?(issue=parent) | |
@@ -1366,14 +1369,7 class Issue < ActiveRecord::Base | |||||
1366 | end |
|
1369 | end | |
1367 |
|
1370 | |||
1368 | def update_nested_set_attributes |
|
1371 | def update_nested_set_attributes | |
1369 |
if |
|
1372 | if parent_id_changed? | |
1370 | # issue was just created |
|
|||
1371 | self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) |
|
|||
1372 | Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id]) |
|
|||
1373 | if @parent_issue |
|
|||
1374 | move_to_child_of(@parent_issue) |
|
|||
1375 | end |
|
|||
1376 | elsif parent_issue_id != parent_id |
|
|||
1377 | update_nested_set_attributes_on_parent_change |
|
1373 | update_nested_set_attributes_on_parent_change | |
1378 | end |
|
1374 | end | |
1379 | remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) |
|
1375 | remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) | |
@@ -1381,31 +1377,7 class Issue < ActiveRecord::Base | |||||
1381 |
|
1377 | |||
1382 | # Updates the nested set for when an existing issue is moved |
|
1378 | # Updates the nested set for when an existing issue is moved | |
1383 | def update_nested_set_attributes_on_parent_change |
|
1379 | def update_nested_set_attributes_on_parent_change | |
1384 | former_parent_id = parent_id |
|
1380 | former_parent_id = parent_id_was | |
1385 | # moving an existing issue |
|
|||
1386 | if @parent_issue && @parent_issue.root_id == root_id |
|
|||
1387 | # inside the same tree |
|
|||
1388 | move_to_child_of(@parent_issue) |
|
|||
1389 | else |
|
|||
1390 | # to another tree |
|
|||
1391 | unless root? |
|
|||
1392 | move_to_right_of(root) |
|
|||
1393 | end |
|
|||
1394 | old_root_id = root_id |
|
|||
1395 | in_tenacious_transaction do |
|
|||
1396 | @parent_issue.reload_nested_set if @parent_issue |
|
|||
1397 | self.reload_nested_set |
|
|||
1398 | self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) |
|
|||
1399 | cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt] |
|
|||
1400 | self.class.base_class.select('id').lock(true).where(cond) |
|
|||
1401 | offset = rdm_right_most_bound + 1 - lft |
|
|||
1402 | Issue.where(cond). |
|
|||
1403 | update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset]) |
|
|||
1404 | end |
|
|||
1405 | if @parent_issue |
|
|||
1406 | move_to_child_of(@parent_issue) |
|
|||
1407 | end |
|
|||
1408 | end |
|
|||
1409 | # delete invalid relations of all descendants |
|
1381 | # delete invalid relations of all descendants | |
1410 | self_and_descendants.each do |issue| |
|
1382 | self_and_descendants.each do |issue| | |
1411 | issue.relations.each do |relation| |
|
1383 | issue.relations.each do |relation| | |
@@ -1416,16 +1388,11 class Issue < ActiveRecord::Base | |||||
1416 | recalculate_attributes_for(former_parent_id) if former_parent_id |
|
1388 | recalculate_attributes_for(former_parent_id) if former_parent_id | |
1417 | end |
|
1389 | end | |
1418 |
|
1390 | |||
1419 | def rdm_right_most_bound |
|
|||
1420 | right_most_node = |
|
|||
1421 | self.class.base_class.unscoped. |
|
|||
1422 | order("#{quoted_right_column_full_name} desc").limit(1).lock(true).first |
|
|||
1423 | right_most_node ? (right_most_node[right_column_name] || 0 ) : 0 |
|
|||
1424 | end |
|
|||
1425 | private :rdm_right_most_bound |
|
|||
1426 |
|
||||
1427 | def update_parent_attributes |
|
1391 | def update_parent_attributes | |
1428 | recalculate_attributes_for(parent_id) if parent_id |
|
1392 | if parent_id | |
|
1393 | recalculate_attributes_for(parent_id) | |||
|
1394 | association(:parent).reset | |||
|
1395 | end | |||
1429 | end |
|
1396 | end | |
1430 |
|
1397 | |||
1431 | def recalculate_attributes_for(issue_id) |
|
1398 | def recalculate_attributes_for(issue_id) |
@@ -17,6 +17,7 | |||||
17 |
|
17 | |||
18 | class Project < ActiveRecord::Base |
|
18 | class Project < ActiveRecord::Base | |
19 | include Redmine::SafeAttributes |
|
19 | include Redmine::SafeAttributes | |
|
20 | include Redmine::NestedSet::ProjectNestedSet | |||
20 |
|
21 | |||
21 | # Project statuses |
|
22 | # Project statuses | |
22 | STATUS_ACTIVE = 1 |
|
23 | STATUS_ACTIVE = 1 | |
@@ -58,7 +59,6 class Project < ActiveRecord::Base | |||||
58 | :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", |
|
59 | :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", | |
59 | :association_foreign_key => 'custom_field_id' |
|
60 | :association_foreign_key => 'custom_field_id' | |
60 |
|
61 | |||
61 | acts_as_nested_set :dependent => :destroy |
|
|||
62 | acts_as_attachable :view_permission => :view_files, |
|
62 | acts_as_attachable :view_permission => :view_files, | |
63 | :edit_permission => :manage_files, |
|
63 | :edit_permission => :manage_files, | |
64 | :delete_permission => :manage_files |
|
64 | :delete_permission => :manage_files | |
@@ -81,8 +81,8 class Project < ActiveRecord::Base | |||||
81 | # reserved words |
|
81 | # reserved words | |
82 | validates_exclusion_of :identifier, :in => %w( new ) |
|
82 | validates_exclusion_of :identifier, :in => %w( new ) | |
83 |
|
83 | |||
84 | after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?} |
|
|||
85 | after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?} |
|
84 | after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?} | |
|
85 | after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?} | |||
86 | before_destroy :delete_all_members |
|
86 | before_destroy :delete_all_members | |
87 |
|
87 | |||
88 | scope :has_module, lambda {|mod| |
|
88 | scope :has_module, lambda {|mod| | |
@@ -414,7 +414,9 class Project < ActiveRecord::Base | |||||
414 | # Nothing to do |
|
414 | # Nothing to do | |
415 | true |
|
415 | true | |
416 | elsif p.nil? || (p.active? && move_possible?(p)) |
|
416 | elsif p.nil? || (p.active? && move_possible?(p)) | |
417 | set_or_update_position_under(p) |
|
417 | self.parent = p | |
|
418 | save | |||
|
419 | p.reload if p | |||
418 | Issue.update_versions_from_hierarchy_change(self) |
|
420 | Issue.update_versions_from_hierarchy_change(self) | |
419 | true |
|
421 | true | |
420 | else |
|
422 | else | |
@@ -423,17 +425,6 class Project < ActiveRecord::Base | |||||
423 | end |
|
425 | end | |
424 | end |
|
426 | end | |
425 |
|
427 | |||
426 | # Recalculates all lft and rgt values based on project names |
|
|||
427 | # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid |
|
|||
428 | # Used in BuildProjectsTree migration |
|
|||
429 | def self.rebuild_tree! |
|
|||
430 | transaction do |
|
|||
431 | update_all "lft = NULL, rgt = NULL" |
|
|||
432 | rebuild!(false) |
|
|||
433 | all.each { |p| p.set_or_update_position_under(p.parent) } |
|
|||
434 | end |
|
|||
435 | end |
|
|||
436 |
|
||||
437 | # Returns an array of the trackers used by the project and its active sub projects |
|
428 | # Returns an array of the trackers used by the project and its active sub projects | |
438 | def rolled_up_trackers |
|
429 | def rolled_up_trackers | |
439 | @rolled_up_trackers ||= |
|
430 | @rolled_up_trackers ||= | |
@@ -781,11 +772,6 class Project < ActiveRecord::Base | |||||
781 |
|
772 | |||
782 | private |
|
773 | private | |
783 |
|
774 | |||
784 | def after_parent_changed(parent_was) |
|
|||
785 | remove_inherited_member_roles |
|
|||
786 | add_inherited_member_roles |
|
|||
787 | end |
|
|||
788 |
|
||||
789 | def update_inherited_members |
|
775 | def update_inherited_members | |
790 | if parent |
|
776 | if parent | |
791 | if inherit_members? && !inherit_members_was |
|
777 | if inherit_members? && !inherit_members_was | |
@@ -816,6 +802,7 class Project < ActiveRecord::Base | |||||
816 | end |
|
802 | end | |
817 | member.save! |
|
803 | member.save! | |
818 | end |
|
804 | end | |
|
805 | memberships.reset | |||
819 | end |
|
806 | end | |
820 | end |
|
807 | end | |
821 |
|
808 | |||
@@ -1043,34 +1030,4 class Project < ActiveRecord::Base | |||||
1043 | end |
|
1030 | end | |
1044 | update_attribute :status, STATUS_ARCHIVED |
|
1031 | update_attribute :status, STATUS_ARCHIVED | |
1045 | end |
|
1032 | end | |
1046 |
|
||||
1047 | def update_position_under_parent |
|
|||
1048 | set_or_update_position_under(parent) |
|
|||
1049 | end |
|
|||
1050 |
|
||||
1051 | public |
|
|||
1052 |
|
||||
1053 | # Inserts/moves the project so that target's children or root projects stay alphabetically sorted |
|
|||
1054 | def set_or_update_position_under(target_parent) |
|
|||
1055 | parent_was = parent |
|
|||
1056 | sibs = (target_parent.nil? ? self.class.roots : target_parent.children) |
|
|||
1057 | to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase } |
|
|||
1058 |
|
||||
1059 | if to_be_inserted_before |
|
|||
1060 | move_to_left_of(to_be_inserted_before) |
|
|||
1061 | elsif target_parent.nil? |
|
|||
1062 | if sibs.empty? |
|
|||
1063 | # move_to_root adds the project in first (ie. left) position |
|
|||
1064 | move_to_root |
|
|||
1065 | else |
|
|||
1066 | move_to_right_of(sibs.last) unless self == sibs.last |
|
|||
1067 | end |
|
|||
1068 | else |
|
|||
1069 | # move_to_child_of adds the project in last (ie.right) position |
|
|||
1070 | move_to_child_of(target_parent) |
|
|||
1071 | end |
|
|||
1072 | if parent_was != target_parent |
|
|||
1073 | after_parent_changed(parent_was) |
|
|||
1074 | end |
|
|||
1075 | end |
|
|||
1076 | end |
|
1033 | end |
@@ -193,16 +193,3 if Rails::VERSION::MAJOR < 4 && RUBY_VERSION >= "2.1" | |||||
193 | end |
|
193 | end | |
194 | end |
|
194 | end | |
195 | end |
|
195 | end | |
196 |
|
||||
197 | module CollectiveIdea |
|
|||
198 | module Acts |
|
|||
199 | module NestedSet |
|
|||
200 | module Model |
|
|||
201 | def leaf_with_new_record? |
|
|||
202 | new_record? || leaf_without_new_record? |
|
|||
203 | end |
|
|||
204 | alias_method_chain :leaf?, :new_record |
|
|||
205 | end |
|
|||
206 | end |
|
|||
207 | end |
|
|||
208 | end |
|
@@ -30,7 +30,6 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s | |||||
30 | require File.expand_path(File.dirname(__FILE__) + '/object_helpers') |
|
30 | require File.expand_path(File.dirname(__FILE__) + '/object_helpers') | |
31 | include ObjectHelpers |
|
31 | include ObjectHelpers | |
32 |
|
32 | |||
33 | require 'awesome_nested_set/version' |
|
|||
34 | require 'net/ldap' |
|
33 | require 'net/ldap' | |
35 |
|
34 | |||
36 | class ActionView::TestCase |
|
35 | class ActionView::TestCase | |
@@ -214,15 +213,9 class ActiveSupport::TestCase | |||||
214 | mail.parts.first.body.encoded |
|
213 | mail.parts.first.body.encoded | |
215 | end |
|
214 | end | |
216 |
|
215 | |||
217 | # awesome_nested_set new node lft and rgt value changed this refactor revision. |
|
216 | # Returns the lft value for a new root issue | |
218 | # https://github.com/collectiveidea/awesome_nested_set/commit/199fca9bb938e40200cd90714dc69247ef017c61 |
|
|||
219 | # The reason of behavior change is that "self.class.base_class.unscoped" was added to this line. |
|
|||
220 | # https://github.com/collectiveidea/awesome_nested_set/commit/199fca9bb9#diff-f61b59a5e6319024e211b0ffdd0e4ef1R273 |
|
|||
221 | # It seems correct behavior because of this line comment. |
|
|||
222 | # https://github.com/collectiveidea/awesome_nested_set/blame/199fca9bb9/lib/awesome_nested_set/model.rb#L278 |
|
|||
223 | def new_issue_lft |
|
217 | def new_issue_lft | |
224 | # ::AwesomeNestedSet::VERSION > "2.1.6" ? Issue.maximum(:rgt) + 1 : 1 |
|
218 | 1 | |
225 | Issue.maximum(:rgt) + 1 |
|
|||
226 | end |
|
219 | end | |
227 | end |
|
220 | end | |
228 |
|
221 |
General Comments 0
You need to be logged in to leave comments.
Login now