##// END OF EJS Templates
Makes user unwatch what he can no longer view after its permissions have changed (#3589)....
Jean-Philippe Lang -
r3053:f33231181f95
parent child
Show More
@@ -0,0 +1,9
1 desc 'Removes watchers from what they can no longer view.'
2
3 namespace :redmine do
4 namespace :watchers do
5 task :prune => :environment do
6 Watcher.prune
7 end
8 end
9 end
@@ -1,46 +1,50
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Board < ActiveRecord::Base
18 class Board < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
20 has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
21 has_many :messages, :dependent => :delete_all, :order => "#{Message.table_name}.created_on DESC"
21 has_many :messages, :dependent => :delete_all, :order => "#{Message.table_name}.created_on DESC"
22 belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
22 belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
23 acts_as_list :scope => :project_id
23 acts_as_list :scope => :project_id
24 acts_as_watchable
24 acts_as_watchable
25
25
26 validates_presence_of :name, :description
26 validates_presence_of :name, :description
27 validates_length_of :name, :maximum => 30
27 validates_length_of :name, :maximum => 30
28 validates_length_of :description, :maximum => 255
28 validates_length_of :description, :maximum => 255
29
29
30 def visible?(user=User.current)
31 !user.nil? && user.allowed_to?(:view_messages, project)
32 end
33
30 def to_s
34 def to_s
31 name
35 name
32 end
36 end
33
37
34 def reset_counters!
38 def reset_counters!
35 self.class.reset_counters!(id)
39 self.class.reset_counters!(id)
36 end
40 end
37
41
38 # Updates topics_count, messages_count and last_message_id attributes for +board_id+
42 # Updates topics_count, messages_count and last_message_id attributes for +board_id+
39 def self.reset_counters!(board_id)
43 def self.reset_counters!(board_id)
40 board_id = board_id.to_i
44 board_id = board_id.to_i
41 update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
45 update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
42 " messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
46 " messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
43 " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
47 " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
44 ["id = ?", board_id])
48 ["id = ?", board_id])
45 end
49 end
46 end
50 end
@@ -1,66 +1,81
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Member < ActiveRecord::Base
18 class Member < ActiveRecord::Base
19 belongs_to :user
19 belongs_to :user
20 belongs_to :principal, :foreign_key => 'user_id'
20 belongs_to :principal, :foreign_key => 'user_id'
21 has_many :member_roles, :dependent => :destroy
21 has_many :member_roles, :dependent => :destroy
22 has_many :roles, :through => :member_roles
22 has_many :roles, :through => :member_roles
23 belongs_to :project
23 belongs_to :project
24
24
25 validates_presence_of :principal, :project
25 validates_presence_of :principal, :project
26 validates_uniqueness_of :user_id, :scope => :project_id
26 validates_uniqueness_of :user_id, :scope => :project_id
27
27
28 after_destroy :unwatch_from_permission_change
29
28 def name
30 def name
29 self.user.name
31 self.user.name
30 end
32 end
31
33
32 alias :base_role_ids= :role_ids=
34 alias :base_role_ids= :role_ids=
33 def role_ids=(arg)
35 def role_ids=(arg)
34 ids = (arg || []).collect(&:to_i) - [0]
36 ids = (arg || []).collect(&:to_i) - [0]
35 # Keep inherited roles
37 # Keep inherited roles
36 ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
38 ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
37
39
38 new_role_ids = ids - role_ids
40 new_role_ids = ids - role_ids
39 # Add new roles
41 # Add new roles
40 new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
42 new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
41 # Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
43 # Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
42 member_roles.select {|mr| !ids.include?(mr.role_id)}.each(&:destroy)
44 member_roles_to_destroy = member_roles.select {|mr| !ids.include?(mr.role_id)}
45 if member_roles_to_destroy.any?
46 member_roles_to_destroy.each(&:destroy)
47 unwatch_from_permission_change
48 end
43 end
49 end
44
50
45 def <=>(member)
51 def <=>(member)
46 a, b = roles.sort.first, member.roles.sort.first
52 a, b = roles.sort.first, member.roles.sort.first
47 a == b ? (principal <=> member.principal) : (a <=> b)
53 a == b ? (principal <=> member.principal) : (a <=> b)
48 end
54 end
49
55
50 def deletable?
56 def deletable?
51 member_roles.detect {|mr| mr.inherited_from}.nil?
57 member_roles.detect {|mr| mr.inherited_from}.nil?
52 end
58 end
53
59
54 def before_destroy
60 def before_destroy
55 if user
61 if user
56 # remove category based auto assignments for this member
62 # remove category based auto assignments for this member
57 IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
63 IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
58 end
64 end
59 end
65 end
60
66
61 protected
67 protected
62
68
63 def validate
69 def validate
64 errors.add_to_base "Role can't be blank" if member_roles.empty? && roles.empty?
70 errors.add_to_base "Role can't be blank" if member_roles.empty? && roles.empty?
65 end
71 end
72
73 private
74
75 # Unwatch things that the user is no longer allowed to view inside project
76 def unwatch_from_permission_change
77 if user
78 Watcher.prune(:user => user, :project => project)
79 end
80 end
66 end
81 end
@@ -1,54 +1,59
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MemberRole < ActiveRecord::Base
18 class MemberRole < ActiveRecord::Base
19 belongs_to :member
19 belongs_to :member
20 belongs_to :role
20 belongs_to :role
21
21
22 after_destroy :remove_member_if_empty
22 after_destroy :remove_member_if_empty
23
23
24 after_create :add_role_to_group_users
24 after_create :add_role_to_group_users
25 after_destroy :remove_role_from_group_users
25 after_destroy :remove_role_from_group_users
26
26
27 validates_presence_of :role
27 validates_presence_of :role
28
28
29 def validate
29 def validate
30 errors.add :role_id, :invalid if role && !role.member?
30 errors.add :role_id, :invalid if role && !role.member?
31 end
31 end
32
32
33 private
33 private
34
34
35 def remove_member_if_empty
35 def remove_member_if_empty
36 if member.roles.empty?
36 if member.roles.empty?
37 member.destroy
37 member.destroy
38 end
38 end
39 end
39 end
40
40
41 def add_role_to_group_users
41 def add_role_to_group_users
42 if member.principal.is_a?(Group)
42 if member.principal.is_a?(Group)
43 member.principal.users.each do |user|
43 member.principal.users.each do |user|
44 user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
44 user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
45 user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
45 user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
46 user_member.save!
46 user_member.save!
47 end
47 end
48 end
48 end
49 end
49 end
50
50
51 def remove_role_from_group_users
51 def remove_role_from_group_users
52 MemberRole.find(:all, :conditions => { :inherited_from => id }).each(&:destroy)
52 MemberRole.find(:all, :conditions => { :inherited_from => id }).group_by(&:member).each do |member, member_roles|
53 member_roles.each(&:destroy)
54 if member && member.user
55 Watcher.prune(:user => member.user, :project => member.project)
56 end
57 end
53 end
58 end
54 end
59 end
@@ -1,94 +1,98
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Message < ActiveRecord::Base
18 class Message < ActiveRecord::Base
19 belongs_to :board
19 belongs_to :board
20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
22 acts_as_attachable
22 acts_as_attachable
23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
24
24
25 acts_as_searchable :columns => ['subject', 'content'],
25 acts_as_searchable :columns => ['subject', 'content'],
26 :include => {:board => :project},
26 :include => {:board => :project},
27 :project_key => 'project_id',
27 :project_key => 'project_id',
28 :date_column => "#{table_name}.created_on"
28 :date_column => "#{table_name}.created_on"
29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
30 :description => :content,
30 :description => :content,
31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
32 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
32 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
33 {:id => o.parent_id, :anchor => "message-#{o.id}"})}
33 {:id => o.parent_id, :anchor => "message-#{o.id}"})}
34
34
35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
36 :author_key => :author_id
36 :author_key => :author_id
37 acts_as_watchable
37 acts_as_watchable
38
38
39 attr_protected :locked, :sticky
39 attr_protected :locked, :sticky
40 validates_presence_of :board, :subject, :content
40 validates_presence_of :board, :subject, :content
41 validates_length_of :subject, :maximum => 255
41 validates_length_of :subject, :maximum => 255
42
42
43 after_create :add_author_as_watcher
43 after_create :add_author_as_watcher
44
44
45 def visible?(user=User.current)
46 !user.nil? && user.allowed_to?(:view_messages, project)
47 end
48
45 def validate_on_create
49 def validate_on_create
46 # Can not reply to a locked topic
50 # Can not reply to a locked topic
47 errors.add_to_base 'Topic is locked' if root.locked? && self != root
51 errors.add_to_base 'Topic is locked' if root.locked? && self != root
48 end
52 end
49
53
50 def after_create
54 def after_create
51 if parent
55 if parent
52 parent.reload.update_attribute(:last_reply_id, self.id)
56 parent.reload.update_attribute(:last_reply_id, self.id)
53 end
57 end
54 board.reset_counters!
58 board.reset_counters!
55 end
59 end
56
60
57 def after_update
61 def after_update
58 if board_id_changed?
62 if board_id_changed?
59 Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
63 Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
60 Board.reset_counters!(board_id_was)
64 Board.reset_counters!(board_id_was)
61 Board.reset_counters!(board_id)
65 Board.reset_counters!(board_id)
62 end
66 end
63 end
67 end
64
68
65 def after_destroy
69 def after_destroy
66 board.reset_counters!
70 board.reset_counters!
67 end
71 end
68
72
69 def sticky=(arg)
73 def sticky=(arg)
70 write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
74 write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
71 end
75 end
72
76
73 def sticky?
77 def sticky?
74 sticky == 1
78 sticky == 1
75 end
79 end
76
80
77 def project
81 def project
78 board.project
82 board.project
79 end
83 end
80
84
81 def editable_by?(usr)
85 def editable_by?(usr)
82 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
86 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
83 end
87 end
84
88
85 def destroyable_by?(usr)
89 def destroyable_by?(usr)
86 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
90 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
87 end
91 end
88
92
89 private
93 private
90
94
91 def add_author_as_watcher
95 def add_author_as_watcher
92 Watcher.create(:watchable => self.root, :user => author)
96 Watcher.create(:watchable => self.root, :user => author)
93 end
97 end
94 end
98 end
@@ -1,30 +1,65
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Watcher < ActiveRecord::Base
18 class Watcher < ActiveRecord::Base
19 belongs_to :watchable, :polymorphic => true
19 belongs_to :watchable, :polymorphic => true
20 belongs_to :user
20 belongs_to :user
21
21
22 validates_presence_of :user
22 validates_presence_of :user
23 validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
23 validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
24
24
25 # Unwatch things that users are no longer allowed to view
26 def self.prune(options={})
27 if options.has_key?(:user)
28 prune_single_user(options[:user], options)
29 else
30 pruned = 0
31 User.find(:all, :conditions => "id IN (SELECT DISTINCT user_id FROM #{table_name})").each do |user|
32 pruned += prune_single_user(user, options)
33 end
34 pruned
35 end
36 end
37
25 protected
38 protected
26
39
27 def validate
40 def validate
28 errors.add :user_id, :invalid unless user.nil? || user.active?
41 errors.add :user_id, :invalid unless user.nil? || user.active?
29 end
42 end
43
44 private
45
46 def self.prune_single_user(user, options={})
47 return unless user.is_a?(User)
48 pruned = 0
49 find(:all, :conditions => {:user_id => user.id}).each do |watcher|
50 next if watcher.watchable.nil?
51
52 if options.has_key?(:project)
53 next unless watcher.watchable.respond_to?(:project) && watcher.watchable.project == options[:project]
54 end
55
56 if watcher.watchable.respond_to?(:visible?)
57 unless watcher.watchable.visible?(user)
58 watcher.destroy
59 pruned += 1
60 end
61 end
62 end
63 pruned
64 end
30 end
65 end
@@ -1,75 +1,79
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Wiki < ActiveRecord::Base
18 class Wiki < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
20 has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
21 has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
21 has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
22
22
23 acts_as_watchable
23 acts_as_watchable
24
24
25 validates_presence_of :start_page
25 validates_presence_of :start_page
26 validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
26 validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
27
27
28 def visible?(user=User.current)
29 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
30 end
31
28 # find the page with the given title
32 # find the page with the given title
29 # if page doesn't exist, return a new page
33 # if page doesn't exist, return a new page
30 def find_or_new_page(title)
34 def find_or_new_page(title)
31 title = start_page if title.blank?
35 title = start_page if title.blank?
32 find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
36 find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
33 end
37 end
34
38
35 # find the page with the given title
39 # find the page with the given title
36 def find_page(title, options = {})
40 def find_page(title, options = {})
37 title = start_page if title.blank?
41 title = start_page if title.blank?
38 title = Wiki.titleize(title)
42 title = Wiki.titleize(title)
39 page = pages.find_by_title(title)
43 page = pages.find_by_title(title)
40 if !page && !(options[:with_redirect] == false)
44 if !page && !(options[:with_redirect] == false)
41 # search for a redirect
45 # search for a redirect
42 redirect = redirects.find_by_title(title)
46 redirect = redirects.find_by_title(title)
43 page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
47 page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
44 end
48 end
45 page
49 page
46 end
50 end
47
51
48 # Finds a page by title
52 # Finds a page by title
49 # The given string can be of one of the forms: "title" or "project:title"
53 # The given string can be of one of the forms: "title" or "project:title"
50 # Examples:
54 # Examples:
51 # Wiki.find_page("bar", project => foo)
55 # Wiki.find_page("bar", project => foo)
52 # Wiki.find_page("foo:bar")
56 # Wiki.find_page("foo:bar")
53 def self.find_page(title, options = {})
57 def self.find_page(title, options = {})
54 project = options[:project]
58 project = options[:project]
55 if title.to_s =~ %r{^([^\:]+)\:(.*)$}
59 if title.to_s =~ %r{^([^\:]+)\:(.*)$}
56 project_identifier, title = $1, $2
60 project_identifier, title = $1, $2
57 project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
61 project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
58 end
62 end
59 if project && project.wiki
63 if project && project.wiki
60 page = project.wiki.find_page(title)
64 page = project.wiki.find_page(title)
61 if page && page.content
65 if page && page.content
62 page
66 page
63 end
67 end
64 end
68 end
65 end
69 end
66
70
67 # turn a string into a valid page title
71 # turn a string into a valid page title
68 def self.titleize(title)
72 def self.titleize(title)
69 # replace spaces with _ and remove unwanted caracters
73 # replace spaces with _ and remove unwanted caracters
70 title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
74 title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
71 # upcase the first letter
75 # upcase the first letter
72 title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
76 title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
73 title
77 title
74 end
78 end
75 end
79 end
@@ -1,189 +1,193
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'diff'
18 require 'diff'
19 require 'enumerator'
19 require 'enumerator'
20
20
21 class WikiPage < ActiveRecord::Base
21 class WikiPage < ActiveRecord::Base
22 belongs_to :wiki
22 belongs_to :wiki
23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
24 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
24 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
25 acts_as_tree :dependent => :nullify, :order => 'title'
25 acts_as_tree :dependent => :nullify, :order => 'title'
26
26
27 acts_as_watchable
27 acts_as_watchable
28 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
28 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
29 :description => :text,
29 :description => :text,
30 :datetime => :created_on,
30 :datetime => :created_on,
31 :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
31 :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
32
32
33 acts_as_searchable :columns => ['title', 'text'],
33 acts_as_searchable :columns => ['title', 'text'],
34 :include => [{:wiki => :project}, :content],
34 :include => [{:wiki => :project}, :content],
35 :project_key => "#{Wiki.table_name}.project_id"
35 :project_key => "#{Wiki.table_name}.project_id"
36
36
37 attr_accessor :redirect_existing_links
37 attr_accessor :redirect_existing_links
38
38
39 validates_presence_of :title
39 validates_presence_of :title
40 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
40 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
41 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
41 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
42 validates_associated :content
42 validates_associated :content
43
43
44 def visible?(user=User.current)
45 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
46 end
47
44 def title=(value)
48 def title=(value)
45 value = Wiki.titleize(value)
49 value = Wiki.titleize(value)
46 @previous_title = read_attribute(:title) if @previous_title.blank?
50 @previous_title = read_attribute(:title) if @previous_title.blank?
47 write_attribute(:title, value)
51 write_attribute(:title, value)
48 end
52 end
49
53
50 def before_save
54 def before_save
51 self.title = Wiki.titleize(title)
55 self.title = Wiki.titleize(title)
52 # Manage redirects if the title has changed
56 # Manage redirects if the title has changed
53 if !@previous_title.blank? && (@previous_title != title) && !new_record?
57 if !@previous_title.blank? && (@previous_title != title) && !new_record?
54 # Update redirects that point to the old title
58 # Update redirects that point to the old title
55 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
59 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
56 r.redirects_to = title
60 r.redirects_to = title
57 r.title == r.redirects_to ? r.destroy : r.save
61 r.title == r.redirects_to ? r.destroy : r.save
58 end
62 end
59 # Remove redirects for the new title
63 # Remove redirects for the new title
60 wiki.redirects.find_all_by_title(title).each(&:destroy)
64 wiki.redirects.find_all_by_title(title).each(&:destroy)
61 # Create a redirect to the new title
65 # Create a redirect to the new title
62 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
66 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
63 @previous_title = nil
67 @previous_title = nil
64 end
68 end
65 end
69 end
66
70
67 def before_destroy
71 def before_destroy
68 # Remove redirects to this page
72 # Remove redirects to this page
69 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
73 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
70 end
74 end
71
75
72 def pretty_title
76 def pretty_title
73 WikiPage.pretty_title(title)
77 WikiPage.pretty_title(title)
74 end
78 end
75
79
76 def content_for_version(version=nil)
80 def content_for_version(version=nil)
77 result = content.versions.find_by_version(version.to_i) if version
81 result = content.versions.find_by_version(version.to_i) if version
78 result ||= content
82 result ||= content
79 result
83 result
80 end
84 end
81
85
82 def diff(version_to=nil, version_from=nil)
86 def diff(version_to=nil, version_from=nil)
83 version_to = version_to ? version_to.to_i : self.content.version
87 version_to = version_to ? version_to.to_i : self.content.version
84 version_from = version_from ? version_from.to_i : version_to - 1
88 version_from = version_from ? version_from.to_i : version_to - 1
85 version_to, version_from = version_from, version_to unless version_from < version_to
89 version_to, version_from = version_from, version_to unless version_from < version_to
86
90
87 content_to = content.versions.find_by_version(version_to)
91 content_to = content.versions.find_by_version(version_to)
88 content_from = content.versions.find_by_version(version_from)
92 content_from = content.versions.find_by_version(version_from)
89
93
90 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
94 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
91 end
95 end
92
96
93 def annotate(version=nil)
97 def annotate(version=nil)
94 version = version ? version.to_i : self.content.version
98 version = version ? version.to_i : self.content.version
95 c = content.versions.find_by_version(version)
99 c = content.versions.find_by_version(version)
96 c ? WikiAnnotate.new(c) : nil
100 c ? WikiAnnotate.new(c) : nil
97 end
101 end
98
102
99 def self.pretty_title(str)
103 def self.pretty_title(str)
100 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
104 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
101 end
105 end
102
106
103 def project
107 def project
104 wiki.project
108 wiki.project
105 end
109 end
106
110
107 def text
111 def text
108 content.text if content
112 content.text if content
109 end
113 end
110
114
111 # Returns true if usr is allowed to edit the page, otherwise false
115 # Returns true if usr is allowed to edit the page, otherwise false
112 def editable_by?(usr)
116 def editable_by?(usr)
113 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
117 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
114 end
118 end
115
119
116 def attachments_deletable?(usr=User.current)
120 def attachments_deletable?(usr=User.current)
117 editable_by?(usr) && super(usr)
121 editable_by?(usr) && super(usr)
118 end
122 end
119
123
120 def parent_title
124 def parent_title
121 @parent_title || (self.parent && self.parent.pretty_title)
125 @parent_title || (self.parent && self.parent.pretty_title)
122 end
126 end
123
127
124 def parent_title=(t)
128 def parent_title=(t)
125 @parent_title = t
129 @parent_title = t
126 parent_page = t.blank? ? nil : self.wiki.find_page(t)
130 parent_page = t.blank? ? nil : self.wiki.find_page(t)
127 self.parent = parent_page
131 self.parent = parent_page
128 end
132 end
129
133
130 protected
134 protected
131
135
132 def validate
136 def validate
133 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
137 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
134 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
138 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
135 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
139 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
136 end
140 end
137 end
141 end
138
142
139 class WikiDiff
143 class WikiDiff
140 attr_reader :diff, :words, :content_to, :content_from
144 attr_reader :diff, :words, :content_to, :content_from
141
145
142 def initialize(content_to, content_from)
146 def initialize(content_to, content_from)
143 @content_to = content_to
147 @content_to = content_to
144 @content_from = content_from
148 @content_from = content_from
145 @words = content_to.text.split(/(\s+)/)
149 @words = content_to.text.split(/(\s+)/)
146 @words = @words.select {|word| word != ' '}
150 @words = @words.select {|word| word != ' '}
147 words_from = content_from.text.split(/(\s+)/)
151 words_from = content_from.text.split(/(\s+)/)
148 words_from = words_from.select {|word| word != ' '}
152 words_from = words_from.select {|word| word != ' '}
149 @diff = words_from.diff @words
153 @diff = words_from.diff @words
150 end
154 end
151 end
155 end
152
156
153 class WikiAnnotate
157 class WikiAnnotate
154 attr_reader :lines, :content
158 attr_reader :lines, :content
155
159
156 def initialize(content)
160 def initialize(content)
157 @content = content
161 @content = content
158 current = content
162 current = content
159 current_lines = current.text.split(/\r?\n/)
163 current_lines = current.text.split(/\r?\n/)
160 @lines = current_lines.collect {|t| [nil, nil, t]}
164 @lines = current_lines.collect {|t| [nil, nil, t]}
161 positions = []
165 positions = []
162 current_lines.size.times {|i| positions << i}
166 current_lines.size.times {|i| positions << i}
163 while (current.previous)
167 while (current.previous)
164 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
168 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
165 d.each_slice(3) do |s|
169 d.each_slice(3) do |s|
166 sign, line = s[0], s[1]
170 sign, line = s[0], s[1]
167 if sign == '+' && positions[line] && positions[line] != -1
171 if sign == '+' && positions[line] && positions[line] != -1
168 if @lines[positions[line]][0].nil?
172 if @lines[positions[line]][0].nil?
169 @lines[positions[line]][0] = current.version
173 @lines[positions[line]][0] = current.version
170 @lines[positions[line]][1] = current.author
174 @lines[positions[line]][1] = current.author
171 end
175 end
172 end
176 end
173 end
177 end
174 d.each_slice(3) do |s|
178 d.each_slice(3) do |s|
175 sign, line = s[0], s[1]
179 sign, line = s[0], s[1]
176 if sign == '-'
180 if sign == '-'
177 positions.insert(line, -1)
181 positions.insert(line, -1)
178 else
182 else
179 positions[line] = nil
183 positions[line] = nil
180 end
184 end
181 end
185 end
182 positions.compact!
186 positions.compact!
183 # Stop if every line is annotated
187 # Stop if every line is annotated
184 break unless @lines.detect { |line| line[0].nil? }
188 break unless @lines.detect { |line| line[0].nil? }
185 current = current.previous
189 current = current.previous
186 end
190 end
187 @lines.each { |line| line[0] ||= current.version }
191 @lines.each { |line| line[0] ||= current.version }
188 end
192 end
189 end
193 end
@@ -1,61 +1,65
1 ---
1 ---
2 enabled_modules_001:
2 enabled_modules_001:
3 name: issue_tracking
3 name: issue_tracking
4 project_id: 1
4 project_id: 1
5 id: 1
5 id: 1
6 enabled_modules_002:
6 enabled_modules_002:
7 name: time_tracking
7 name: time_tracking
8 project_id: 1
8 project_id: 1
9 id: 2
9 id: 2
10 enabled_modules_003:
10 enabled_modules_003:
11 name: news
11 name: news
12 project_id: 1
12 project_id: 1
13 id: 3
13 id: 3
14 enabled_modules_004:
14 enabled_modules_004:
15 name: documents
15 name: documents
16 project_id: 1
16 project_id: 1
17 id: 4
17 id: 4
18 enabled_modules_005:
18 enabled_modules_005:
19 name: files
19 name: files
20 project_id: 1
20 project_id: 1
21 id: 5
21 id: 5
22 enabled_modules_006:
22 enabled_modules_006:
23 name: wiki
23 name: wiki
24 project_id: 1
24 project_id: 1
25 id: 6
25 id: 6
26 enabled_modules_007:
26 enabled_modules_007:
27 name: repository
27 name: repository
28 project_id: 1
28 project_id: 1
29 id: 7
29 id: 7
30 enabled_modules_008:
30 enabled_modules_008:
31 name: boards
31 name: boards
32 project_id: 1
32 project_id: 1
33 id: 8
33 id: 8
34 enabled_modules_009:
34 enabled_modules_009:
35 name: repository
35 name: repository
36 project_id: 3
36 project_id: 3
37 id: 9
37 id: 9
38 enabled_modules_010:
38 enabled_modules_010:
39 name: wiki
39 name: wiki
40 project_id: 3
40 project_id: 3
41 id: 10
41 id: 10
42 enabled_modules_011:
42 enabled_modules_011:
43 name: issue_tracking
43 name: issue_tracking
44 project_id: 2
44 project_id: 2
45 id: 11
45 id: 11
46 enabled_modules_012:
46 enabled_modules_012:
47 name: time_tracking
47 name: time_tracking
48 project_id: 3
48 project_id: 3
49 id: 12
49 id: 12
50 enabled_modules_013:
50 enabled_modules_013:
51 name: issue_tracking
51 name: issue_tracking
52 project_id: 3
52 project_id: 3
53 id: 13
53 id: 13
54 enabled_modules_014:
54 enabled_modules_014:
55 name: issue_tracking
55 name: issue_tracking
56 project_id: 5
56 project_id: 5
57 id: 14
57 id: 14
58 enabled_modules_015:
58 enabled_modules_015:
59 name: wiki
59 name: wiki
60 project_id: 2
60 project_id: 2
61 id: 15
61 id: 15
62 enabled_modules_016:
63 name: boards
64 project_id: 2
65 id: 16
@@ -1,68 +1,79
1 ---
1 ---
2 messages_001:
2 messages_001:
3 created_on: 2007-05-12 17:15:32 +02:00
3 created_on: 2007-05-12 17:15:32 +02:00
4 updated_on: 2007-05-12 17:15:32 +02:00
4 updated_on: 2007-05-12 17:15:32 +02:00
5 subject: First post
5 subject: First post
6 id: 1
6 id: 1
7 replies_count: 2
7 replies_count: 2
8 last_reply_id: 3
8 last_reply_id: 3
9 content: "This is the very first post\n\
9 content: "This is the very first post\n\
10 in the forum"
10 in the forum"
11 author_id: 1
11 author_id: 1
12 parent_id:
12 parent_id:
13 board_id: 1
13 board_id: 1
14 messages_002:
14 messages_002:
15 created_on: 2007-05-12 17:18:00 +02:00
15 created_on: 2007-05-12 17:18:00 +02:00
16 updated_on: 2007-05-12 17:18:00 +02:00
16 updated_on: 2007-05-12 17:18:00 +02:00
17 subject: First reply
17 subject: First reply
18 id: 2
18 id: 2
19 replies_count: 0
19 replies_count: 0
20 last_reply_id:
20 last_reply_id:
21 content: "Reply to the first post"
21 content: "Reply to the first post"
22 author_id: 1
22 author_id: 1
23 parent_id: 1
23 parent_id: 1
24 board_id: 1
24 board_id: 1
25 messages_003:
25 messages_003:
26 created_on: 2007-05-12 17:18:02 +02:00
26 created_on: 2007-05-12 17:18:02 +02:00
27 updated_on: 2007-05-12 17:18:02 +02:00
27 updated_on: 2007-05-12 17:18:02 +02:00
28 subject: "RE: First post"
28 subject: "RE: First post"
29 id: 3
29 id: 3
30 replies_count: 0
30 replies_count: 0
31 last_reply_id:
31 last_reply_id:
32 content: "An other reply"
32 content: "An other reply"
33 author_id: 2
33 author_id: 2
34 parent_id: 1
34 parent_id: 1
35 board_id: 1
35 board_id: 1
36 messages_004:
36 messages_004:
37 created_on: 2007-08-12 17:15:32 +02:00
37 created_on: 2007-08-12 17:15:32 +02:00
38 updated_on: 2007-08-12 17:15:32 +02:00
38 updated_on: 2007-08-12 17:15:32 +02:00
39 subject: Post 2
39 subject: Post 2
40 id: 4
40 id: 4
41 replies_count: 2
41 replies_count: 2
42 last_reply_id: 6
42 last_reply_id: 6
43 content: "This is an other post"
43 content: "This is an other post"
44 author_id:
44 author_id:
45 parent_id:
45 parent_id:
46 board_id: 1
46 board_id: 1
47 messages_005:
47 messages_005:
48 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
48 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
49 updated_on: <%= 3.days.ago.to_date.to_s(:db) %>
49 updated_on: <%= 3.days.ago.to_date.to_s(:db) %>
50 subject: 'RE: post 2'
50 subject: 'RE: post 2'
51 id: 5
51 id: 5
52 replies_count: 0
52 replies_count: 0
53 last_reply_id:
53 last_reply_id:
54 content: "Reply to the second post"
54 content: "Reply to the second post"
55 author_id: 1
55 author_id: 1
56 parent_id: 4
56 parent_id: 4
57 board_id: 1
57 board_id: 1
58 messages_006:
58 messages_006:
59 created_on: <%= 2.days.ago.to_date.to_s(:db) %>
59 created_on: <%= 2.days.ago.to_date.to_s(:db) %>
60 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
60 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
61 subject: 'RE: post 2'
61 subject: 'RE: post 2'
62 id: 6
62 id: 6
63 replies_count: 0
63 replies_count: 0
64 last_reply_id:
64 last_reply_id:
65 content: "Another reply to the second post"
65 content: "Another reply to the second post"
66 author_id: 3
66 author_id: 3
67 parent_id: 4
67 parent_id: 4
68 board_id: 1
68 board_id: 1
69 messages_007:
70 created_on: <%= 2.days.ago.to_date.to_s(:db) %>
71 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
72 subject: 'Message on a private project'
73 id: 7
74 replies_count: 0
75 last_reply_id:
76 content: "This is a private message"
77 author_id: 1
78 parent_id:
79 board_id: 3
@@ -1,71 +1,137
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class MemberTest < ActiveSupport::TestCase
20 class MemberTest < ActiveSupport::TestCase
21 fixtures :users, :projects, :roles, :members, :member_roles
21 fixtures :all
22
22
23 def setup
23 def setup
24 @jsmith = Member.find(1)
24 @jsmith = Member.find(1)
25 end
25 end
26
26
27 def test_create
27 def test_create
28 member = Member.new(:project_id => 1, :user_id => 4, :role_ids => [1, 2])
28 member = Member.new(:project_id => 1, :user_id => 4, :role_ids => [1, 2])
29 assert member.save
29 assert member.save
30 member.reload
30 member.reload
31
31
32 assert_equal 2, member.roles.size
32 assert_equal 2, member.roles.size
33 assert_equal Role.find(1), member.roles.sort.first
33 assert_equal Role.find(1), member.roles.sort.first
34 end
34 end
35
35
36 def test_update
36 def test_update
37 assert_equal "eCookbook", @jsmith.project.name
37 assert_equal "eCookbook", @jsmith.project.name
38 assert_equal "Manager", @jsmith.roles.first.name
38 assert_equal "Manager", @jsmith.roles.first.name
39 assert_equal "jsmith", @jsmith.user.login
39 assert_equal "jsmith", @jsmith.user.login
40
40
41 @jsmith.mail_notification = !@jsmith.mail_notification
41 @jsmith.mail_notification = !@jsmith.mail_notification
42 assert @jsmith.save
42 assert @jsmith.save
43 end
43 end
44
44
45 def test_update_roles
45 def test_update_roles
46 assert_equal 1, @jsmith.roles.size
46 assert_equal 1, @jsmith.roles.size
47 @jsmith.role_ids = [1, 2]
47 @jsmith.role_ids = [1, 2]
48 assert @jsmith.save
48 assert @jsmith.save
49 assert_equal 2, @jsmith.reload.roles.size
49 assert_equal 2, @jsmith.reload.roles.size
50 end
50 end
51
51
52 def test_validate
52 def test_validate
53 member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [2])
53 member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [2])
54 # same use can't have more than one membership for a project
54 # same use can't have more than one membership for a project
55 assert !member.save
55 assert !member.save
56
56
57 member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [])
57 member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [])
58 # must have one role at least
58 # must have one role at least
59 assert !member.save
59 assert !member.save
60 end
60 end
61
61
62 def test_destroy
62 def test_destroy
63 assert_difference 'Member.count', -1 do
63 assert_difference 'Member.count', -1 do
64 assert_difference 'MemberRole.count', -1 do
64 assert_difference 'MemberRole.count', -1 do
65 @jsmith.destroy
65 @jsmith.destroy
66 end
66 end
67 end
67 end
68
68
69 assert_raise(ActiveRecord::RecordNotFound) { Member.find(@jsmith.id) }
69 assert_raise(ActiveRecord::RecordNotFound) { Member.find(@jsmith.id) }
70 end
70 end
71
72 context "removing permissions" do
73 setup do
74 Watcher.delete_all("user_id = 9")
75 user = User.find(9)
76 # public
77 Watcher.create!(:watchable => Issue.find(1), :user_id => user)
78 # private
79 Watcher.create!(:watchable => Issue.find(4), :user => user)
80 Watcher.create!(:watchable => Message.find(7), :user => user)
81 Watcher.create!(:watchable => Wiki.find(2), :user => user)
82 Watcher.create!(:watchable => WikiPage.find(3), :user => user)
83 end
84
85 context "of user" do
86 setup do
87 @member = Member.create!(:project => Project.find(2), :principal => User.find(9), :role_ids => [1, 2])
88 end
89
90 context "by deleting membership" do
91 should "prune watchers" do
92 assert_difference 'Watcher.count', -4 do
93 @member.destroy
94 end
95 end
96 end
97
98 context "by updating roles" do
99 should "prune watchers" do
100 Role.find(2).remove_permission! :view_wiki_pages
101 member = Member.first(:order => 'id desc')
102 assert_difference 'Watcher.count', -2 do
103 member.role_ids = [2]
104 member.save
105 end
106 assert !Message.find(7).watched_by?(@user)
107 end
108 end
109 end
110
111 context "of group" do
112 setup do
113 group = Group.find(10)
114 @member = Member.create!(:project => Project.find(2), :principal => group, :role_ids => [1, 2])
115 group.users << User.find(9)
116 end
117
118 context "by deleting membership" do
119 should "prune watchers" do
120 assert_difference 'Watcher.count', -4 do
121 @member.destroy
122 end
123 end
124 end
125
126 context "by updating roles" do
127 should "prune watchers" do
128 Role.find(2).remove_permission! :view_wiki_pages
129 assert_difference 'Watcher.count', -2 do
130 @member.role_ids = [2]
131 @member.save
132 end
133 end
134 end
135 end
136 end
71 end
137 end
@@ -1,69 +1,105
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class WatcherTest < ActiveSupport::TestCase
20 class WatcherTest < ActiveSupport::TestCase
21 fixtures :issues, :users
21 fixtures :projects, :users, :members, :member_roles, :roles, :enabled_modules,
22 :issues,
23 :boards, :messages,
24 :wikis, :wiki_pages,
25 :watchers
22
26
23 def setup
27 def setup
24 @user = User.find(1)
28 @user = User.find(1)
25 @issue = Issue.find(1)
29 @issue = Issue.find(1)
26 end
30 end
27
31
28 def test_watch
32 def test_watch
29 assert @issue.add_watcher(@user)
33 assert @issue.add_watcher(@user)
30 @issue.reload
34 @issue.reload
31 assert @issue.watchers.detect { |w| w.user == @user }
35 assert @issue.watchers.detect { |w| w.user == @user }
32 end
36 end
33
37
34 def test_cant_watch_twice
38 def test_cant_watch_twice
35 assert @issue.add_watcher(@user)
39 assert @issue.add_watcher(@user)
36 assert !@issue.add_watcher(@user)
40 assert !@issue.add_watcher(@user)
37 end
41 end
38
42
39 def test_watched_by
43 def test_watched_by
40 assert @issue.add_watcher(@user)
44 assert @issue.add_watcher(@user)
41 @issue.reload
45 @issue.reload
42 assert @issue.watched_by?(@user)
46 assert @issue.watched_by?(@user)
43 assert Issue.watched_by(@user).include?(@issue)
47 assert Issue.watched_by(@user).include?(@issue)
44 end
48 end
45
49
46 def test_recipients
50 def test_recipients
47 @issue.watchers.delete_all
51 @issue.watchers.delete_all
48 @issue.reload
52 @issue.reload
49
53
50 assert @issue.watcher_recipients.empty?
54 assert @issue.watcher_recipients.empty?
51 assert @issue.add_watcher(@user)
55 assert @issue.add_watcher(@user)
52
56
53 @user.mail_notification = true
57 @user.mail_notification = true
54 @user.save
58 @user.save
55 @issue.reload
59 @issue.reload
56 assert @issue.watcher_recipients.include?(@user.mail)
60 assert @issue.watcher_recipients.include?(@user.mail)
57
61
58 @user.mail_notification = false
62 @user.mail_notification = false
59 @user.save
63 @user.save
60 @issue.reload
64 @issue.reload
61 assert @issue.watcher_recipients.include?(@user.mail)
65 assert @issue.watcher_recipients.include?(@user.mail)
62 end
66 end
63
67
64 def test_unwatch
68 def test_unwatch
65 assert @issue.add_watcher(@user)
69 assert @issue.add_watcher(@user)
66 @issue.reload
70 @issue.reload
67 assert_equal 1, @issue.remove_watcher(@user)
71 assert_equal 1, @issue.remove_watcher(@user)
68 end
72 end
73
74 def test_prune
75 Watcher.delete_all("user_id = 9")
76 user = User.find(9)
77
78 # public
79 Watcher.create!(:watchable => Issue.find(1), :user => user)
80 Watcher.create!(:watchable => Issue.find(2), :user => user)
81 Watcher.create!(:watchable => Message.find(1), :user => user)
82 Watcher.create!(:watchable => Wiki.find(1), :user => user)
83 Watcher.create!(:watchable => WikiPage.find(2), :user => user)
84
85 # private project (id: 2)
86 Member.create!(:project => Project.find(2), :principal => user, :role_ids => [1])
87 Watcher.create!(:watchable => Issue.find(4), :user => user)
88 Watcher.create!(:watchable => Message.find(7), :user => user)
89 Watcher.create!(:watchable => Wiki.find(2), :user => user)
90 Watcher.create!(:watchable => WikiPage.find(3), :user => user)
91
92 assert_no_difference 'Watcher.count' do
93 Watcher.prune(:user => User.find(9))
94 end
95
96 Member.delete_all
97
98 assert_difference 'Watcher.count', -4 do
99 Watcher.prune(:user => User.find(9))
100 end
101
102 assert Issue.find(1).watched_by?(user)
103 assert !Issue.find(4).watched_by?(user)
104 end
69 end
105 end
General Comments 0
You need to be logged in to leave comments. Login now