##// END OF EJS Templates
Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled....
Jean-Philippe Lang -
r1905:394fc9c10945
parent child
Show More
@@ -0,0 +1,49
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../../../test_helper'
19
20 class Redmine::AccessControlTest < Test::Unit::TestCase
21
22 def setup
23 @access_module = Redmine::AccessControl
24 end
25
26 def test_permissions
27 perms = @access_module.permissions
28 assert perms.is_a?(Array)
29 assert perms.first.is_a?(Redmine::AccessControl::Permission)
30 end
31
32 def test_module_permission
33 perm = @access_module.permission(:view_issues)
34 assert perm.is_a?(Redmine::AccessControl::Permission)
35 assert_equal :view_issues, perm.name
36 assert_equal :issue_tracking, perm.project_module
37 assert perm.actions.is_a?(Array)
38 assert perm.actions.include?('issues/index')
39 end
40
41 def test_no_module_permission
42 perm = @access_module.permission(:edit_project)
43 assert perm.is_a?(Redmine::AccessControl::Permission)
44 assert_equal :edit_project, perm.name
45 assert_nil perm.project_module
46 assert perm.actions.is_a?(Array)
47 assert perm.actions.include?('projects/settings')
48 end
49 end
@@ -1,268 +1,274
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 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 has_many :users, :through => :members
24 has_many :users, :through => :members
25 has_many :enabled_modules, :dependent => :delete_all
25 has_many :enabled_modules, :dependent => :delete_all
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
28 has_many :issue_changes, :through => :issues, :source => :journals
28 has_many :issue_changes, :through => :issues, :source => :journals
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
30 has_many :time_entries, :dependent => :delete_all
30 has_many :time_entries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
32 has_many :documents, :dependent => :destroy
32 has_many :documents, :dependent => :destroy
33 has_many :news, :dependent => :delete_all, :include => :author
33 has_many :news, :dependent => :delete_all, :include => :author
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
36 has_one :repository, :dependent => :destroy
36 has_one :repository, :dependent => :destroy
37 has_many :changesets, :through => :repository
37 has_many :changesets, :through => :repository
38 has_one :wiki, :dependent => :destroy
38 has_one :wiki, :dependent => :destroy
39 # Custom field for the project issues
39 # Custom field for the project issues
40 has_and_belongs_to_many :issue_custom_fields,
40 has_and_belongs_to_many :issue_custom_fields,
41 :class_name => 'IssueCustomField',
41 :class_name => 'IssueCustomField',
42 :order => "#{CustomField.table_name}.position",
42 :order => "#{CustomField.table_name}.position",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 :association_foreign_key => 'custom_field_id'
44 :association_foreign_key => 'custom_field_id'
45
45
46 acts_as_tree :order => "name", :counter_cache => true
46 acts_as_tree :order => "name", :counter_cache => true
47
47
48 acts_as_customizable
48 acts_as_customizable
49 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
49 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
50 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
50 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
51 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
51 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
52 :author => nil
52 :author => nil
53
53
54 attr_protected :status, :enabled_module_names
54 attr_protected :status, :enabled_module_names
55
55
56 validates_presence_of :name, :identifier
56 validates_presence_of :name, :identifier
57 validates_uniqueness_of :name, :identifier
57 validates_uniqueness_of :name, :identifier
58 validates_associated :repository, :wiki
58 validates_associated :repository, :wiki
59 validates_length_of :name, :maximum => 30
59 validates_length_of :name, :maximum => 30
60 validates_length_of :homepage, :maximum => 255
60 validates_length_of :homepage, :maximum => 255
61 validates_length_of :identifier, :in => 3..20
61 validates_length_of :identifier, :in => 3..20
62 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
62 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
63
63
64 before_destroy :delete_all_members
64 before_destroy :delete_all_members
65
65
66 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
66 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
67
67
68 def identifier=(identifier)
68 def identifier=(identifier)
69 super unless identifier_frozen?
69 super unless identifier_frozen?
70 end
70 end
71
71
72 def identifier_frozen?
72 def identifier_frozen?
73 errors[:identifier].nil? && !(new_record? || identifier.blank?)
73 errors[:identifier].nil? && !(new_record? || identifier.blank?)
74 end
74 end
75
75
76 def issues_with_subprojects(include_subprojects=false)
76 def issues_with_subprojects(include_subprojects=false)
77 conditions = nil
77 conditions = nil
78 if include_subprojects
78 if include_subprojects
79 ids = [id] + child_ids
79 ids = [id] + child_ids
80 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
80 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
81 end
81 end
82 conditions ||= ["#{Project.table_name}.id = ?", id]
82 conditions ||= ["#{Project.table_name}.id = ?", id]
83 # Quick and dirty fix for Rails 2 compatibility
83 # Quick and dirty fix for Rails 2 compatibility
84 Issue.send(:with_scope, :find => { :conditions => conditions }) do
84 Issue.send(:with_scope, :find => { :conditions => conditions }) do
85 Version.send(:with_scope, :find => { :conditions => conditions }) do
85 Version.send(:with_scope, :find => { :conditions => conditions }) do
86 yield
86 yield
87 end
87 end
88 end
88 end
89 end
89 end
90
90
91 # returns latest created projects
91 # returns latest created projects
92 # non public projects will be returned only if user is a member of those
92 # non public projects will be returned only if user is a member of those
93 def self.latest(user=nil, count=5)
93 def self.latest(user=nil, count=5)
94 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
94 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
95 end
95 end
96
96
97 def self.visible_by(user=nil)
97 def self.visible_by(user=nil)
98 user ||= User.current
98 user ||= User.current
99 if user && user.admin?
99 if user && user.admin?
100 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
100 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
101 elsif user && user.memberships.any?
101 elsif user && user.memberships.any?
102 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
102 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
103 else
103 else
104 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
104 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
105 end
105 end
106 end
106 end
107
107
108 def self.allowed_to_condition(user, permission, options={})
108 def self.allowed_to_condition(user, permission, options={})
109 statements = []
109 statements = []
110 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
110 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
111 if perm = Redmine::AccessControl.permission(permission)
112 unless perm.project_module.nil?
113 # If the permission belongs to a project module, make sure the module is enabled
114 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
115 end
116 end
111 if options[:project]
117 if options[:project]
112 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
118 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
113 project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
119 project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
114 base_statement = "(#{project_statement}) AND (#{base_statement})"
120 base_statement = "(#{project_statement}) AND (#{base_statement})"
115 end
121 end
116 if user.admin?
122 if user.admin?
117 # no restriction
123 # no restriction
118 else
124 else
119 statements << "1=0"
125 statements << "1=0"
120 if user.logged?
126 if user.logged?
121 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
127 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
122 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
128 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
123 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
129 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
124 elsif Role.anonymous.allowed_to?(permission)
130 elsif Role.anonymous.allowed_to?(permission)
125 # anonymous user allowed on public project
131 # anonymous user allowed on public project
126 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
132 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
127 else
133 else
128 # anonymous user is not authorized
134 # anonymous user is not authorized
129 end
135 end
130 end
136 end
131 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
137 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
132 end
138 end
133
139
134 def project_condition(with_subprojects)
140 def project_condition(with_subprojects)
135 cond = "#{Project.table_name}.id = #{id}"
141 cond = "#{Project.table_name}.id = #{id}"
136 cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
142 cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
137 cond
143 cond
138 end
144 end
139
145
140 def self.find(*args)
146 def self.find(*args)
141 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
147 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
142 project = find_by_identifier(*args)
148 project = find_by_identifier(*args)
143 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
149 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
144 project
150 project
145 else
151 else
146 super
152 super
147 end
153 end
148 end
154 end
149
155
150 def to_param
156 def to_param
151 # id is used for projects with a numeric identifier (compatibility)
157 # id is used for projects with a numeric identifier (compatibility)
152 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
158 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
153 end
159 end
154
160
155 def active?
161 def active?
156 self.status == STATUS_ACTIVE
162 self.status == STATUS_ACTIVE
157 end
163 end
158
164
159 def archive
165 def archive
160 # Archive subprojects if any
166 # Archive subprojects if any
161 children.each do |subproject|
167 children.each do |subproject|
162 subproject.archive
168 subproject.archive
163 end
169 end
164 update_attribute :status, STATUS_ARCHIVED
170 update_attribute :status, STATUS_ARCHIVED
165 end
171 end
166
172
167 def unarchive
173 def unarchive
168 return false if parent && !parent.active?
174 return false if parent && !parent.active?
169 update_attribute :status, STATUS_ACTIVE
175 update_attribute :status, STATUS_ACTIVE
170 end
176 end
171
177
172 def active_children
178 def active_children
173 children.select {|child| child.active?}
179 children.select {|child| child.active?}
174 end
180 end
175
181
176 # Returns an array of the trackers used by the project and its sub projects
182 # Returns an array of the trackers used by the project and its sub projects
177 def rolled_up_trackers
183 def rolled_up_trackers
178 @rolled_up_trackers ||=
184 @rolled_up_trackers ||=
179 Tracker.find(:all, :include => :projects,
185 Tracker.find(:all, :include => :projects,
180 :select => "DISTINCT #{Tracker.table_name}.*",
186 :select => "DISTINCT #{Tracker.table_name}.*",
181 :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
187 :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
182 :order => "#{Tracker.table_name}.position")
188 :order => "#{Tracker.table_name}.position")
183 end
189 end
184
190
185 # Deletes all project's members
191 # Deletes all project's members
186 def delete_all_members
192 def delete_all_members
187 Member.delete_all(['project_id = ?', id])
193 Member.delete_all(['project_id = ?', id])
188 end
194 end
189
195
190 # Users issues can be assigned to
196 # Users issues can be assigned to
191 def assignable_users
197 def assignable_users
192 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
198 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
193 end
199 end
194
200
195 # Returns the mail adresses of users that should be always notified on project events
201 # Returns the mail adresses of users that should be always notified on project events
196 def recipients
202 def recipients
197 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
203 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
198 end
204 end
199
205
200 # Returns an array of all custom fields enabled for project issues
206 # Returns an array of all custom fields enabled for project issues
201 # (explictly associated custom fields and custom fields enabled for all projects)
207 # (explictly associated custom fields and custom fields enabled for all projects)
202 def all_issue_custom_fields
208 def all_issue_custom_fields
203 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
209 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
204 end
210 end
205
211
206 def project
212 def project
207 self
213 self
208 end
214 end
209
215
210 def <=>(project)
216 def <=>(project)
211 name.downcase <=> project.name.downcase
217 name.downcase <=> project.name.downcase
212 end
218 end
213
219
214 def to_s
220 def to_s
215 name
221 name
216 end
222 end
217
223
218 # Returns a short description of the projects (first lines)
224 # Returns a short description of the projects (first lines)
219 def short_description(length = 255)
225 def short_description(length = 255)
220 description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
226 description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
221 end
227 end
222
228
223 def allows_to?(action)
229 def allows_to?(action)
224 if action.is_a? Hash
230 if action.is_a? Hash
225 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
231 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
226 else
232 else
227 allowed_permissions.include? action
233 allowed_permissions.include? action
228 end
234 end
229 end
235 end
230
236
231 def module_enabled?(module_name)
237 def module_enabled?(module_name)
232 module_name = module_name.to_s
238 module_name = module_name.to_s
233 enabled_modules.detect {|m| m.name == module_name}
239 enabled_modules.detect {|m| m.name == module_name}
234 end
240 end
235
241
236 def enabled_module_names=(module_names)
242 def enabled_module_names=(module_names)
237 enabled_modules.clear
243 enabled_modules.clear
238 module_names = [] unless module_names && module_names.is_a?(Array)
244 module_names = [] unless module_names && module_names.is_a?(Array)
239 module_names.each do |name|
245 module_names.each do |name|
240 enabled_modules << EnabledModule.new(:name => name.to_s)
246 enabled_modules << EnabledModule.new(:name => name.to_s)
241 end
247 end
242 end
248 end
243
249
244 # Returns an auto-generated project identifier based on the last identifier used
250 # Returns an auto-generated project identifier based on the last identifier used
245 def self.next_identifier
251 def self.next_identifier
246 p = Project.find(:first, :order => 'created_on DESC')
252 p = Project.find(:first, :order => 'created_on DESC')
247 p.nil? ? nil : p.identifier.to_s.succ
253 p.nil? ? nil : p.identifier.to_s.succ
248 end
254 end
249
255
250 protected
256 protected
251 def validate
257 def validate
252 errors.add(parent_id, " must be a root project") if parent and parent.parent
258 errors.add(parent_id, " must be a root project") if parent and parent.parent
253 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
259 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
254 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
260 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
255 end
261 end
256
262
257 private
263 private
258 def allowed_permissions
264 def allowed_permissions
259 @allowed_permissions ||= begin
265 @allowed_permissions ||= begin
260 module_names = enabled_modules.collect {|m| m.name}
266 module_names = enabled_modules.collect {|m| m.name}
261 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
267 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
262 end
268 end
263 end
269 end
264
270
265 def allowed_actions
271 def allowed_actions
266 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
272 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
267 end
273 end
268 end
274 end
@@ -1,385 +1,385
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :default_order
19 attr_accessor :name, :sortable, :default_order
20 include GLoc
20 include GLoc
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.default_order = options[:default_order]
25 self.default_order = options[:default_order]
26 end
26 end
27
27
28 def caption
28 def caption
29 set_language_if_valid(User.current.language)
29 set_language_if_valid(User.current.language)
30 l("field_#{name}")
30 l("field_#{name}")
31 end
31 end
32 end
32 end
33
33
34 class QueryCustomFieldColumn < QueryColumn
34 class QueryCustomFieldColumn < QueryColumn
35
35
36 def initialize(custom_field)
36 def initialize(custom_field)
37 self.name = "cf_#{custom_field.id}".to_sym
37 self.name = "cf_#{custom_field.id}".to_sym
38 self.sortable = false
38 self.sortable = false
39 @cf = custom_field
39 @cf = custom_field
40 end
40 end
41
41
42 def caption
42 def caption
43 @cf.name
43 @cf.name
44 end
44 end
45
45
46 def custom_field
46 def custom_field
47 @cf
47 @cf
48 end
48 end
49 end
49 end
50
50
51 class Query < ActiveRecord::Base
51 class Query < ActiveRecord::Base
52 belongs_to :project
52 belongs_to :project
53 belongs_to :user
53 belongs_to :user
54 serialize :filters
54 serialize :filters
55 serialize :column_names
55 serialize :column_names
56
56
57 attr_protected :project_id, :user_id
57 attr_protected :project_id, :user_id
58
58
59 validates_presence_of :name, :on => :save
59 validates_presence_of :name, :on => :save
60 validates_length_of :name, :maximum => 255
60 validates_length_of :name, :maximum => 255
61
61
62 @@operators = { "=" => :label_equals,
62 @@operators = { "=" => :label_equals,
63 "!" => :label_not_equals,
63 "!" => :label_not_equals,
64 "o" => :label_open_issues,
64 "o" => :label_open_issues,
65 "c" => :label_closed_issues,
65 "c" => :label_closed_issues,
66 "!*" => :label_none,
66 "!*" => :label_none,
67 "*" => :label_all,
67 "*" => :label_all,
68 ">=" => '>=',
68 ">=" => '>=',
69 "<=" => '<=',
69 "<=" => '<=',
70 "<t+" => :label_in_less_than,
70 "<t+" => :label_in_less_than,
71 ">t+" => :label_in_more_than,
71 ">t+" => :label_in_more_than,
72 "t+" => :label_in,
72 "t+" => :label_in,
73 "t" => :label_today,
73 "t" => :label_today,
74 "w" => :label_this_week,
74 "w" => :label_this_week,
75 ">t-" => :label_less_than_ago,
75 ">t-" => :label_less_than_ago,
76 "<t-" => :label_more_than_ago,
76 "<t-" => :label_more_than_ago,
77 "t-" => :label_ago,
77 "t-" => :label_ago,
78 "~" => :label_contains,
78 "~" => :label_contains,
79 "!~" => :label_not_contains }
79 "!~" => :label_not_contains }
80
80
81 cattr_reader :operators
81 cattr_reader :operators
82
82
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
86 :list_subprojects => [ "*", "!*", "=" ],
86 :list_subprojects => [ "*", "!*", "=" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
89 :string => [ "=", "~", "!", "!~" ],
89 :string => [ "=", "~", "!", "!~" ],
90 :text => [ "~", "!~" ],
90 :text => [ "~", "!~" ],
91 :integer => [ "=", ">=", "<=", "!*", "*" ] }
91 :integer => [ "=", ">=", "<=", "!*", "*" ] }
92
92
93 cattr_reader :operators_by_filter_type
93 cattr_reader :operators_by_filter_type
94
94
95 @@available_columns = [
95 @@available_columns = [
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
100 QueryColumn.new(:author),
100 QueryColumn.new(:author),
101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
110 ]
110 ]
111 cattr_reader :available_columns
111 cattr_reader :available_columns
112
112
113 def initialize(attributes = nil)
113 def initialize(attributes = nil)
114 super attributes
114 super attributes
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
116 set_language_if_valid(User.current.language)
116 set_language_if_valid(User.current.language)
117 end
117 end
118
118
119 def after_initialize
119 def after_initialize
120 # Store the fact that project is nil (used in #editable_by?)
120 # Store the fact that project is nil (used in #editable_by?)
121 @is_for_all = project.nil?
121 @is_for_all = project.nil?
122 end
122 end
123
123
124 def validate
124 def validate
125 filters.each_key do |field|
125 filters.each_key do |field|
126 errors.add label_for(field), :activerecord_error_blank unless
126 errors.add label_for(field), :activerecord_error_blank unless
127 # filter requires one or more values
127 # filter requires one or more values
128 (values_for(field) and !values_for(field).first.blank?) or
128 (values_for(field) and !values_for(field).first.blank?) or
129 # filter doesn't require any value
129 # filter doesn't require any value
130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
131 end if filters
131 end if filters
132 end
132 end
133
133
134 def editable_by?(user)
134 def editable_by?(user)
135 return false unless user
135 return false unless user
136 # Admin can edit them all and regular users can edit their private queries
136 # Admin can edit them all and regular users can edit their private queries
137 return true if user.admin? || (!is_public && self.user_id == user.id)
137 return true if user.admin? || (!is_public && self.user_id == user.id)
138 # Members can not edit public queries that are for all project (only admin is allowed to)
138 # Members can not edit public queries that are for all project (only admin is allowed to)
139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
140 end
140 end
141
141
142 def available_filters
142 def available_filters
143 return @available_filters if @available_filters
143 return @available_filters if @available_filters
144
144
145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
146
146
147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
150 "subject" => { :type => :text, :order => 8 },
150 "subject" => { :type => :text, :order => 8 },
151 "created_on" => { :type => :date_past, :order => 9 },
151 "created_on" => { :type => :date_past, :order => 9 },
152 "updated_on" => { :type => :date_past, :order => 10 },
152 "updated_on" => { :type => :date_past, :order => 10 },
153 "start_date" => { :type => :date, :order => 11 },
153 "start_date" => { :type => :date, :order => 11 },
154 "due_date" => { :type => :date, :order => 12 },
154 "due_date" => { :type => :date, :order => 12 },
155 "estimated_hours" => { :type => :integer, :order => 13 },
155 "estimated_hours" => { :type => :integer, :order => 13 },
156 "done_ratio" => { :type => :integer, :order => 14 }}
156 "done_ratio" => { :type => :integer, :order => 14 }}
157
157
158 user_values = []
158 user_values = []
159 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
159 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
160 if project
160 if project
161 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
161 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
162 else
162 else
163 # members of the user's projects
163 # members of the user's projects
164 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
164 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
165 end
165 end
166 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
166 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
167 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
167 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
168
168
169 if project
169 if project
170 # project specific filters
170 # project specific filters
171 unless @project.issue_categories.empty?
171 unless @project.issue_categories.empty?
172 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
172 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
173 end
173 end
174 unless @project.versions.empty?
174 unless @project.versions.empty?
175 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
175 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
176 end
176 end
177 unless @project.active_children.empty?
177 unless @project.active_children.empty?
178 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
178 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
179 end
179 end
180 add_custom_fields_filters(@project.all_issue_custom_fields)
180 add_custom_fields_filters(@project.all_issue_custom_fields)
181 else
181 else
182 # global filters for cross project issue list
182 # global filters for cross project issue list
183 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
183 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
184 end
184 end
185 @available_filters
185 @available_filters
186 end
186 end
187
187
188 def add_filter(field, operator, values)
188 def add_filter(field, operator, values)
189 # values must be an array
189 # values must be an array
190 return unless values and values.is_a? Array # and !values.first.empty?
190 return unless values and values.is_a? Array # and !values.first.empty?
191 # check if field is defined as an available filter
191 # check if field is defined as an available filter
192 if available_filters.has_key? field
192 if available_filters.has_key? field
193 filter_options = available_filters[field]
193 filter_options = available_filters[field]
194 # check if operator is allowed for that filter
194 # check if operator is allowed for that filter
195 #if @@operators_by_filter_type[filter_options[:type]].include? operator
195 #if @@operators_by_filter_type[filter_options[:type]].include? operator
196 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
196 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
197 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
197 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
198 #end
198 #end
199 filters[field] = {:operator => operator, :values => values }
199 filters[field] = {:operator => operator, :values => values }
200 end
200 end
201 end
201 end
202
202
203 def add_short_filter(field, expression)
203 def add_short_filter(field, expression)
204 return unless expression
204 return unless expression
205 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
205 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
206 add_filter field, (parms[0] || "="), [parms[1] || ""]
206 add_filter field, (parms[0] || "="), [parms[1] || ""]
207 end
207 end
208
208
209 def has_filter?(field)
209 def has_filter?(field)
210 filters and filters[field]
210 filters and filters[field]
211 end
211 end
212
212
213 def operator_for(field)
213 def operator_for(field)
214 has_filter?(field) ? filters[field][:operator] : nil
214 has_filter?(field) ? filters[field][:operator] : nil
215 end
215 end
216
216
217 def values_for(field)
217 def values_for(field)
218 has_filter?(field) ? filters[field][:values] : nil
218 has_filter?(field) ? filters[field][:values] : nil
219 end
219 end
220
220
221 def label_for(field)
221 def label_for(field)
222 label = available_filters[field][:name] if available_filters.has_key?(field)
222 label = available_filters[field][:name] if available_filters.has_key?(field)
223 label ||= field.gsub(/\_id$/, "")
223 label ||= field.gsub(/\_id$/, "")
224 end
224 end
225
225
226 def available_columns
226 def available_columns
227 return @available_columns if @available_columns
227 return @available_columns if @available_columns
228 @available_columns = Query.available_columns
228 @available_columns = Query.available_columns
229 @available_columns += (project ?
229 @available_columns += (project ?
230 project.all_issue_custom_fields :
230 project.all_issue_custom_fields :
231 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
231 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
232 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
232 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
233 end
233 end
234
234
235 def columns
235 def columns
236 if has_default_columns?
236 if has_default_columns?
237 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
237 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
238 else
238 else
239 # preserve the column_names order
239 # preserve the column_names order
240 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
240 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
241 end
241 end
242 end
242 end
243
243
244 def column_names=(names)
244 def column_names=(names)
245 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
245 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
246 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
246 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
247 write_attribute(:column_names, names)
247 write_attribute(:column_names, names)
248 end
248 end
249
249
250 def has_column?(column)
250 def has_column?(column)
251 column_names && column_names.include?(column.name)
251 column_names && column_names.include?(column.name)
252 end
252 end
253
253
254 def has_default_columns?
254 def has_default_columns?
255 column_names.nil? || column_names.empty?
255 column_names.nil? || column_names.empty?
256 end
256 end
257
257
258 def project_statement
258 def project_statement
259 project_clauses = []
259 project_clauses = []
260 if project && !@project.active_children.empty?
260 if project && !@project.active_children.empty?
261 ids = [project.id]
261 ids = [project.id]
262 if has_filter?("subproject_id")
262 if has_filter?("subproject_id")
263 case operator_for("subproject_id")
263 case operator_for("subproject_id")
264 when '='
264 when '='
265 # include the selected subprojects
265 # include the selected subprojects
266 ids += values_for("subproject_id").each(&:to_i)
266 ids += values_for("subproject_id").each(&:to_i)
267 when '!*'
267 when '!*'
268 # main project only
268 # main project only
269 else
269 else
270 # all subprojects
270 # all subprojects
271 ids += project.child_ids
271 ids += project.child_ids
272 end
272 end
273 elsif Setting.display_subprojects_issues?
273 elsif Setting.display_subprojects_issues?
274 ids += project.child_ids
274 ids += project.child_ids
275 end
275 end
276 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
276 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
277 elsif project
277 elsif project
278 project_clauses << "#{Project.table_name}.id = %d" % project.id
278 project_clauses << "#{Project.table_name}.id = %d" % project.id
279 end
279 end
280 project_clauses << Project.visible_by(User.current)
280 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
281 project_clauses.join(' AND ')
281 project_clauses.join(' AND ')
282 end
282 end
283
283
284 def statement
284 def statement
285 # filters clauses
285 # filters clauses
286 filters_clauses = []
286 filters_clauses = []
287 filters.each_key do |field|
287 filters.each_key do |field|
288 next if field == "subproject_id"
288 next if field == "subproject_id"
289 v = values_for(field).clone
289 v = values_for(field).clone
290 next unless v and !v.empty?
290 next unless v and !v.empty?
291
291
292 sql = ''
292 sql = ''
293 is_custom_filter = false
293 is_custom_filter = false
294 if field =~ /^cf_(\d+)$/
294 if field =~ /^cf_(\d+)$/
295 # custom field
295 # custom field
296 db_table = CustomValue.table_name
296 db_table = CustomValue.table_name
297 db_field = 'value'
297 db_field = 'value'
298 is_custom_filter = true
298 is_custom_filter = true
299 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
299 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
300 else
300 else
301 # regular field
301 # regular field
302 db_table = Issue.table_name
302 db_table = Issue.table_name
303 db_field = field
303 db_field = field
304 sql << '('
304 sql << '('
305 end
305 end
306
306
307 # "me" value subsitution
307 # "me" value subsitution
308 if %w(assigned_to_id author_id).include?(field)
308 if %w(assigned_to_id author_id).include?(field)
309 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
309 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
310 end
310 end
311
311
312 case operator_for field
312 case operator_for field
313 when "="
313 when "="
314 sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
314 sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
315 when "!"
315 when "!"
316 sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
316 sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
317 when "!*"
317 when "!*"
318 sql = sql + "#{db_table}.#{db_field} IS NULL"
318 sql = sql + "#{db_table}.#{db_field} IS NULL"
319 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
319 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
320 when "*"
320 when "*"
321 sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
321 sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
322 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
322 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
323 when ">="
323 when ">="
324 sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
324 sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
325 when "<="
325 when "<="
326 sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
326 sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
327 when "o"
327 when "o"
328 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
328 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
329 when "c"
329 when "c"
330 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
330 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
331 when ">t-"
331 when ">t-"
332 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
332 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
333 when "<t-"
333 when "<t-"
334 sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
334 sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
335 when "t-"
335 when "t-"
336 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
336 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
337 when ">t+"
337 when ">t+"
338 sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
338 sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
339 when "<t+"
339 when "<t+"
340 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
340 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
341 when "t+"
341 when "t+"
342 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
342 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
343 when "t"
343 when "t"
344 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
344 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
345 when "w"
345 when "w"
346 from = l(:general_first_day_of_week) == '7' ?
346 from = l(:general_first_day_of_week) == '7' ?
347 # week starts on sunday
347 # week starts on sunday
348 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
348 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
349 # week starts on monday (Rails default)
349 # week starts on monday (Rails default)
350 Time.now.at_beginning_of_week
350 Time.now.at_beginning_of_week
351 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
351 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
352 when "~"
352 when "~"
353 sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
353 sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
354 when "!~"
354 when "!~"
355 sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
355 sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
356 end
356 end
357 sql << ')'
357 sql << ')'
358 filters_clauses << sql
358 filters_clauses << sql
359 end if filters and valid?
359 end if filters and valid?
360
360
361 (filters_clauses << project_statement).join(' AND ')
361 (filters_clauses << project_statement).join(' AND ')
362 end
362 end
363
363
364 private
364 private
365
365
366 def add_custom_fields_filters(custom_fields)
366 def add_custom_fields_filters(custom_fields)
367 @available_filters ||= {}
367 @available_filters ||= {}
368
368
369 custom_fields.select(&:is_filter?).each do |field|
369 custom_fields.select(&:is_filter?).each do |field|
370 case field.field_format
370 case field.field_format
371 when "text"
371 when "text"
372 options = { :type => :text, :order => 20 }
372 options = { :type => :text, :order => 20 }
373 when "list"
373 when "list"
374 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
374 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
375 when "date"
375 when "date"
376 options = { :type => :date, :order => 20 }
376 options = { :type => :date, :order => 20 }
377 when "bool"
377 when "bool"
378 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
378 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
379 else
379 else
380 options = { :type => :string, :order => 20 }
380 options = { :type => :string, :order => 20 }
381 end
381 end
382 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
382 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
383 end
383 end
384 end
384 end
385 end
385 end
@@ -1,112 +1,120
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 module Redmine
18 module Redmine
19 module AccessControl
19 module AccessControl
20
20
21 class << self
21 class << self
22 def map
22 def map
23 mapper = Mapper.new
23 mapper = Mapper.new
24 yield mapper
24 yield mapper
25 @permissions ||= []
25 @permissions ||= []
26 @permissions += mapper.mapped_permissions
26 @permissions += mapper.mapped_permissions
27 end
27 end
28
28
29 def permissions
29 def permissions
30 @permissions
30 @permissions
31 end
31 end
32
32
33 # Returns the permission of given name or nil if it wasn't found
34 # Argument should be a symbol
35 def permission(name)
36 permissions.detect {|p| p.name == name}
37 end
38
39 # Returns the actions that are allowed by the permission of given name
33 def allowed_actions(permission_name)
40 def allowed_actions(permission_name)
34 perm = @permissions.detect {|p| p.name == permission_name}
41 perm = permission(permission_name)
35 perm ? perm.actions : []
42 perm ? perm.actions : []
36 end
43 end
37
44
38 def public_permissions
45 def public_permissions
39 @public_permissions ||= @permissions.select {|p| p.public?}
46 @public_permissions ||= @permissions.select {|p| p.public?}
40 end
47 end
41
48
42 def members_only_permissions
49 def members_only_permissions
43 @members_only_permissions ||= @permissions.select {|p| p.require_member?}
50 @members_only_permissions ||= @permissions.select {|p| p.require_member?}
44 end
51 end
45
52
46 def loggedin_only_permissions
53 def loggedin_only_permissions
47 @loggedin_only_permissions ||= @permissions.select {|p| p.require_loggedin?}
54 @loggedin_only_permissions ||= @permissions.select {|p| p.require_loggedin?}
48 end
55 end
49
56
50 def available_project_modules
57 def available_project_modules
51 @available_project_modules ||= @permissions.collect(&:project_module).uniq.compact
58 @available_project_modules ||= @permissions.collect(&:project_module).uniq.compact
52 end
59 end
53
60
54 def modules_permissions(modules)
61 def modules_permissions(modules)
55 @permissions.select {|p| p.project_module.nil? || modules.include?(p.project_module.to_s)}
62 @permissions.select {|p| p.project_module.nil? || modules.include?(p.project_module.to_s)}
56 end
63 end
57 end
64 end
58
65
59 class Mapper
66 class Mapper
60 def initialize
67 def initialize
61 @project_module = nil
68 @project_module = nil
62 end
69 end
63
70
64 def permission(name, hash, options={})
71 def permission(name, hash, options={})
65 @permissions ||= []
72 @permissions ||= []
66 options.merge!(:project_module => @project_module)
73 options.merge!(:project_module => @project_module)
67 @permissions << Permission.new(name, hash, options)
74 @permissions << Permission.new(name, hash, options)
68 end
75 end
69
76
70 def project_module(name, options={})
77 def project_module(name, options={})
71 @project_module = name
78 @project_module = name
72 yield self
79 yield self
73 @project_module = nil
80 @project_module = nil
74 end
81 end
75
82
76 def mapped_permissions
83 def mapped_permissions
77 @permissions
84 @permissions
78 end
85 end
79 end
86 end
80
87
81 class Permission
88 class Permission
82 attr_reader :name, :actions, :project_module
89 attr_reader :name, :actions, :project_module
83
90
84 def initialize(name, hash, options)
91 def initialize(name, hash, options)
85 @name = name
92 @name = name
86 @actions = []
93 @actions = []
87 @public = options[:public] || false
94 @public = options[:public] || false
88 @require = options[:require]
95 @require = options[:require]
89 @project_module = options[:project_module]
96 @project_module = options[:project_module]
90 hash.each do |controller, actions|
97 hash.each do |controller, actions|
91 if actions.is_a? Array
98 if actions.is_a? Array
92 @actions << actions.collect {|action| "#{controller}/#{action}"}
99 @actions << actions.collect {|action| "#{controller}/#{action}"}
93 else
100 else
94 @actions << "#{controller}/#{actions}"
101 @actions << "#{controller}/#{actions}"
95 end
102 end
96 end
103 end
104 @actions.flatten!
97 end
105 end
98
106
99 def public?
107 def public?
100 @public
108 @public
101 end
109 end
102
110
103 def require_member?
111 def require_member?
104 @require && @require == :member
112 @require && @require == :member
105 end
113 end
106
114
107 def require_loggedin?
115 def require_loggedin?
108 @require && (@require == :member || @require == :loggedin)
116 @require && (@require == :member || @require == :loggedin)
109 end
117 end
110 end
118 end
111 end
119 end
112 end
120 end
@@ -1,46 +1,58
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:
47 name: time_tracking
48 project_id: 3
49 id: 12
50 enabled_modules_013:
51 name: issue_tracking
52 project_id: 3
53 id: 13
54 enabled_modules_014:
55 name: issue_tracking
56 project_id: 5
57 id: 14
46 No newline at end of file
58
@@ -1,690 +1,701
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < Test::Unit::TestCase
24 class IssuesControllerTest < Test::Unit::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :issues,
29 :issues,
30 :issue_statuses,
30 :issue_statuses,
31 :versions,
31 :versions,
32 :trackers,
32 :trackers,
33 :projects_trackers,
33 :projects_trackers,
34 :issue_categories,
34 :issue_categories,
35 :enabled_modules,
35 :enabled_modules,
36 :enumerations,
36 :enumerations,
37 :attachments,
37 :attachments,
38 :workflows,
38 :workflows,
39 :custom_fields,
39 :custom_fields,
40 :custom_values,
40 :custom_values,
41 :custom_fields_trackers,
41 :custom_fields_trackers,
42 :time_entries,
42 :time_entries,
43 :journals,
43 :journals,
44 :journal_details
44 :journal_details
45
45
46 def setup
46 def setup
47 @controller = IssuesController.new
47 @controller = IssuesController.new
48 @request = ActionController::TestRequest.new
48 @request = ActionController::TestRequest.new
49 @response = ActionController::TestResponse.new
49 @response = ActionController::TestResponse.new
50 User.current = nil
50 User.current = nil
51 end
51 end
52
52
53 def test_index
53 def test_index
54 get :index
54 get :index
55 assert_response :success
55 assert_response :success
56 assert_template 'index.rhtml'
56 assert_template 'index.rhtml'
57 assert_not_nil assigns(:issues)
57 assert_not_nil assigns(:issues)
58 assert_nil assigns(:project)
58 assert_nil assigns(:project)
59 assert_tag :tag => 'a', :content => /Can't print recipes/
59 assert_tag :tag => 'a', :content => /Can't print recipes/
60 assert_tag :tag => 'a', :content => /Subproject issue/
60 assert_tag :tag => 'a', :content => /Subproject issue/
61 # private projects hidden
61 # private projects hidden
62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
64 end
64 end
65
65
66 def test_index_should_not_list_issues_when_module_disabled
67 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
68 get :index
69 assert_response :success
70 assert_template 'index.rhtml'
71 assert_not_nil assigns(:issues)
72 assert_nil assigns(:project)
73 assert_no_tag :tag => 'a', :content => /Can't print recipes/
74 assert_tag :tag => 'a', :content => /Subproject issue/
75 end
76
66 def test_index_with_project
77 def test_index_with_project
67 Setting.display_subprojects_issues = 0
78 Setting.display_subprojects_issues = 0
68 get :index, :project_id => 1
79 get :index, :project_id => 1
69 assert_response :success
80 assert_response :success
70 assert_template 'index.rhtml'
81 assert_template 'index.rhtml'
71 assert_not_nil assigns(:issues)
82 assert_not_nil assigns(:issues)
72 assert_tag :tag => 'a', :content => /Can't print recipes/
83 assert_tag :tag => 'a', :content => /Can't print recipes/
73 assert_no_tag :tag => 'a', :content => /Subproject issue/
84 assert_no_tag :tag => 'a', :content => /Subproject issue/
74 end
85 end
75
86
76 def test_index_with_project_and_subprojects
87 def test_index_with_project_and_subprojects
77 Setting.display_subprojects_issues = 1
88 Setting.display_subprojects_issues = 1
78 get :index, :project_id => 1
89 get :index, :project_id => 1
79 assert_response :success
90 assert_response :success
80 assert_template 'index.rhtml'
91 assert_template 'index.rhtml'
81 assert_not_nil assigns(:issues)
92 assert_not_nil assigns(:issues)
82 assert_tag :tag => 'a', :content => /Can't print recipes/
93 assert_tag :tag => 'a', :content => /Can't print recipes/
83 assert_tag :tag => 'a', :content => /Subproject issue/
94 assert_tag :tag => 'a', :content => /Subproject issue/
84 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
95 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
85 end
96 end
86
97
87 def test_index_with_project_and_subprojects_should_show_private_subprojects
98 def test_index_with_project_and_subprojects_should_show_private_subprojects
88 @request.session[:user_id] = 2
99 @request.session[:user_id] = 2
89 Setting.display_subprojects_issues = 1
100 Setting.display_subprojects_issues = 1
90 get :index, :project_id => 1
101 get :index, :project_id => 1
91 assert_response :success
102 assert_response :success
92 assert_template 'index.rhtml'
103 assert_template 'index.rhtml'
93 assert_not_nil assigns(:issues)
104 assert_not_nil assigns(:issues)
94 assert_tag :tag => 'a', :content => /Can't print recipes/
105 assert_tag :tag => 'a', :content => /Can't print recipes/
95 assert_tag :tag => 'a', :content => /Subproject issue/
106 assert_tag :tag => 'a', :content => /Subproject issue/
96 assert_tag :tag => 'a', :content => /Issue of a private subproject/
107 assert_tag :tag => 'a', :content => /Issue of a private subproject/
97 end
108 end
98
109
99 def test_index_with_project_and_filter
110 def test_index_with_project_and_filter
100 get :index, :project_id => 1, :set_filter => 1
111 get :index, :project_id => 1, :set_filter => 1
101 assert_response :success
112 assert_response :success
102 assert_template 'index.rhtml'
113 assert_template 'index.rhtml'
103 assert_not_nil assigns(:issues)
114 assert_not_nil assigns(:issues)
104 end
115 end
105
116
106 def test_index_csv_with_project
117 def test_index_csv_with_project
107 get :index, :format => 'csv'
118 get :index, :format => 'csv'
108 assert_response :success
119 assert_response :success
109 assert_not_nil assigns(:issues)
120 assert_not_nil assigns(:issues)
110 assert_equal 'text/csv', @response.content_type
121 assert_equal 'text/csv', @response.content_type
111
122
112 get :index, :project_id => 1, :format => 'csv'
123 get :index, :project_id => 1, :format => 'csv'
113 assert_response :success
124 assert_response :success
114 assert_not_nil assigns(:issues)
125 assert_not_nil assigns(:issues)
115 assert_equal 'text/csv', @response.content_type
126 assert_equal 'text/csv', @response.content_type
116 end
127 end
117
128
118 def test_index_pdf
129 def test_index_pdf
119 get :index, :format => 'pdf'
130 get :index, :format => 'pdf'
120 assert_response :success
131 assert_response :success
121 assert_not_nil assigns(:issues)
132 assert_not_nil assigns(:issues)
122 assert_equal 'application/pdf', @response.content_type
133 assert_equal 'application/pdf', @response.content_type
123
134
124 get :index, :project_id => 1, :format => 'pdf'
135 get :index, :project_id => 1, :format => 'pdf'
125 assert_response :success
136 assert_response :success
126 assert_not_nil assigns(:issues)
137 assert_not_nil assigns(:issues)
127 assert_equal 'application/pdf', @response.content_type
138 assert_equal 'application/pdf', @response.content_type
128 end
139 end
129
140
130 def test_gantt
141 def test_gantt
131 get :gantt, :project_id => 1
142 get :gantt, :project_id => 1
132 assert_response :success
143 assert_response :success
133 assert_template 'gantt.rhtml'
144 assert_template 'gantt.rhtml'
134 assert_not_nil assigns(:gantt)
145 assert_not_nil assigns(:gantt)
135 events = assigns(:gantt).events
146 events = assigns(:gantt).events
136 assert_not_nil events
147 assert_not_nil events
137 # Issue with start and due dates
148 # Issue with start and due dates
138 i = Issue.find(1)
149 i = Issue.find(1)
139 assert_not_nil i.due_date
150 assert_not_nil i.due_date
140 assert events.include?(Issue.find(1))
151 assert events.include?(Issue.find(1))
141 # Issue with without due date but targeted to a version with date
152 # Issue with without due date but targeted to a version with date
142 i = Issue.find(2)
153 i = Issue.find(2)
143 assert_nil i.due_date
154 assert_nil i.due_date
144 assert events.include?(i)
155 assert events.include?(i)
145 end
156 end
146
157
147 def test_gantt_export_to_pdf
158 def test_gantt_export_to_pdf
148 get :gantt, :project_id => 1, :format => 'pdf'
159 get :gantt, :project_id => 1, :format => 'pdf'
149 assert_response :success
160 assert_response :success
150 assert_template 'gantt.rfpdf'
161 assert_template 'gantt.rfpdf'
151 assert_equal 'application/pdf', @response.content_type
162 assert_equal 'application/pdf', @response.content_type
152 assert_not_nil assigns(:gantt)
163 assert_not_nil assigns(:gantt)
153 end
164 end
154
165
155 if Object.const_defined?(:Magick)
166 if Object.const_defined?(:Magick)
156 def test_gantt_image
167 def test_gantt_image
157 get :gantt, :project_id => 1, :format => 'png'
168 get :gantt, :project_id => 1, :format => 'png'
158 assert_response :success
169 assert_response :success
159 assert_equal 'image/png', @response.content_type
170 assert_equal 'image/png', @response.content_type
160 end
171 end
161 else
172 else
162 puts "RMagick not installed. Skipping tests !!!"
173 puts "RMagick not installed. Skipping tests !!!"
163 end
174 end
164
175
165 def test_calendar
176 def test_calendar
166 get :calendar, :project_id => 1
177 get :calendar, :project_id => 1
167 assert_response :success
178 assert_response :success
168 assert_template 'calendar'
179 assert_template 'calendar'
169 assert_not_nil assigns(:calendar)
180 assert_not_nil assigns(:calendar)
170 end
181 end
171
182
172 def test_changes
183 def test_changes
173 get :changes, :project_id => 1
184 get :changes, :project_id => 1
174 assert_response :success
185 assert_response :success
175 assert_not_nil assigns(:journals)
186 assert_not_nil assigns(:journals)
176 assert_equal 'application/atom+xml', @response.content_type
187 assert_equal 'application/atom+xml', @response.content_type
177 end
188 end
178
189
179 def test_show_by_anonymous
190 def test_show_by_anonymous
180 get :show, :id => 1
191 get :show, :id => 1
181 assert_response :success
192 assert_response :success
182 assert_template 'show.rhtml'
193 assert_template 'show.rhtml'
183 assert_not_nil assigns(:issue)
194 assert_not_nil assigns(:issue)
184 assert_equal Issue.find(1), assigns(:issue)
195 assert_equal Issue.find(1), assigns(:issue)
185
196
186 # anonymous role is allowed to add a note
197 # anonymous role is allowed to add a note
187 assert_tag :tag => 'form',
198 assert_tag :tag => 'form',
188 :descendant => { :tag => 'fieldset',
199 :descendant => { :tag => 'fieldset',
189 :child => { :tag => 'legend',
200 :child => { :tag => 'legend',
190 :content => /Notes/ } }
201 :content => /Notes/ } }
191 end
202 end
192
203
193 def test_show_by_manager
204 def test_show_by_manager
194 @request.session[:user_id] = 2
205 @request.session[:user_id] = 2
195 get :show, :id => 1
206 get :show, :id => 1
196 assert_response :success
207 assert_response :success
197
208
198 assert_tag :tag => 'form',
209 assert_tag :tag => 'form',
199 :descendant => { :tag => 'fieldset',
210 :descendant => { :tag => 'fieldset',
200 :child => { :tag => 'legend',
211 :child => { :tag => 'legend',
201 :content => /Change properties/ } },
212 :content => /Change properties/ } },
202 :descendant => { :tag => 'fieldset',
213 :descendant => { :tag => 'fieldset',
203 :child => { :tag => 'legend',
214 :child => { :tag => 'legend',
204 :content => /Log time/ } },
215 :content => /Log time/ } },
205 :descendant => { :tag => 'fieldset',
216 :descendant => { :tag => 'fieldset',
206 :child => { :tag => 'legend',
217 :child => { :tag => 'legend',
207 :content => /Notes/ } }
218 :content => /Notes/ } }
208 end
219 end
209
220
210 def test_get_new
221 def test_get_new
211 @request.session[:user_id] = 2
222 @request.session[:user_id] = 2
212 get :new, :project_id => 1, :tracker_id => 1
223 get :new, :project_id => 1, :tracker_id => 1
213 assert_response :success
224 assert_response :success
214 assert_template 'new'
225 assert_template 'new'
215
226
216 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
227 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
217 :value => 'Default string' }
228 :value => 'Default string' }
218 end
229 end
219
230
220 def test_get_new_without_tracker_id
231 def test_get_new_without_tracker_id
221 @request.session[:user_id] = 2
232 @request.session[:user_id] = 2
222 get :new, :project_id => 1
233 get :new, :project_id => 1
223 assert_response :success
234 assert_response :success
224 assert_template 'new'
235 assert_template 'new'
225
236
226 issue = assigns(:issue)
237 issue = assigns(:issue)
227 assert_not_nil issue
238 assert_not_nil issue
228 assert_equal Project.find(1).trackers.first, issue.tracker
239 assert_equal Project.find(1).trackers.first, issue.tracker
229 end
240 end
230
241
231 def test_update_new_form
242 def test_update_new_form
232 @request.session[:user_id] = 2
243 @request.session[:user_id] = 2
233 xhr :post, :new, :project_id => 1,
244 xhr :post, :new, :project_id => 1,
234 :issue => {:tracker_id => 2,
245 :issue => {:tracker_id => 2,
235 :subject => 'This is the test_new issue',
246 :subject => 'This is the test_new issue',
236 :description => 'This is the description',
247 :description => 'This is the description',
237 :priority_id => 5}
248 :priority_id => 5}
238 assert_response :success
249 assert_response :success
239 assert_template 'new'
250 assert_template 'new'
240 end
251 end
241
252
242 def test_post_new
253 def test_post_new
243 @request.session[:user_id] = 2
254 @request.session[:user_id] = 2
244 post :new, :project_id => 1,
255 post :new, :project_id => 1,
245 :issue => {:tracker_id => 3,
256 :issue => {:tracker_id => 3,
246 :subject => 'This is the test_new issue',
257 :subject => 'This is the test_new issue',
247 :description => 'This is the description',
258 :description => 'This is the description',
248 :priority_id => 5,
259 :priority_id => 5,
249 :estimated_hours => '',
260 :estimated_hours => '',
250 :custom_field_values => {'2' => 'Value for field 2'}}
261 :custom_field_values => {'2' => 'Value for field 2'}}
251 assert_redirected_to 'issues/show'
262 assert_redirected_to 'issues/show'
252
263
253 issue = Issue.find_by_subject('This is the test_new issue')
264 issue = Issue.find_by_subject('This is the test_new issue')
254 assert_not_nil issue
265 assert_not_nil issue
255 assert_equal 2, issue.author_id
266 assert_equal 2, issue.author_id
256 assert_equal 3, issue.tracker_id
267 assert_equal 3, issue.tracker_id
257 assert_nil issue.estimated_hours
268 assert_nil issue.estimated_hours
258 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
269 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
259 assert_not_nil v
270 assert_not_nil v
260 assert_equal 'Value for field 2', v.value
271 assert_equal 'Value for field 2', v.value
261 end
272 end
262
273
263 def test_post_new_without_custom_fields_param
274 def test_post_new_without_custom_fields_param
264 @request.session[:user_id] = 2
275 @request.session[:user_id] = 2
265 post :new, :project_id => 1,
276 post :new, :project_id => 1,
266 :issue => {:tracker_id => 1,
277 :issue => {:tracker_id => 1,
267 :subject => 'This is the test_new issue',
278 :subject => 'This is the test_new issue',
268 :description => 'This is the description',
279 :description => 'This is the description',
269 :priority_id => 5}
280 :priority_id => 5}
270 assert_redirected_to 'issues/show'
281 assert_redirected_to 'issues/show'
271 end
282 end
272
283
273 def test_post_new_with_required_custom_field_and_without_custom_fields_param
284 def test_post_new_with_required_custom_field_and_without_custom_fields_param
274 field = IssueCustomField.find_by_name('Database')
285 field = IssueCustomField.find_by_name('Database')
275 field.update_attribute(:is_required, true)
286 field.update_attribute(:is_required, true)
276
287
277 @request.session[:user_id] = 2
288 @request.session[:user_id] = 2
278 post :new, :project_id => 1,
289 post :new, :project_id => 1,
279 :issue => {:tracker_id => 1,
290 :issue => {:tracker_id => 1,
280 :subject => 'This is the test_new issue',
291 :subject => 'This is the test_new issue',
281 :description => 'This is the description',
292 :description => 'This is the description',
282 :priority_id => 5}
293 :priority_id => 5}
283 assert_response :success
294 assert_response :success
284 assert_template 'new'
295 assert_template 'new'
285 issue = assigns(:issue)
296 issue = assigns(:issue)
286 assert_not_nil issue
297 assert_not_nil issue
287 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
298 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
288 end
299 end
289
300
290 def test_post_should_preserve_fields_values_on_validation_failure
301 def test_post_should_preserve_fields_values_on_validation_failure
291 @request.session[:user_id] = 2
302 @request.session[:user_id] = 2
292 post :new, :project_id => 1,
303 post :new, :project_id => 1,
293 :issue => {:tracker_id => 1,
304 :issue => {:tracker_id => 1,
294 :subject => 'This is the test_new issue',
305 :subject => 'This is the test_new issue',
295 # empty description
306 # empty description
296 :description => '',
307 :description => '',
297 :priority_id => 6,
308 :priority_id => 6,
298 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
309 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
299 assert_response :success
310 assert_response :success
300 assert_template 'new'
311 assert_template 'new'
301
312
302 assert_tag :input, :attributes => { :name => 'issue[subject]',
313 assert_tag :input, :attributes => { :name => 'issue[subject]',
303 :value => 'This is the test_new issue' }
314 :value => 'This is the test_new issue' }
304 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
315 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
305 :child => { :tag => 'option', :attributes => { :selected => 'selected',
316 :child => { :tag => 'option', :attributes => { :selected => 'selected',
306 :value => '6' },
317 :value => '6' },
307 :content => 'High' }
318 :content => 'High' }
308 # Custom fields
319 # Custom fields
309 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
320 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
310 :child => { :tag => 'option', :attributes => { :selected => 'selected',
321 :child => { :tag => 'option', :attributes => { :selected => 'selected',
311 :value => 'Oracle' },
322 :value => 'Oracle' },
312 :content => 'Oracle' }
323 :content => 'Oracle' }
313 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
324 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
314 :value => 'Value for field 2'}
325 :value => 'Value for field 2'}
315 end
326 end
316
327
317 def test_copy_issue
328 def test_copy_issue
318 @request.session[:user_id] = 2
329 @request.session[:user_id] = 2
319 get :new, :project_id => 1, :copy_from => 1
330 get :new, :project_id => 1, :copy_from => 1
320 assert_template 'new'
331 assert_template 'new'
321 assert_not_nil assigns(:issue)
332 assert_not_nil assigns(:issue)
322 orig = Issue.find(1)
333 orig = Issue.find(1)
323 assert_equal orig.subject, assigns(:issue).subject
334 assert_equal orig.subject, assigns(:issue).subject
324 end
335 end
325
336
326 def test_get_edit
337 def test_get_edit
327 @request.session[:user_id] = 2
338 @request.session[:user_id] = 2
328 get :edit, :id => 1
339 get :edit, :id => 1
329 assert_response :success
340 assert_response :success
330 assert_template 'edit'
341 assert_template 'edit'
331 assert_not_nil assigns(:issue)
342 assert_not_nil assigns(:issue)
332 assert_equal Issue.find(1), assigns(:issue)
343 assert_equal Issue.find(1), assigns(:issue)
333 end
344 end
334
345
335 def test_get_edit_with_params
346 def test_get_edit_with_params
336 @request.session[:user_id] = 2
347 @request.session[:user_id] = 2
337 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
348 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
338 assert_response :success
349 assert_response :success
339 assert_template 'edit'
350 assert_template 'edit'
340
351
341 issue = assigns(:issue)
352 issue = assigns(:issue)
342 assert_not_nil issue
353 assert_not_nil issue
343
354
344 assert_equal 5, issue.status_id
355 assert_equal 5, issue.status_id
345 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
356 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
346 :child => { :tag => 'option',
357 :child => { :tag => 'option',
347 :content => 'Closed',
358 :content => 'Closed',
348 :attributes => { :selected => 'selected' } }
359 :attributes => { :selected => 'selected' } }
349
360
350 assert_equal 7, issue.priority_id
361 assert_equal 7, issue.priority_id
351 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
362 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
352 :child => { :tag => 'option',
363 :child => { :tag => 'option',
353 :content => 'Urgent',
364 :content => 'Urgent',
354 :attributes => { :selected => 'selected' } }
365 :attributes => { :selected => 'selected' } }
355 end
366 end
356
367
357 def test_reply_to_issue
368 def test_reply_to_issue
358 @request.session[:user_id] = 2
369 @request.session[:user_id] = 2
359 get :reply, :id => 1
370 get :reply, :id => 1
360 assert_response :success
371 assert_response :success
361 assert_select_rjs :show, "update"
372 assert_select_rjs :show, "update"
362 end
373 end
363
374
364 def test_reply_to_note
375 def test_reply_to_note
365 @request.session[:user_id] = 2
376 @request.session[:user_id] = 2
366 get :reply, :id => 1, :journal_id => 2
377 get :reply, :id => 1, :journal_id => 2
367 assert_response :success
378 assert_response :success
368 assert_select_rjs :show, "update"
379 assert_select_rjs :show, "update"
369 end
380 end
370
381
371 def test_post_edit_without_custom_fields_param
382 def test_post_edit_without_custom_fields_param
372 @request.session[:user_id] = 2
383 @request.session[:user_id] = 2
373 ActionMailer::Base.deliveries.clear
384 ActionMailer::Base.deliveries.clear
374
385
375 issue = Issue.find(1)
386 issue = Issue.find(1)
376 assert_equal '125', issue.custom_value_for(2).value
387 assert_equal '125', issue.custom_value_for(2).value
377 old_subject = issue.subject
388 old_subject = issue.subject
378 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
389 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
379
390
380 assert_difference('Journal.count') do
391 assert_difference('Journal.count') do
381 assert_difference('JournalDetail.count', 2) do
392 assert_difference('JournalDetail.count', 2) do
382 post :edit, :id => 1, :issue => {:subject => new_subject,
393 post :edit, :id => 1, :issue => {:subject => new_subject,
383 :priority_id => '6',
394 :priority_id => '6',
384 :category_id => '1' # no change
395 :category_id => '1' # no change
385 }
396 }
386 end
397 end
387 end
398 end
388 assert_redirected_to 'issues/show/1'
399 assert_redirected_to 'issues/show/1'
389 issue.reload
400 issue.reload
390 assert_equal new_subject, issue.subject
401 assert_equal new_subject, issue.subject
391 # Make sure custom fields were not cleared
402 # Make sure custom fields were not cleared
392 assert_equal '125', issue.custom_value_for(2).value
403 assert_equal '125', issue.custom_value_for(2).value
393
404
394 mail = ActionMailer::Base.deliveries.last
405 mail = ActionMailer::Base.deliveries.last
395 assert_kind_of TMail::Mail, mail
406 assert_kind_of TMail::Mail, mail
396 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
407 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
397 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
408 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
398 end
409 end
399
410
400 def test_post_edit_with_custom_field_change
411 def test_post_edit_with_custom_field_change
401 @request.session[:user_id] = 2
412 @request.session[:user_id] = 2
402 issue = Issue.find(1)
413 issue = Issue.find(1)
403 assert_equal '125', issue.custom_value_for(2).value
414 assert_equal '125', issue.custom_value_for(2).value
404
415
405 assert_difference('Journal.count') do
416 assert_difference('Journal.count') do
406 assert_difference('JournalDetail.count', 3) do
417 assert_difference('JournalDetail.count', 3) do
407 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
418 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
408 :priority_id => '6',
419 :priority_id => '6',
409 :category_id => '1', # no change
420 :category_id => '1', # no change
410 :custom_field_values => { '2' => 'New custom value' }
421 :custom_field_values => { '2' => 'New custom value' }
411 }
422 }
412 end
423 end
413 end
424 end
414 assert_redirected_to 'issues/show/1'
425 assert_redirected_to 'issues/show/1'
415 issue.reload
426 issue.reload
416 assert_equal 'New custom value', issue.custom_value_for(2).value
427 assert_equal 'New custom value', issue.custom_value_for(2).value
417
428
418 mail = ActionMailer::Base.deliveries.last
429 mail = ActionMailer::Base.deliveries.last
419 assert_kind_of TMail::Mail, mail
430 assert_kind_of TMail::Mail, mail
420 assert mail.body.include?("Searchable field changed from 125 to New custom value")
431 assert mail.body.include?("Searchable field changed from 125 to New custom value")
421 end
432 end
422
433
423 def test_post_edit_with_status_and_assignee_change
434 def test_post_edit_with_status_and_assignee_change
424 issue = Issue.find(1)
435 issue = Issue.find(1)
425 assert_equal 1, issue.status_id
436 assert_equal 1, issue.status_id
426 @request.session[:user_id] = 2
437 @request.session[:user_id] = 2
427 assert_difference('TimeEntry.count', 0) do
438 assert_difference('TimeEntry.count', 0) do
428 post :edit,
439 post :edit,
429 :id => 1,
440 :id => 1,
430 :issue => { :status_id => 2, :assigned_to_id => 3 },
441 :issue => { :status_id => 2, :assigned_to_id => 3 },
431 :notes => 'Assigned to dlopper',
442 :notes => 'Assigned to dlopper',
432 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
443 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
433 end
444 end
434 assert_redirected_to 'issues/show/1'
445 assert_redirected_to 'issues/show/1'
435 issue.reload
446 issue.reload
436 assert_equal 2, issue.status_id
447 assert_equal 2, issue.status_id
437 j = issue.journals.find(:first, :order => 'id DESC')
448 j = issue.journals.find(:first, :order => 'id DESC')
438 assert_equal 'Assigned to dlopper', j.notes
449 assert_equal 'Assigned to dlopper', j.notes
439 assert_equal 2, j.details.size
450 assert_equal 2, j.details.size
440
451
441 mail = ActionMailer::Base.deliveries.last
452 mail = ActionMailer::Base.deliveries.last
442 assert mail.body.include?("Status changed from New to Assigned")
453 assert mail.body.include?("Status changed from New to Assigned")
443 end
454 end
444
455
445 def test_post_edit_with_note_only
456 def test_post_edit_with_note_only
446 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
457 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
447 # anonymous user
458 # anonymous user
448 post :edit,
459 post :edit,
449 :id => 1,
460 :id => 1,
450 :notes => notes
461 :notes => notes
451 assert_redirected_to 'issues/show/1'
462 assert_redirected_to 'issues/show/1'
452 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
463 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
453 assert_equal notes, j.notes
464 assert_equal notes, j.notes
454 assert_equal 0, j.details.size
465 assert_equal 0, j.details.size
455 assert_equal User.anonymous, j.user
466 assert_equal User.anonymous, j.user
456
467
457 mail = ActionMailer::Base.deliveries.last
468 mail = ActionMailer::Base.deliveries.last
458 assert mail.body.include?(notes)
469 assert mail.body.include?(notes)
459 end
470 end
460
471
461 def test_post_edit_with_note_and_spent_time
472 def test_post_edit_with_note_and_spent_time
462 @request.session[:user_id] = 2
473 @request.session[:user_id] = 2
463 spent_hours_before = Issue.find(1).spent_hours
474 spent_hours_before = Issue.find(1).spent_hours
464 assert_difference('TimeEntry.count') do
475 assert_difference('TimeEntry.count') do
465 post :edit,
476 post :edit,
466 :id => 1,
477 :id => 1,
467 :notes => '2.5 hours added',
478 :notes => '2.5 hours added',
468 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
479 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
469 end
480 end
470 assert_redirected_to 'issues/show/1'
481 assert_redirected_to 'issues/show/1'
471
482
472 issue = Issue.find(1)
483 issue = Issue.find(1)
473
484
474 j = issue.journals.find(:first, :order => 'id DESC')
485 j = issue.journals.find(:first, :order => 'id DESC')
475 assert_equal '2.5 hours added', j.notes
486 assert_equal '2.5 hours added', j.notes
476 assert_equal 0, j.details.size
487 assert_equal 0, j.details.size
477
488
478 t = issue.time_entries.find(:first, :order => 'id DESC')
489 t = issue.time_entries.find(:first, :order => 'id DESC')
479 assert_not_nil t
490 assert_not_nil t
480 assert_equal 2.5, t.hours
491 assert_equal 2.5, t.hours
481 assert_equal spent_hours_before + 2.5, issue.spent_hours
492 assert_equal spent_hours_before + 2.5, issue.spent_hours
482 end
493 end
483
494
484 def test_post_edit_with_attachment_only
495 def test_post_edit_with_attachment_only
485 set_tmp_attachments_directory
496 set_tmp_attachments_directory
486
497
487 # anonymous user
498 # anonymous user
488 post :edit,
499 post :edit,
489 :id => 1,
500 :id => 1,
490 :notes => '',
501 :notes => '',
491 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
502 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
492 assert_redirected_to 'issues/show/1'
503 assert_redirected_to 'issues/show/1'
493 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
504 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
494 assert j.notes.blank?
505 assert j.notes.blank?
495 assert_equal 1, j.details.size
506 assert_equal 1, j.details.size
496 assert_equal 'testfile.txt', j.details.first.value
507 assert_equal 'testfile.txt', j.details.first.value
497 assert_equal User.anonymous, j.user
508 assert_equal User.anonymous, j.user
498
509
499 mail = ActionMailer::Base.deliveries.last
510 mail = ActionMailer::Base.deliveries.last
500 assert mail.body.include?('testfile.txt')
511 assert mail.body.include?('testfile.txt')
501 end
512 end
502
513
503 def test_post_edit_with_no_change
514 def test_post_edit_with_no_change
504 issue = Issue.find(1)
515 issue = Issue.find(1)
505 issue.journals.clear
516 issue.journals.clear
506 ActionMailer::Base.deliveries.clear
517 ActionMailer::Base.deliveries.clear
507
518
508 post :edit,
519 post :edit,
509 :id => 1,
520 :id => 1,
510 :notes => ''
521 :notes => ''
511 assert_redirected_to 'issues/show/1'
522 assert_redirected_to 'issues/show/1'
512
523
513 issue.reload
524 issue.reload
514 assert issue.journals.empty?
525 assert issue.journals.empty?
515 # No email should be sent
526 # No email should be sent
516 assert ActionMailer::Base.deliveries.empty?
527 assert ActionMailer::Base.deliveries.empty?
517 end
528 end
518
529
519 def test_bulk_edit
530 def test_bulk_edit
520 @request.session[:user_id] = 2
531 @request.session[:user_id] = 2
521 # update issues priority
532 # update issues priority
522 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
533 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
523 assert_response 302
534 assert_response 302
524 # check that the issues were updated
535 # check that the issues were updated
525 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
536 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
526 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
537 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
527 end
538 end
528
539
529 def test_bulk_unassign
540 def test_bulk_unassign
530 assert_not_nil Issue.find(2).assigned_to
541 assert_not_nil Issue.find(2).assigned_to
531 @request.session[:user_id] = 2
542 @request.session[:user_id] = 2
532 # unassign issues
543 # unassign issues
533 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
544 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
534 assert_response 302
545 assert_response 302
535 # check that the issues were updated
546 # check that the issues were updated
536 assert_nil Issue.find(2).assigned_to
547 assert_nil Issue.find(2).assigned_to
537 end
548 end
538
549
539 def test_move_one_issue_to_another_project
550 def test_move_one_issue_to_another_project
540 @request.session[:user_id] = 1
551 @request.session[:user_id] = 1
541 post :move, :id => 1, :new_project_id => 2
552 post :move, :id => 1, :new_project_id => 2
542 assert_redirected_to 'projects/ecookbook/issues'
553 assert_redirected_to 'projects/ecookbook/issues'
543 assert_equal 2, Issue.find(1).project_id
554 assert_equal 2, Issue.find(1).project_id
544 end
555 end
545
556
546 def test_bulk_move_to_another_project
557 def test_bulk_move_to_another_project
547 @request.session[:user_id] = 1
558 @request.session[:user_id] = 1
548 post :move, :ids => [1, 2], :new_project_id => 2
559 post :move, :ids => [1, 2], :new_project_id => 2
549 assert_redirected_to 'projects/ecookbook/issues'
560 assert_redirected_to 'projects/ecookbook/issues'
550 # Issues moved to project 2
561 # Issues moved to project 2
551 assert_equal 2, Issue.find(1).project_id
562 assert_equal 2, Issue.find(1).project_id
552 assert_equal 2, Issue.find(2).project_id
563 assert_equal 2, Issue.find(2).project_id
553 # No tracker change
564 # No tracker change
554 assert_equal 1, Issue.find(1).tracker_id
565 assert_equal 1, Issue.find(1).tracker_id
555 assert_equal 2, Issue.find(2).tracker_id
566 assert_equal 2, Issue.find(2).tracker_id
556 end
567 end
557
568
558 def test_bulk_move_to_another_tracker
569 def test_bulk_move_to_another_tracker
559 @request.session[:user_id] = 1
570 @request.session[:user_id] = 1
560 post :move, :ids => [1, 2], :new_tracker_id => 2
571 post :move, :ids => [1, 2], :new_tracker_id => 2
561 assert_redirected_to 'projects/ecookbook/issues'
572 assert_redirected_to 'projects/ecookbook/issues'
562 assert_equal 2, Issue.find(1).tracker_id
573 assert_equal 2, Issue.find(1).tracker_id
563 assert_equal 2, Issue.find(2).tracker_id
574 assert_equal 2, Issue.find(2).tracker_id
564 end
575 end
565
576
566 def test_context_menu_one_issue
577 def test_context_menu_one_issue
567 @request.session[:user_id] = 2
578 @request.session[:user_id] = 2
568 get :context_menu, :ids => [1]
579 get :context_menu, :ids => [1]
569 assert_response :success
580 assert_response :success
570 assert_template 'context_menu'
581 assert_template 'context_menu'
571 assert_tag :tag => 'a', :content => 'Edit',
582 assert_tag :tag => 'a', :content => 'Edit',
572 :attributes => { :href => '/issues/edit/1',
583 :attributes => { :href => '/issues/edit/1',
573 :class => 'icon-edit' }
584 :class => 'icon-edit' }
574 assert_tag :tag => 'a', :content => 'Closed',
585 assert_tag :tag => 'a', :content => 'Closed',
575 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
586 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
576 :class => '' }
587 :class => '' }
577 assert_tag :tag => 'a', :content => 'Immediate',
588 assert_tag :tag => 'a', :content => 'Immediate',
578 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
589 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
579 :class => '' }
590 :class => '' }
580 assert_tag :tag => 'a', :content => 'Dave Lopper',
591 assert_tag :tag => 'a', :content => 'Dave Lopper',
581 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
592 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
582 :class => '' }
593 :class => '' }
583 assert_tag :tag => 'a', :content => 'Copy',
594 assert_tag :tag => 'a', :content => 'Copy',
584 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
595 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
585 :class => 'icon-copy' }
596 :class => 'icon-copy' }
586 assert_tag :tag => 'a', :content => 'Move',
597 assert_tag :tag => 'a', :content => 'Move',
587 :attributes => { :href => '/issues/move?ids%5B%5D=1',
598 :attributes => { :href => '/issues/move?ids%5B%5D=1',
588 :class => 'icon-move' }
599 :class => 'icon-move' }
589 assert_tag :tag => 'a', :content => 'Delete',
600 assert_tag :tag => 'a', :content => 'Delete',
590 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
601 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
591 :class => 'icon-del' }
602 :class => 'icon-del' }
592 end
603 end
593
604
594 def test_context_menu_one_issue_by_anonymous
605 def test_context_menu_one_issue_by_anonymous
595 get :context_menu, :ids => [1]
606 get :context_menu, :ids => [1]
596 assert_response :success
607 assert_response :success
597 assert_template 'context_menu'
608 assert_template 'context_menu'
598 assert_tag :tag => 'a', :content => 'Delete',
609 assert_tag :tag => 'a', :content => 'Delete',
599 :attributes => { :href => '#',
610 :attributes => { :href => '#',
600 :class => 'icon-del disabled' }
611 :class => 'icon-del disabled' }
601 end
612 end
602
613
603 def test_context_menu_multiple_issues_of_same_project
614 def test_context_menu_multiple_issues_of_same_project
604 @request.session[:user_id] = 2
615 @request.session[:user_id] = 2
605 get :context_menu, :ids => [1, 2]
616 get :context_menu, :ids => [1, 2]
606 assert_response :success
617 assert_response :success
607 assert_template 'context_menu'
618 assert_template 'context_menu'
608 assert_tag :tag => 'a', :content => 'Edit',
619 assert_tag :tag => 'a', :content => 'Edit',
609 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
620 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
610 :class => 'icon-edit' }
621 :class => 'icon-edit' }
611 assert_tag :tag => 'a', :content => 'Immediate',
622 assert_tag :tag => 'a', :content => 'Immediate',
612 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
623 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
613 :class => '' }
624 :class => '' }
614 assert_tag :tag => 'a', :content => 'Dave Lopper',
625 assert_tag :tag => 'a', :content => 'Dave Lopper',
615 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
626 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
616 :class => '' }
627 :class => '' }
617 assert_tag :tag => 'a', :content => 'Move',
628 assert_tag :tag => 'a', :content => 'Move',
618 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
629 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
619 :class => 'icon-move' }
630 :class => 'icon-move' }
620 assert_tag :tag => 'a', :content => 'Delete',
631 assert_tag :tag => 'a', :content => 'Delete',
621 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
632 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
622 :class => 'icon-del' }
633 :class => 'icon-del' }
623 end
634 end
624
635
625 def test_context_menu_multiple_issues_of_different_project
636 def test_context_menu_multiple_issues_of_different_project
626 @request.session[:user_id] = 2
637 @request.session[:user_id] = 2
627 get :context_menu, :ids => [1, 2, 4]
638 get :context_menu, :ids => [1, 2, 4]
628 assert_response :success
639 assert_response :success
629 assert_template 'context_menu'
640 assert_template 'context_menu'
630 assert_tag :tag => 'a', :content => 'Delete',
641 assert_tag :tag => 'a', :content => 'Delete',
631 :attributes => { :href => '#',
642 :attributes => { :href => '#',
632 :class => 'icon-del disabled' }
643 :class => 'icon-del disabled' }
633 end
644 end
634
645
635 def test_destroy_issue_with_no_time_entries
646 def test_destroy_issue_with_no_time_entries
636 assert_nil TimeEntry.find_by_issue_id(2)
647 assert_nil TimeEntry.find_by_issue_id(2)
637 @request.session[:user_id] = 2
648 @request.session[:user_id] = 2
638 post :destroy, :id => 2
649 post :destroy, :id => 2
639 assert_redirected_to 'projects/ecookbook/issues'
650 assert_redirected_to 'projects/ecookbook/issues'
640 assert_nil Issue.find_by_id(2)
651 assert_nil Issue.find_by_id(2)
641 end
652 end
642
653
643 def test_destroy_issues_with_time_entries
654 def test_destroy_issues_with_time_entries
644 @request.session[:user_id] = 2
655 @request.session[:user_id] = 2
645 post :destroy, :ids => [1, 3]
656 post :destroy, :ids => [1, 3]
646 assert_response :success
657 assert_response :success
647 assert_template 'destroy'
658 assert_template 'destroy'
648 assert_not_nil assigns(:hours)
659 assert_not_nil assigns(:hours)
649 assert Issue.find_by_id(1) && Issue.find_by_id(3)
660 assert Issue.find_by_id(1) && Issue.find_by_id(3)
650 end
661 end
651
662
652 def test_destroy_issues_and_destroy_time_entries
663 def test_destroy_issues_and_destroy_time_entries
653 @request.session[:user_id] = 2
664 @request.session[:user_id] = 2
654 post :destroy, :ids => [1, 3], :todo => 'destroy'
665 post :destroy, :ids => [1, 3], :todo => 'destroy'
655 assert_redirected_to 'projects/ecookbook/issues'
666 assert_redirected_to 'projects/ecookbook/issues'
656 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
667 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
657 assert_nil TimeEntry.find_by_id([1, 2])
668 assert_nil TimeEntry.find_by_id([1, 2])
658 end
669 end
659
670
660 def test_destroy_issues_and_assign_time_entries_to_project
671 def test_destroy_issues_and_assign_time_entries_to_project
661 @request.session[:user_id] = 2
672 @request.session[:user_id] = 2
662 post :destroy, :ids => [1, 3], :todo => 'nullify'
673 post :destroy, :ids => [1, 3], :todo => 'nullify'
663 assert_redirected_to 'projects/ecookbook/issues'
674 assert_redirected_to 'projects/ecookbook/issues'
664 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
675 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
665 assert_nil TimeEntry.find(1).issue_id
676 assert_nil TimeEntry.find(1).issue_id
666 assert_nil TimeEntry.find(2).issue_id
677 assert_nil TimeEntry.find(2).issue_id
667 end
678 end
668
679
669 def test_destroy_issues_and_reassign_time_entries_to_another_issue
680 def test_destroy_issues_and_reassign_time_entries_to_another_issue
670 @request.session[:user_id] = 2
681 @request.session[:user_id] = 2
671 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
682 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
672 assert_redirected_to 'projects/ecookbook/issues'
683 assert_redirected_to 'projects/ecookbook/issues'
673 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
684 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
674 assert_equal 2, TimeEntry.find(1).issue_id
685 assert_equal 2, TimeEntry.find(1).issue_id
675 assert_equal 2, TimeEntry.find(2).issue_id
686 assert_equal 2, TimeEntry.find(2).issue_id
676 end
687 end
677
688
678 def test_destroy_attachment
689 def test_destroy_attachment
679 issue = Issue.find(3)
690 issue = Issue.find(3)
680 a = issue.attachments.size
691 a = issue.attachments.size
681 @request.session[:user_id] = 2
692 @request.session[:user_id] = 2
682 post :destroy_attachment, :id => 3, :attachment_id => 1
693 post :destroy_attachment, :id => 3, :attachment_id => 1
683 assert_redirected_to 'issues/show/3'
694 assert_redirected_to 'issues/show/3'
684 assert_nil Attachment.find_by_id(1)
695 assert_nil Attachment.find_by_id(1)
685 issue.reload
696 issue.reload
686 assert_equal((a-1), issue.attachments.size)
697 assert_equal((a-1), issue.attachments.size)
687 j = issue.journals.find(:first, :order => 'created_on DESC')
698 j = issue.journals.find(:first, :order => 'created_on DESC')
688 assert_equal 'attachment', j.details.first.property
699 assert_equal 'attachment', j.details.first.property
689 end
700 end
690 end
701 end
General Comments 0
You need to be logged in to leave comments. Login now