##// END OF EJS Templates
Added per-project tracker selection. Trackers can be selected on project settings....
Jean-Philippe Lang -
r907:8d91afc33e3b
parent child
Show More
@@ -0,0 +1,19
1 class CreateProjectsTrackers < ActiveRecord::Migration
2 def self.up
3 create_table :projects_trackers, :id => false do |t|
4 t.column :project_id, :integer, :default => 0, :null => false
5 t.column :tracker_id, :integer, :default => 0, :null => false
6 end
7 add_index :projects_trackers, :project_id, :name => :projects_trackers_project_id
8
9 # Associates all trackers to all projects (as it was before)
10 tracker_ids = Tracker.find(:all).collect(&:id)
11 Project.find(:all).each do |project|
12 project.tracker_ids = tracker_ids
13 end
14 end
15
16 def self.down
17 drop_table :projects_trackers
18 end
19 end
@@ -0,0 +1,46
1 ---
2 projects_trackers_012:
3 project_id: 4
4 tracker_id: 3
5 projects_trackers_001:
6 project_id: 1
7 tracker_id: 1
8 projects_trackers_013:
9 project_id: 5
10 tracker_id: 1
11 projects_trackers_002:
12 project_id: 1
13 tracker_id: 2
14 projects_trackers_014:
15 project_id: 5
16 tracker_id: 2
17 projects_trackers_003:
18 project_id: 1
19 tracker_id: 3
20 projects_trackers_015:
21 project_id: 5
22 tracker_id: 3
23 projects_trackers_004:
24 project_id: 2
25 tracker_id: 1
26 projects_trackers_005:
27 project_id: 2
28 tracker_id: 2
29 projects_trackers_006:
30 project_id: 2
31 tracker_id: 3
32 projects_trackers_007:
33 project_id: 3
34 tracker_id: 1
35 projects_trackers_008:
36 project_id: 3
37 tracker_id: 2
38 projects_trackers_009:
39 project_id: 3
40 tracker_id: 3
41 projects_trackers_010:
42 project_id: 4
43 tracker_id: 1
44 projects_trackers_011:
45 project_id: 4
46 tracker_id: 2
@@ -1,249 +1,250
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 layout 'base'
19 layout 'base'
20 before_filter :find_project, :authorize, :except => [:index, :changes, :preview]
20 before_filter :find_project, :authorize, :except => [:index, :changes, :preview]
21 before_filter :find_optional_project, :only => [:index, :changes]
21 before_filter :find_optional_project, :only => [:index, :changes]
22 accept_key_auth :index, :changes
22 accept_key_auth :index, :changes
23
23
24 cache_sweeper :issue_sweeper, :only => [ :edit, :change_status, :destroy ]
24 cache_sweeper :issue_sweeper, :only => [ :edit, :change_status, :destroy ]
25
25
26 helper :projects
26 helper :projects
27 include ProjectsHelper
27 include ProjectsHelper
28 helper :custom_fields
28 helper :custom_fields
29 include CustomFieldsHelper
29 include CustomFieldsHelper
30 helper :ifpdf
30 helper :ifpdf
31 include IfpdfHelper
31 include IfpdfHelper
32 helper :issue_relations
32 helper :issue_relations
33 include IssueRelationsHelper
33 include IssueRelationsHelper
34 helper :watchers
34 helper :watchers
35 include WatchersHelper
35 include WatchersHelper
36 helper :attachments
36 helper :attachments
37 include AttachmentsHelper
37 include AttachmentsHelper
38 helper :queries
38 helper :queries
39 helper :sort
39 helper :sort
40 include SortHelper
40 include SortHelper
41 include IssuesHelper
41 include IssuesHelper
42
42
43 def index
43 def index
44 sort_init "#{Issue.table_name}.id", "desc"
44 sort_init "#{Issue.table_name}.id", "desc"
45 sort_update
45 sort_update
46 retrieve_query
46 retrieve_query
47 if @query.valid?
47 if @query.valid?
48 limit = %w(pdf csv).include?(params[:format]) ? Setting.issues_export_limit.to_i : 25
48 limit = %w(pdf csv).include?(params[:format]) ? Setting.issues_export_limit.to_i : 25
49 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
49 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
50 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
50 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
51 @issues = Issue.find :all, :order => sort_clause,
51 @issues = Issue.find :all, :order => sort_clause,
52 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
52 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
53 :conditions => @query.statement,
53 :conditions => @query.statement,
54 :limit => limit,
54 :limit => limit,
55 :offset => @issue_pages.current.offset
55 :offset => @issue_pages.current.offset
56 respond_to do |format|
56 respond_to do |format|
57 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
57 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
58 format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
58 format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
59 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
59 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
60 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
60 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
61 end
61 end
62 else
62 else
63 # Send html if the query is not valid
63 # Send html if the query is not valid
64 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
64 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
65 end
65 end
66 end
66 end
67
67
68 def changes
68 def changes
69 sort_init "#{Issue.table_name}.id", "desc"
69 sort_init "#{Issue.table_name}.id", "desc"
70 sort_update
70 sort_update
71 retrieve_query
71 retrieve_query
72 if @query.valid?
72 if @query.valid?
73 @changes = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
73 @changes = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
74 :conditions => @query.statement,
74 :conditions => @query.statement,
75 :limit => 25,
75 :limit => 25,
76 :order => "#{Journal.table_name}.created_on DESC"
76 :order => "#{Journal.table_name}.created_on DESC"
77 end
77 end
78 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
78 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
79 render :layout => false, :content_type => 'application/atom+xml'
79 render :layout => false, :content_type => 'application/atom+xml'
80 end
80 end
81
81
82 def show
82 def show
83 @custom_values = @issue.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
83 @custom_values = @issue.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
84 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
84 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
85 @status_options = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
85 @status_options = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
86 respond_to do |format|
86 respond_to do |format|
87 format.html { render :template => 'issues/show.rhtml' }
87 format.html { render :template => 'issues/show.rhtml' }
88 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
88 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
89 end
89 end
90 end
90 end
91
91
92 def edit
92 def edit
93 @priorities = Enumeration::get_values('IPRI')
93 @priorities = Enumeration::get_values('IPRI')
94 if request.get?
94 if request.get?
95 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
95 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
96 else
96 else
97 begin
97 begin
98 @issue.init_journal(User.current)
98 @issue.init_journal(User.current)
99 # Retrieve custom fields and values
99 # Retrieve custom fields and values
100 if params["custom_fields"]
100 if params["custom_fields"]
101 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
101 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
102 @issue.custom_values = @custom_values
102 @issue.custom_values = @custom_values
103 end
103 end
104 @issue.attributes = params[:issue]
104 @issue.attributes = params[:issue]
105 if @issue.save
105 if @issue.save
106 flash[:notice] = l(:notice_successful_update)
106 flash[:notice] = l(:notice_successful_update)
107 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
107 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
108 end
108 end
109 rescue ActiveRecord::StaleObjectError
109 rescue ActiveRecord::StaleObjectError
110 # Optimistic locking exception
110 # Optimistic locking exception
111 flash[:error] = l(:notice_locking_conflict)
111 flash[:error] = l(:notice_locking_conflict)
112 end
112 end
113 end
113 end
114 end
114 end
115
115
116 def add_note
116 def add_note
117 journal = @issue.init_journal(User.current, params[:notes])
117 journal = @issue.init_journal(User.current, params[:notes])
118 params[:attachments].each { |file|
118 params[:attachments].each { |file|
119 next unless file.size > 0
119 next unless file.size > 0
120 a = Attachment.create(:container => @issue, :file => file, :author => User.current)
120 a = Attachment.create(:container => @issue, :file => file, :author => User.current)
121 journal.details << JournalDetail.new(:property => 'attachment',
121 journal.details << JournalDetail.new(:property => 'attachment',
122 :prop_key => a.id,
122 :prop_key => a.id,
123 :value => a.filename) unless a.new_record?
123 :value => a.filename) unless a.new_record?
124 } if params[:attachments] and params[:attachments].is_a? Array
124 } if params[:attachments] and params[:attachments].is_a? Array
125 if journal.save
125 if journal.save
126 flash[:notice] = l(:notice_successful_update)
126 flash[:notice] = l(:notice_successful_update)
127 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
127 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
128 redirect_to :action => 'show', :id => @issue
128 redirect_to :action => 'show', :id => @issue
129 return
129 return
130 end
130 end
131 show
131 show
132 end
132 end
133
133
134 def change_status
134 def change_status
135 @status_options = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
135 @status_options = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
136 @new_status = IssueStatus.find(params[:new_status_id])
136 @new_status = IssueStatus.find(params[:new_status_id])
137 if params[:confirm]
137 if params[:confirm]
138 begin
138 begin
139 journal = @issue.init_journal(User.current, params[:notes])
139 journal = @issue.init_journal(User.current, params[:notes])
140 @issue.status = @new_status
140 @issue.status = @new_status
141 if @issue.update_attributes(params[:issue])
141 if @issue.update_attributes(params[:issue])
142 # Save attachments
142 # Save attachments
143 params[:attachments].each { |file|
143 params[:attachments].each { |file|
144 next unless file.size > 0
144 next unless file.size > 0
145 a = Attachment.create(:container => @issue, :file => file, :author => User.current)
145 a = Attachment.create(:container => @issue, :file => file, :author => User.current)
146 journal.details << JournalDetail.new(:property => 'attachment',
146 journal.details << JournalDetail.new(:property => 'attachment',
147 :prop_key => a.id,
147 :prop_key => a.id,
148 :value => a.filename) unless a.new_record?
148 :value => a.filename) unless a.new_record?
149 } if params[:attachments] and params[:attachments].is_a? Array
149 } if params[:attachments] and params[:attachments].is_a? Array
150
150
151 # Log time
151 # Log time
152 if current_role.allowed_to?(:log_time)
152 if current_role.allowed_to?(:log_time)
153 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
153 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
154 @time_entry.attributes = params[:time_entry]
154 @time_entry.attributes = params[:time_entry]
155 @time_entry.save
155 @time_entry.save
156 end
156 end
157
157
158 flash[:notice] = l(:notice_successful_update)
158 flash[:notice] = l(:notice_successful_update)
159 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
159 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
160 redirect_to :action => 'show', :id => @issue
160 redirect_to :action => 'show', :id => @issue
161 end
161 end
162 rescue ActiveRecord::StaleObjectError
162 rescue ActiveRecord::StaleObjectError
163 # Optimistic locking exception
163 # Optimistic locking exception
164 flash[:error] = l(:notice_locking_conflict)
164 flash[:error] = l(:notice_locking_conflict)
165 end
165 end
166 end
166 end
167 @assignable_to = @project.members.find(:all, :include => :user).collect{ |m| m.user }
167 @assignable_to = @project.members.find(:all, :include => :user).collect{ |m| m.user }
168 @activities = Enumeration::get_values('ACTI')
168 @activities = Enumeration::get_values('ACTI')
169 end
169 end
170
170
171 def destroy
171 def destroy
172 @issue.destroy
172 @issue.destroy
173 redirect_to :action => 'index', :project_id => @project
173 redirect_to :action => 'index', :project_id => @project
174 end
174 end
175
175
176 def destroy_attachment
176 def destroy_attachment
177 a = @issue.attachments.find(params[:attachment_id])
177 a = @issue.attachments.find(params[:attachment_id])
178 a.destroy
178 a.destroy
179 journal = @issue.init_journal(User.current)
179 journal = @issue.init_journal(User.current)
180 journal.details << JournalDetail.new(:property => 'attachment',
180 journal.details << JournalDetail.new(:property => 'attachment',
181 :prop_key => a.id,
181 :prop_key => a.id,
182 :old_value => a.filename)
182 :old_value => a.filename)
183 journal.save
183 journal.save
184 redirect_to :action => 'show', :id => @issue
184 redirect_to :action => 'show', :id => @issue
185 end
185 end
186
186
187 def context_menu
187 def context_menu
188 @priorities = Enumeration.get_values('IPRI').reverse
188 @priorities = Enumeration.get_values('IPRI').reverse
189 @statuses = IssueStatus.find(:all, :order => 'position')
189 @statuses = IssueStatus.find(:all, :order => 'position')
190 @allowed_statuses = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
190 @allowed_statuses = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
191 @assignables = @issue.assignable_users
191 @assignables = @issue.assignable_users
192 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
192 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
193 @can = {:edit => User.current.allowed_to?(:edit_issues, @project),
193 @can = {:edit => User.current.allowed_to?(:edit_issues, @project),
194 :change_status => User.current.allowed_to?(:change_issue_status, @project),
194 :change_status => User.current.allowed_to?(:change_issue_status, @project),
195 :add => User.current.allowed_to?(:add_issues, @project),
195 :add => User.current.allowed_to?(:add_issues, @project),
196 :move => User.current.allowed_to?(:move_issues, @project),
196 :move => User.current.allowed_to?(:move_issues, @project),
197 :copy => (@project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
197 :delete => User.current.allowed_to?(:delete_issues, @project)}
198 :delete => User.current.allowed_to?(:delete_issues, @project)}
198 render :layout => false
199 render :layout => false
199 end
200 end
200
201
201 def preview
202 def preview
202 issue = Issue.find_by_id(params[:id])
203 issue = Issue.find_by_id(params[:id])
203 @attachements = issue.attachments if issue
204 @attachements = issue.attachments if issue
204 @text = params[:issue][:description]
205 @text = params[:issue][:description]
205 render :partial => 'common/preview'
206 render :partial => 'common/preview'
206 end
207 end
207
208
208 private
209 private
209 def find_project
210 def find_project
210 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
211 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
211 @project = @issue.project
212 @project = @issue.project
212 rescue ActiveRecord::RecordNotFound
213 rescue ActiveRecord::RecordNotFound
213 render_404
214 render_404
214 end
215 end
215
216
216 def find_optional_project
217 def find_optional_project
217 return true unless params[:project_id]
218 return true unless params[:project_id]
218 @project = Project.find(params[:project_id])
219 @project = Project.find(params[:project_id])
219 authorize
220 authorize
220 rescue ActiveRecord::RecordNotFound
221 rescue ActiveRecord::RecordNotFound
221 render_404
222 render_404
222 end
223 end
223
224
224 # Retrieve query from session or build a new query
225 # Retrieve query from session or build a new query
225 def retrieve_query
226 def retrieve_query
226 if params[:query_id]
227 if params[:query_id]
227 @query = Query.find(params[:query_id], :conditions => {:project_id => (@project ? @project.id : nil)})
228 @query = Query.find(params[:query_id], :conditions => {:project_id => (@project ? @project.id : nil)})
228 session[:query] = @query
229 session[:query] = @query
229 else
230 else
230 if params[:set_filter] or !session[:query] or session[:query].project != @project
231 if params[:set_filter] or !session[:query] or session[:query].project != @project
231 # Give it a name, required to be valid
232 # Give it a name, required to be valid
232 @query = Query.new(:name => "_")
233 @query = Query.new(:name => "_")
233 @query.project = @project
234 @query.project = @project
234 if params[:fields] and params[:fields].is_a? Array
235 if params[:fields] and params[:fields].is_a? Array
235 params[:fields].each do |field|
236 params[:fields].each do |field|
236 @query.add_filter(field, params[:operators][field], params[:values][field])
237 @query.add_filter(field, params[:operators][field], params[:values][field])
237 end
238 end
238 else
239 else
239 @query.available_filters.keys.each do |field|
240 @query.available_filters.keys.each do |field|
240 @query.add_short_filter(field, params[field]) if params[field]
241 @query.add_short_filter(field, params[field]) if params[field]
241 end
242 end
242 end
243 end
243 session[:query] = @query
244 session[:query] = @query
244 else
245 else
245 @query = session[:query]
246 @query = session[:query]
246 end
247 end
247 end
248 end
248 end
249 end
249 end
250 end
@@ -1,550 +1,556
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 layout 'base'
19 layout 'base'
20 before_filter :find_project, :except => [ :index, :list, :add ]
20 before_filter :find_project, :except => [ :index, :list, :add ]
21 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
21 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
22 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
22 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
23 accept_key_auth :activity, :calendar
23 accept_key_auth :activity, :calendar
24
24
25 cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ]
25 cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ]
26 cache_sweeper :issue_sweeper, :only => [ :add_issue ]
26 cache_sweeper :issue_sweeper, :only => [ :add_issue ]
27 cache_sweeper :version_sweeper, :only => [ :add_version ]
27 cache_sweeper :version_sweeper, :only => [ :add_version ]
28
28
29 helper :sort
29 helper :sort
30 include SortHelper
30 include SortHelper
31 helper :custom_fields
31 helper :custom_fields
32 include CustomFieldsHelper
32 include CustomFieldsHelper
33 helper :ifpdf
33 helper :ifpdf
34 include IfpdfHelper
34 include IfpdfHelper
35 helper :issues
35 helper :issues
36 helper IssuesHelper
36 helper IssuesHelper
37 helper :queries
37 helper :queries
38 include QueriesHelper
38 include QueriesHelper
39 helper :repositories
39 helper :repositories
40 include RepositoriesHelper
40 include RepositoriesHelper
41 include ProjectsHelper
41 include ProjectsHelper
42
42
43 def index
43 def index
44 list
44 list
45 render :action => 'list' unless request.xhr?
45 render :action => 'list' unless request.xhr?
46 end
46 end
47
47
48 # Lists visible projects
48 # Lists visible projects
49 def list
49 def list
50 projects = Project.find :all,
50 projects = Project.find :all,
51 :conditions => Project.visible_by(User.current),
51 :conditions => Project.visible_by(User.current),
52 :include => :parent
52 :include => :parent
53 @project_tree = projects.group_by {|p| p.parent || p}
53 @project_tree = projects.group_by {|p| p.parent || p}
54 @project_tree.each_key {|p| @project_tree[p] -= [p]}
54 @project_tree.each_key {|p| @project_tree[p] -= [p]}
55 end
55 end
56
56
57 # Add a new project
57 # Add a new project
58 def add
58 def add
59 @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
59 @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
60 @trackers = Tracker.all
60 @root_projects = Project.find(:all, :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}")
61 @root_projects = Project.find(:all, :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}")
61 @project = Project.new(params[:project])
62 @project = Project.new(params[:project])
62 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
63 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
63 if request.get?
64 if request.get?
64 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
65 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
66 @project.trackers = Tracker.all
65 else
67 else
66 @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
68 @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
67 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
69 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
68 @project.custom_values = @custom_values
70 @project.custom_values = @custom_values
69 if @project.save
71 if @project.save
70 @project.enabled_module_names = params[:enabled_modules]
72 @project.enabled_module_names = params[:enabled_modules]
71 flash[:notice] = l(:notice_successful_create)
73 flash[:notice] = l(:notice_successful_create)
72 redirect_to :controller => 'admin', :action => 'projects'
74 redirect_to :controller => 'admin', :action => 'projects'
73 end
75 end
74 end
76 end
75 end
77 end
76
78
77 # Show @project
79 # Show @project
78 def show
80 def show
79 @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
81 @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
80 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
82 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
81 @subprojects = @project.active_children
83 @subprojects = @project.active_children
82 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
84 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
83 @trackers = Tracker.find(:all, :order => 'position')
85 @trackers = @project.trackers
84 @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false])
86 @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false])
85 @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
87 @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
86 @total_hours = @project.time_entries.sum(:hours)
88 @total_hours = @project.time_entries.sum(:hours)
87 @key = User.current.rss_key
89 @key = User.current.rss_key
88 end
90 end
89
91
90 def settings
92 def settings
91 @root_projects = Project::find(:all, :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id])
93 @root_projects = Project::find(:all, :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id])
92 @custom_fields = IssueCustomField.find(:all)
94 @custom_fields = IssueCustomField.find(:all)
93 @issue_category ||= IssueCategory.new
95 @issue_category ||= IssueCategory.new
94 @member ||= @project.members.new
96 @member ||= @project.members.new
97 @trackers = Tracker.all
95 @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
98 @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
96 @repository ||= @project.repository
99 @repository ||= @project.repository
97 @wiki ||= @project.wiki
100 @wiki ||= @project.wiki
98 end
101 end
99
102
100 # Edit @project
103 # Edit @project
101 def edit
104 def edit
102 if request.post?
105 if request.post?
103 @project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
106 @project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
104 if params[:custom_fields]
107 if params[:custom_fields]
105 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
108 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
106 @project.custom_values = @custom_values
109 @project.custom_values = @custom_values
107 end
110 end
108 @project.attributes = params[:project]
111 @project.attributes = params[:project]
109 if @project.save
112 if @project.save
110 flash[:notice] = l(:notice_successful_update)
113 flash[:notice] = l(:notice_successful_update)
111 redirect_to :action => 'settings', :id => @project
114 redirect_to :action => 'settings', :id => @project
112 else
115 else
113 settings
116 settings
114 render :action => 'settings'
117 render :action => 'settings'
115 end
118 end
116 end
119 end
117 end
120 end
118
121
119 def modules
122 def modules
120 @project.enabled_module_names = params[:enabled_modules]
123 @project.enabled_module_names = params[:enabled_modules]
121 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
124 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
122 end
125 end
123
126
124 def archive
127 def archive
125 @project.archive if request.post? && @project.active?
128 @project.archive if request.post? && @project.active?
126 redirect_to :controller => 'admin', :action => 'projects'
129 redirect_to :controller => 'admin', :action => 'projects'
127 end
130 end
128
131
129 def unarchive
132 def unarchive
130 @project.unarchive if request.post? && !@project.active?
133 @project.unarchive if request.post? && !@project.active?
131 redirect_to :controller => 'admin', :action => 'projects'
134 redirect_to :controller => 'admin', :action => 'projects'
132 end
135 end
133
136
134 # Delete @project
137 # Delete @project
135 def destroy
138 def destroy
136 @project_to_destroy = @project
139 @project_to_destroy = @project
137 if request.post? and params[:confirm]
140 if request.post? and params[:confirm]
138 @project_to_destroy.destroy
141 @project_to_destroy.destroy
139 redirect_to :controller => 'admin', :action => 'projects'
142 redirect_to :controller => 'admin', :action => 'projects'
140 end
143 end
141 # hide project in layout
144 # hide project in layout
142 @project = nil
145 @project = nil
143 end
146 end
144
147
145 # Add a new issue category to @project
148 # Add a new issue category to @project
146 def add_issue_category
149 def add_issue_category
147 @category = @project.issue_categories.build(params[:category])
150 @category = @project.issue_categories.build(params[:category])
148 if request.post? and @category.save
151 if request.post? and @category.save
149 respond_to do |format|
152 respond_to do |format|
150 format.html do
153 format.html do
151 flash[:notice] = l(:notice_successful_create)
154 flash[:notice] = l(:notice_successful_create)
152 redirect_to :action => 'settings', :tab => 'categories', :id => @project
155 redirect_to :action => 'settings', :tab => 'categories', :id => @project
153 end
156 end
154 format.js do
157 format.js do
155 # IE doesn't support the replace_html rjs method for select box options
158 # IE doesn't support the replace_html rjs method for select box options
156 render(:update) {|page| page.replace "issue_category_id",
159 render(:update) {|page| page.replace "issue_category_id",
157 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
160 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
158 }
161 }
159 end
162 end
160 end
163 end
161 end
164 end
162 end
165 end
163
166
164 # Add a new version to @project
167 # Add a new version to @project
165 def add_version
168 def add_version
166 @version = @project.versions.build(params[:version])
169 @version = @project.versions.build(params[:version])
167 if request.post? and @version.save
170 if request.post? and @version.save
168 flash[:notice] = l(:notice_successful_create)
171 flash[:notice] = l(:notice_successful_create)
169 redirect_to :action => 'settings', :tab => 'versions', :id => @project
172 redirect_to :action => 'settings', :tab => 'versions', :id => @project
170 end
173 end
171 end
174 end
172
175
173 # Add a new document to @project
176 # Add a new document to @project
174 def add_document
177 def add_document
175 @document = @project.documents.build(params[:document])
178 @document = @project.documents.build(params[:document])
176 if request.post? and @document.save
179 if request.post? and @document.save
177 # Save the attachments
180 # Save the attachments
178 params[:attachments].each { |a|
181 params[:attachments].each { |a|
179 Attachment.create(:container => @document, :file => a, :author => User.current) unless a.size == 0
182 Attachment.create(:container => @document, :file => a, :author => User.current) unless a.size == 0
180 } if params[:attachments] and params[:attachments].is_a? Array
183 } if params[:attachments] and params[:attachments].is_a? Array
181 flash[:notice] = l(:notice_successful_create)
184 flash[:notice] = l(:notice_successful_create)
182 Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
185 Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
183 redirect_to :action => 'list_documents', :id => @project
186 redirect_to :action => 'list_documents', :id => @project
184 end
187 end
185 end
188 end
186
189
187 # Show documents list of @project
190 # Show documents list of @project
188 def list_documents
191 def list_documents
189 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
192 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
190 documents = @project.documents.find :all, :include => [:attachments, :category]
193 documents = @project.documents.find :all, :include => [:attachments, :category]
191 case @sort_by
194 case @sort_by
192 when 'date'
195 when 'date'
193 @grouped = documents.group_by {|d| d.created_on.to_date }
196 @grouped = documents.group_by {|d| d.created_on.to_date }
194 when 'title'
197 when 'title'
195 @grouped = documents.group_by {|d| d.title.first.upcase}
198 @grouped = documents.group_by {|d| d.title.first.upcase}
196 when 'author'
199 when 'author'
197 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
200 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
198 else
201 else
199 @grouped = documents.group_by(&:category)
202 @grouped = documents.group_by(&:category)
200 end
203 end
201 render :layout => false if request.xhr?
204 render :layout => false if request.xhr?
202 end
205 end
203
206
204 # Add a new issue to @project
207 # Add a new issue to @project
205 # The new issue will be created from an existing one if copy_from parameter is given
208 # The new issue will be created from an existing one if copy_from parameter is given
206 def add_issue
209 def add_issue
207 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
210 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
208 @issue.project = @project
211 @issue.project = @project
209 @issue.author = User.current
212 @issue.author = User.current
210 @issue.tracker ||= Tracker.find(params[:tracker_id])
213 @issue.tracker ||= @project.trackers.find(params[:tracker_id])
211
214
212 default_status = IssueStatus.default
215 default_status = IssueStatus.default
213 unless default_status
216 unless default_status
214 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
217 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
215 render :nothing => true, :layout => true
218 render :nothing => true, :layout => true
216 return
219 return
217 end
220 end
218 @issue.status = default_status
221 @issue.status = default_status
219 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker))
222 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker))
220
223
221 if request.get?
224 if request.get?
222 @issue.start_date ||= Date.today
225 @issue.start_date ||= Date.today
223 @custom_values = @issue.custom_values.empty? ?
226 @custom_values = @issue.custom_values.empty? ?
224 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
227 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
225 @issue.custom_values
228 @issue.custom_values
226 else
229 else
227 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
230 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
228 # Check that the user is allowed to apply the requested status
231 # Check that the user is allowed to apply the requested status
229 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
232 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
230 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
233 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
231 @issue.custom_values = @custom_values
234 @issue.custom_values = @custom_values
232 if @issue.save
235 if @issue.save
233 if params[:attachments] && params[:attachments].is_a?(Array)
236 if params[:attachments] && params[:attachments].is_a?(Array)
234 # Save attachments
237 # Save attachments
235 params[:attachments].each {|a| Attachment.create(:container => @issue, :file => a, :author => User.current) unless a.size == 0}
238 params[:attachments].each {|a| Attachment.create(:container => @issue, :file => a, :author => User.current) unless a.size == 0}
236 end
239 end
237 flash[:notice] = l(:notice_successful_create)
240 flash[:notice] = l(:notice_successful_create)
238 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
241 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
239 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
242 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
240 return
243 return
241 end
244 end
242 end
245 end
243 @priorities = Enumeration::get_values('IPRI')
246 @priorities = Enumeration::get_values('IPRI')
244 end
247 end
245
248
246 # Bulk edit issues
249 # Bulk edit issues
247 def bulk_edit_issues
250 def bulk_edit_issues
248 if request.post?
251 if request.post?
249 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
252 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
250 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
253 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
251 assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id])
254 assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id])
252 category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id])
255 category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id])
253 fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id])
256 fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id])
254 issues = @project.issues.find_all_by_id(params[:issue_ids])
257 issues = @project.issues.find_all_by_id(params[:issue_ids])
255 unsaved_issue_ids = []
258 unsaved_issue_ids = []
256 issues.each do |issue|
259 issues.each do |issue|
257 journal = issue.init_journal(User.current, params[:notes])
260 journal = issue.init_journal(User.current, params[:notes])
258 issue.priority = priority if priority
261 issue.priority = priority if priority
259 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
262 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
260 issue.category = category if category
263 issue.category = category if category
261 issue.fixed_version = fixed_version if fixed_version
264 issue.fixed_version = fixed_version if fixed_version
262 issue.start_date = params[:start_date] unless params[:start_date].blank?
265 issue.start_date = params[:start_date] unless params[:start_date].blank?
263 issue.due_date = params[:due_date] unless params[:due_date].blank?
266 issue.due_date = params[:due_date] unless params[:due_date].blank?
264 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
267 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
265 # Don't save any change to the issue if the user is not authorized to apply the requested status
268 # Don't save any change to the issue if the user is not authorized to apply the requested status
266 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
269 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
267 # Send notification for each issue (if changed)
270 # Send notification for each issue (if changed)
268 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
271 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
269 else
272 else
270 # Keep unsaved issue ids to display them in flash error
273 # Keep unsaved issue ids to display them in flash error
271 unsaved_issue_ids << issue.id
274 unsaved_issue_ids << issue.id
272 end
275 end
273 end
276 end
274 if unsaved_issue_ids.empty?
277 if unsaved_issue_ids.empty?
275 flash[:notice] = l(:notice_successful_update) unless issues.empty?
278 flash[:notice] = l(:notice_successful_update) unless issues.empty?
276 else
279 else
277 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #'))
280 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #'))
278 end
281 end
279 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
282 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
280 return
283 return
281 end
284 end
282 if current_role && User.current.allowed_to?(:change_issue_status, @project)
285 if current_role && User.current.allowed_to?(:change_issue_status, @project)
283 # Find potential statuses the user could be allowed to switch issues to
286 # Find potential statuses the user could be allowed to switch issues to
284 @available_statuses = Workflow.find(:all, :include => :new_status,
287 @available_statuses = Workflow.find(:all, :include => :new_status,
285 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
288 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
286 end
289 end
287 render :update do |page|
290 render :update do |page|
288 page.hide 'query_form'
291 page.hide 'query_form'
289 page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form'
292 page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form'
290 end
293 end
291 end
294 end
292
295
293 def move_issues
296 def move_issues
294 @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
297 @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
295 redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues
298 redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues
299
296 @projects = []
300 @projects = []
297 # find projects to which the user is allowed to move the issue
301 # find projects to which the user is allowed to move the issue
298 if User.current.admin?
302 if User.current.admin?
299 # admin is allowed to move issues to any active (visible) project
303 # admin is allowed to move issues to any active (visible) project
300 @projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
304 @projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
301 else
305 else
302 User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)}
306 User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)}
303 end
307 end
304 # issue can be moved to any tracker
308 @target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
305 @trackers = Tracker.find(:all)
309 @target_project ||= @project
306 if request.post? && params[:new_project_id] && @projects.collect(&:id).include?(params[:new_project_id].to_i) && params[:new_tracker_id]
310 @trackers = @target_project.trackers
307 new_project = Project.find_by_id(params[:new_project_id])
311 if request.post?
308 new_tracker = params[:new_tracker_id].blank? ? nil : Tracker.find_by_id(params[:new_tracker_id])
312 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
309 unsaved_issue_ids = []
313 unsaved_issue_ids = []
310 @issues.each do |issue|
314 @issues.each do |issue|
311 unsaved_issue_ids << issue.id unless issue.move_to(new_project, new_tracker)
315 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
312 end
316 end
313 if unsaved_issue_ids.empty?
317 if unsaved_issue_ids.empty?
314 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
318 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
315 else
319 else
316 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
320 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
317 end
321 end
318 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
322 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
323 return
319 end
324 end
325 render :layout => false if request.xhr?
320 end
326 end
321
327
322 # Add a news to @project
328 # Add a news to @project
323 def add_news
329 def add_news
324 @news = News.new(:project => @project, :author => User.current)
330 @news = News.new(:project => @project, :author => User.current)
325 if request.post?
331 if request.post?
326 @news.attributes = params[:news]
332 @news.attributes = params[:news]
327 if @news.save
333 if @news.save
328 flash[:notice] = l(:notice_successful_create)
334 flash[:notice] = l(:notice_successful_create)
329 Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
335 Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
330 redirect_to :controller => 'news', :action => 'index', :project_id => @project
336 redirect_to :controller => 'news', :action => 'index', :project_id => @project
331 end
337 end
332 end
338 end
333 end
339 end
334
340
335 def add_file
341 def add_file
336 if request.post?
342 if request.post?
337 @version = @project.versions.find_by_id(params[:version_id])
343 @version = @project.versions.find_by_id(params[:version_id])
338 # Save the attachments
344 # Save the attachments
339 @attachments = []
345 @attachments = []
340 params[:attachments].each { |file|
346 params[:attachments].each { |file|
341 next unless file.size > 0
347 next unless file.size > 0
342 a = Attachment.create(:container => @version, :file => file, :author => User.current)
348 a = Attachment.create(:container => @version, :file => file, :author => User.current)
343 @attachments << a unless a.new_record?
349 @attachments << a unless a.new_record?
344 } if params[:attachments] and params[:attachments].is_a? Array
350 } if params[:attachments] and params[:attachments].is_a? Array
345 Mailer.deliver_attachments_added(@attachments) if !@attachments.empty? && Setting.notified_events.include?('file_added')
351 Mailer.deliver_attachments_added(@attachments) if !@attachments.empty? && Setting.notified_events.include?('file_added')
346 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
352 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
347 end
353 end
348 @versions = @project.versions.sort
354 @versions = @project.versions.sort
349 end
355 end
350
356
351 def list_files
357 def list_files
352 @versions = @project.versions.sort
358 @versions = @project.versions.sort
353 end
359 end
354
360
355 # Show changelog for @project
361 # Show changelog for @project
356 def changelog
362 def changelog
357 @trackers = Tracker.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
363 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
358 retrieve_selected_tracker_ids(@trackers)
364 retrieve_selected_tracker_ids(@trackers)
359 @versions = @project.versions.sort
365 @versions = @project.versions.sort
360 end
366 end
361
367
362 def roadmap
368 def roadmap
363 @trackers = Tracker.find(:all, :conditions => ["is_in_roadmap=?", true], :order => 'position')
369 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
364 retrieve_selected_tracker_ids(@trackers)
370 retrieve_selected_tracker_ids(@trackers)
365 @versions = @project.versions.sort
371 @versions = @project.versions.sort
366 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
372 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
367 end
373 end
368
374
369 def activity
375 def activity
370 if params[:year] and params[:year].to_i > 1900
376 if params[:year] and params[:year].to_i > 1900
371 @year = params[:year].to_i
377 @year = params[:year].to_i
372 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
378 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
373 @month = params[:month].to_i
379 @month = params[:month].to_i
374 end
380 end
375 end
381 end
376 @year ||= Date.today.year
382 @year ||= Date.today.year
377 @month ||= Date.today.month
383 @month ||= Date.today.month
378
384
379 case params[:format]
385 case params[:format]
380 when 'atom'
386 when 'atom'
381 # 30 last days
387 # 30 last days
382 @date_from = Date.today - 30
388 @date_from = Date.today - 30
383 @date_to = Date.today + 1
389 @date_to = Date.today + 1
384 else
390 else
385 # current month
391 # current month
386 @date_from = Date.civil(@year, @month, 1)
392 @date_from = Date.civil(@year, @month, 1)
387 @date_to = @date_from >> 1
393 @date_to = @date_from >> 1
388 end
394 end
389
395
390 @event_types = %w(issues news files documents changesets wiki_pages messages)
396 @event_types = %w(issues news files documents changesets wiki_pages messages)
391 @event_types.delete('wiki_pages') unless @project.wiki
397 @event_types.delete('wiki_pages') unless @project.wiki
392 @event_types.delete('changesets') unless @project.repository
398 @event_types.delete('changesets') unless @project.repository
393 @event_types.delete('messages') unless @project.boards.any?
399 @event_types.delete('messages') unless @project.boards.any?
394 # only show what the user is allowed to view
400 # only show what the user is allowed to view
395 @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
401 @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
396
402
397 @scope = @event_types.select {|t| params["show_#{t}"]}
403 @scope = @event_types.select {|t| params["show_#{t}"]}
398 # default events if none is specified in parameters
404 # default events if none is specified in parameters
399 @scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
405 @scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
400
406
401 @events = []
407 @events = []
402
408
403 if @scope.include?('issues')
409 if @scope.include?('issues')
404 @events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] )
410 @events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] )
405 @events += @project.issues_status_changes(@date_from, @date_to)
411 @events += @project.issues_status_changes(@date_from, @date_to)
406 end
412 end
407
413
408 if @scope.include?('news')
414 if @scope.include?('news')
409 @events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author )
415 @events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author )
410 end
416 end
411
417
412 if @scope.include?('files')
418 if @scope.include?('files')
413 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
419 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
414 end
420 end
415
421
416 if @scope.include?('documents')
422 if @scope.include?('documents')
417 @events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] )
423 @events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] )
418 @events += Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
424 @events += Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
419 end
425 end
420
426
421 if @scope.include?('wiki_pages')
427 if @scope.include?('wiki_pages')
422 select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
428 select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
423 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
429 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
424 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
430 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
425 "#{WikiContent.versioned_table_name}.id"
431 "#{WikiContent.versioned_table_name}.id"
426 joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
432 joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
427 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
433 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
428 conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
434 conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
429 @project.id, @date_from, @date_to]
435 @project.id, @date_from, @date_to]
430
436
431 @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions)
437 @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions)
432 end
438 end
433
439
434 if @scope.include?('changesets')
440 if @scope.include?('changesets')
435 @events += @project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to])
441 @events += @project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to])
436 end
442 end
437
443
438 if @scope.include?('messages')
444 if @scope.include?('messages')
439 @events += Message.find(:all,
445 @events += Message.find(:all,
440 :include => [:board, :author],
446 :include => [:board, :author],
441 :conditions => ["#{Board.table_name}.project_id=? AND #{Message.table_name}.parent_id IS NULL AND #{Message.table_name}.created_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
447 :conditions => ["#{Board.table_name}.project_id=? AND #{Message.table_name}.parent_id IS NULL AND #{Message.table_name}.created_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
442 end
448 end
443
449
444 @events_by_day = @events.group_by(&:event_date)
450 @events_by_day = @events.group_by(&:event_date)
445
451
446 respond_to do |format|
452 respond_to do |format|
447 format.html { render :layout => false if request.xhr? }
453 format.html { render :layout => false if request.xhr? }
448 format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") }
454 format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") }
449 end
455 end
450 end
456 end
451
457
452 def calendar
458 def calendar
453 @trackers = Tracker.find(:all, :order => 'position')
459 @trackers = Tracker.find(:all, :order => 'position')
454 retrieve_selected_tracker_ids(@trackers)
460 retrieve_selected_tracker_ids(@trackers)
455
461
456 if params[:year] and params[:year].to_i > 1900
462 if params[:year] and params[:year].to_i > 1900
457 @year = params[:year].to_i
463 @year = params[:year].to_i
458 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
464 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
459 @month = params[:month].to_i
465 @month = params[:month].to_i
460 end
466 end
461 end
467 end
462 @year ||= Date.today.year
468 @year ||= Date.today.year
463 @month ||= Date.today.month
469 @month ||= Date.today.month
464 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
470 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
465
471
466 events = []
472 events = []
467 @project.issues_with_subprojects(params[:with_subprojects]) do
473 @project.issues_with_subprojects(params[:with_subprojects]) do
468 events += Issue.find(:all,
474 events += Issue.find(:all,
469 :include => [:tracker, :status, :assigned_to, :priority, :project],
475 :include => [:tracker, :status, :assigned_to, :priority, :project],
470 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
476 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
471 ) unless @selected_tracker_ids.empty?
477 ) unless @selected_tracker_ids.empty?
472 end
478 end
473 events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
479 events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
474 @calendar.events = events
480 @calendar.events = events
475
481
476 render :layout => false if request.xhr?
482 render :layout => false if request.xhr?
477 end
483 end
478
484
479 def gantt
485 def gantt
480 @trackers = Tracker.find(:all, :order => 'position')
486 @trackers = Tracker.find(:all, :order => 'position')
481 retrieve_selected_tracker_ids(@trackers)
487 retrieve_selected_tracker_ids(@trackers)
482
488
483 if params[:year] and params[:year].to_i >0
489 if params[:year] and params[:year].to_i >0
484 @year_from = params[:year].to_i
490 @year_from = params[:year].to_i
485 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
491 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
486 @month_from = params[:month].to_i
492 @month_from = params[:month].to_i
487 else
493 else
488 @month_from = 1
494 @month_from = 1
489 end
495 end
490 else
496 else
491 @month_from ||= Date.today.month
497 @month_from ||= Date.today.month
492 @year_from ||= Date.today.year
498 @year_from ||= Date.today.year
493 end
499 end
494
500
495 zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
501 zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
496 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
502 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
497 months = (params[:months] || User.current.pref[:gantt_months]).to_i
503 months = (params[:months] || User.current.pref[:gantt_months]).to_i
498 @months = (months > 0 && months < 25) ? months : 6
504 @months = (months > 0 && months < 25) ? months : 6
499
505
500 # Save gantt paramters as user preference (zoom and months count)
506 # Save gantt paramters as user preference (zoom and months count)
501 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
507 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
502 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
508 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
503 User.current.preference.save
509 User.current.preference.save
504 end
510 end
505
511
506 @date_from = Date.civil(@year_from, @month_from, 1)
512 @date_from = Date.civil(@year_from, @month_from, 1)
507 @date_to = (@date_from >> @months) - 1
513 @date_to = (@date_from >> @months) - 1
508
514
509 @events = []
515 @events = []
510 @project.issues_with_subprojects(params[:with_subprojects]) do
516 @project.issues_with_subprojects(params[:with_subprojects]) do
511 @events += Issue.find(:all,
517 @events += Issue.find(:all,
512 :order => "start_date, due_date",
518 :order => "start_date, due_date",
513 :include => [:tracker, :status, :assigned_to, :priority, :project],
519 :include => [:tracker, :status, :assigned_to, :priority, :project],
514 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
520 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
515 ) unless @selected_tracker_ids.empty?
521 ) unless @selected_tracker_ids.empty?
516 end
522 end
517 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
523 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
518 @events.sort! {|x,y| x.start_date <=> y.start_date }
524 @events.sort! {|x,y| x.start_date <=> y.start_date }
519
525
520 if params[:format]=='pdf'
526 if params[:format]=='pdf'
521 @options_for_rfpdf ||= {}
527 @options_for_rfpdf ||= {}
522 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
528 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
523 render :template => "projects/gantt.rfpdf", :layout => false
529 render :template => "projects/gantt.rfpdf", :layout => false
524 elsif params[:format]=='png' && respond_to?('gantt_image')
530 elsif params[:format]=='png' && respond_to?('gantt_image')
525 image = gantt_image(@events, @date_from, @months, @zoom)
531 image = gantt_image(@events, @date_from, @months, @zoom)
526 image.format = 'PNG'
532 image.format = 'PNG'
527 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
533 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
528 else
534 else
529 render :template => "projects/gantt.rhtml"
535 render :template => "projects/gantt.rhtml"
530 end
536 end
531 end
537 end
532
538
533 private
539 private
534 # Find project of id params[:id]
540 # Find project of id params[:id]
535 # if not found, redirect to project list
541 # if not found, redirect to project list
536 # Used as a before_filter
542 # Used as a before_filter
537 def find_project
543 def find_project
538 @project = Project.find(params[:id])
544 @project = Project.find(params[:id])
539 rescue ActiveRecord::RecordNotFound
545 rescue ActiveRecord::RecordNotFound
540 render_404
546 render_404
541 end
547 end
542
548
543 def retrieve_selected_tracker_ids(selectable_trackers)
549 def retrieve_selected_tracker_ids(selectable_trackers)
544 if ids = params[:tracker_ids]
550 if ids = params[:tracker_ids]
545 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
551 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
546 else
552 else
547 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
553 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
548 end
554 end
549 end
555 end
550 end
556 end
@@ -1,213 +1,213
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 ReportsController < ApplicationController
18 class ReportsController < ApplicationController
19 layout 'base'
19 layout 'base'
20 before_filter :find_project, :authorize
20 before_filter :find_project, :authorize
21
21
22 def issue_report
22 def issue_report
23 @statuses = IssueStatus.find(:all, :order => 'position')
23 @statuses = IssueStatus.find(:all, :order => 'position')
24
24
25 case params[:detail]
25 case params[:detail]
26 when "tracker"
26 when "tracker"
27 @field = "tracker_id"
27 @field = "tracker_id"
28 @rows = Tracker.find :all, :order => 'position'
28 @rows = @project.trackers
29 @data = issues_by_tracker
29 @data = issues_by_tracker
30 @report_title = l(:field_tracker)
30 @report_title = l(:field_tracker)
31 render :template => "reports/issue_report_details"
31 render :template => "reports/issue_report_details"
32 when "version"
32 when "version"
33 @field = "fixed_version_id"
33 @field = "fixed_version_id"
34 @rows = @project.versions.sort
34 @rows = @project.versions.sort
35 @data = issues_by_version
35 @data = issues_by_version
36 @report_title = l(:field_version)
36 @report_title = l(:field_version)
37 render :template => "reports/issue_report_details"
37 render :template => "reports/issue_report_details"
38 when "priority"
38 when "priority"
39 @field = "priority_id"
39 @field = "priority_id"
40 @rows = Enumeration::get_values('IPRI')
40 @rows = Enumeration::get_values('IPRI')
41 @data = issues_by_priority
41 @data = issues_by_priority
42 @report_title = l(:field_priority)
42 @report_title = l(:field_priority)
43 render :template => "reports/issue_report_details"
43 render :template => "reports/issue_report_details"
44 when "category"
44 when "category"
45 @field = "category_id"
45 @field = "category_id"
46 @rows = @project.issue_categories
46 @rows = @project.issue_categories
47 @data = issues_by_category
47 @data = issues_by_category
48 @report_title = l(:field_category)
48 @report_title = l(:field_category)
49 render :template => "reports/issue_report_details"
49 render :template => "reports/issue_report_details"
50 when "author"
50 when "author"
51 @field = "author_id"
51 @field = "author_id"
52 @rows = @project.members.collect { |m| m.user }
52 @rows = @project.members.collect { |m| m.user }
53 @data = issues_by_author
53 @data = issues_by_author
54 @report_title = l(:field_author)
54 @report_title = l(:field_author)
55 render :template => "reports/issue_report_details"
55 render :template => "reports/issue_report_details"
56 when "subproject"
56 when "subproject"
57 @field = "project_id"
57 @field = "project_id"
58 @rows = @project.active_children
58 @rows = @project.active_children
59 @data = issues_by_subproject
59 @data = issues_by_subproject
60 @report_title = l(:field_subproject)
60 @report_title = l(:field_subproject)
61 render :template => "reports/issue_report_details"
61 render :template => "reports/issue_report_details"
62 else
62 else
63 @trackers = Tracker.find(:all, :order => 'position')
63 @trackers = @project.trackers
64 @versions = @project.versions.sort
64 @versions = @project.versions.sort
65 @priorities = Enumeration::get_values('IPRI')
65 @priorities = Enumeration::get_values('IPRI')
66 @categories = @project.issue_categories
66 @categories = @project.issue_categories
67 @authors = @project.members.collect { |m| m.user }
67 @authors = @project.members.collect { |m| m.user }
68 @subprojects = @project.active_children
68 @subprojects = @project.active_children
69 issues_by_tracker
69 issues_by_tracker
70 issues_by_version
70 issues_by_version
71 issues_by_priority
71 issues_by_priority
72 issues_by_category
72 issues_by_category
73 issues_by_author
73 issues_by_author
74 issues_by_subproject
74 issues_by_subproject
75
75
76 render :template => "reports/issue_report"
76 render :template => "reports/issue_report"
77 end
77 end
78 end
78 end
79
79
80 def delays
80 def delays
81 @trackers = Tracker.find(:all)
81 @trackers = Tracker.find(:all)
82 if request.get?
82 if request.get?
83 @selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
83 @selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
84 else
84 else
85 @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
85 @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
86 end
86 end
87 @selected_tracker_ids ||= []
87 @selected_tracker_ids ||= []
88 @raw =
88 @raw =
89 ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
89 ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
90 FROM issue_histories a, issue_histories b, issues i
90 FROM issue_histories a, issue_histories b, issues i
91 WHERE a.status_id =5
91 WHERE a.status_id =5
92 AND a.issue_id = b.issue_id
92 AND a.issue_id = b.issue_id
93 AND a.issue_id = i.id
93 AND a.issue_id = i.id
94 AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
94 AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
95 AND b.id = (
95 AND b.id = (
96 SELECT min( c.id )
96 SELECT min( c.id )
97 FROM issue_histories c
97 FROM issue_histories c
98 WHERE b.issue_id = c.issue_id )
98 WHERE b.issue_id = c.issue_id )
99 GROUP BY delay") unless @selected_tracker_ids.empty?
99 GROUP BY delay") unless @selected_tracker_ids.empty?
100 @raw ||=[]
100 @raw ||=[]
101
101
102 @x_from = 0
102 @x_from = 0
103 @x_to = 0
103 @x_to = 0
104 @y_from = 0
104 @y_from = 0
105 @y_to = 0
105 @y_to = 0
106 @sum_total = 0
106 @sum_total = 0
107 @sum_delay = 0
107 @sum_delay = 0
108 @raw.each do |r|
108 @raw.each do |r|
109 @x_to = [r['delay'].to_i, @x_to].max
109 @x_to = [r['delay'].to_i, @x_to].max
110 @y_to = [r['total'].to_i, @y_to].max
110 @y_to = [r['total'].to_i, @y_to].max
111 @sum_total = @sum_total + r['total'].to_i
111 @sum_total = @sum_total + r['total'].to_i
112 @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
112 @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
113 end
113 end
114 end
114 end
115
115
116 private
116 private
117 # Find project of id params[:id]
117 # Find project of id params[:id]
118 def find_project
118 def find_project
119 @project = Project.find(params[:id])
119 @project = Project.find(params[:id])
120 rescue ActiveRecord::RecordNotFound
120 rescue ActiveRecord::RecordNotFound
121 render_404
121 render_404
122 end
122 end
123
123
124 def issues_by_tracker
124 def issues_by_tracker
125 @issues_by_tracker ||=
125 @issues_by_tracker ||=
126 ActiveRecord::Base.connection.select_all("select s.id as status_id,
126 ActiveRecord::Base.connection.select_all("select s.id as status_id,
127 s.is_closed as closed,
127 s.is_closed as closed,
128 t.id as tracker_id,
128 t.id as tracker_id,
129 count(i.id) as total
129 count(i.id) as total
130 from
130 from
131 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
131 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
132 where
132 where
133 i.status_id=s.id
133 i.status_id=s.id
134 and i.tracker_id=t.id
134 and i.tracker_id=t.id
135 and i.project_id=#{@project.id}
135 and i.project_id=#{@project.id}
136 group by s.id, s.is_closed, t.id")
136 group by s.id, s.is_closed, t.id")
137 end
137 end
138
138
139 def issues_by_version
139 def issues_by_version
140 @issues_by_version ||=
140 @issues_by_version ||=
141 ActiveRecord::Base.connection.select_all("select s.id as status_id,
141 ActiveRecord::Base.connection.select_all("select s.id as status_id,
142 s.is_closed as closed,
142 s.is_closed as closed,
143 v.id as fixed_version_id,
143 v.id as fixed_version_id,
144 count(i.id) as total
144 count(i.id) as total
145 from
145 from
146 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
146 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
147 where
147 where
148 i.status_id=s.id
148 i.status_id=s.id
149 and i.fixed_version_id=v.id
149 and i.fixed_version_id=v.id
150 and i.project_id=#{@project.id}
150 and i.project_id=#{@project.id}
151 group by s.id, s.is_closed, v.id")
151 group by s.id, s.is_closed, v.id")
152 end
152 end
153
153
154 def issues_by_priority
154 def issues_by_priority
155 @issues_by_priority ||=
155 @issues_by_priority ||=
156 ActiveRecord::Base.connection.select_all("select s.id as status_id,
156 ActiveRecord::Base.connection.select_all("select s.id as status_id,
157 s.is_closed as closed,
157 s.is_closed as closed,
158 p.id as priority_id,
158 p.id as priority_id,
159 count(i.id) as total
159 count(i.id) as total
160 from
160 from
161 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
161 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
162 where
162 where
163 i.status_id=s.id
163 i.status_id=s.id
164 and i.priority_id=p.id
164 and i.priority_id=p.id
165 and i.project_id=#{@project.id}
165 and i.project_id=#{@project.id}
166 group by s.id, s.is_closed, p.id")
166 group by s.id, s.is_closed, p.id")
167 end
167 end
168
168
169 def issues_by_category
169 def issues_by_category
170 @issues_by_category ||=
170 @issues_by_category ||=
171 ActiveRecord::Base.connection.select_all("select s.id as status_id,
171 ActiveRecord::Base.connection.select_all("select s.id as status_id,
172 s.is_closed as closed,
172 s.is_closed as closed,
173 c.id as category_id,
173 c.id as category_id,
174 count(i.id) as total
174 count(i.id) as total
175 from
175 from
176 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
176 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
177 where
177 where
178 i.status_id=s.id
178 i.status_id=s.id
179 and i.category_id=c.id
179 and i.category_id=c.id
180 and i.project_id=#{@project.id}
180 and i.project_id=#{@project.id}
181 group by s.id, s.is_closed, c.id")
181 group by s.id, s.is_closed, c.id")
182 end
182 end
183
183
184 def issues_by_author
184 def issues_by_author
185 @issues_by_author ||=
185 @issues_by_author ||=
186 ActiveRecord::Base.connection.select_all("select s.id as status_id,
186 ActiveRecord::Base.connection.select_all("select s.id as status_id,
187 s.is_closed as closed,
187 s.is_closed as closed,
188 a.id as author_id,
188 a.id as author_id,
189 count(i.id) as total
189 count(i.id) as total
190 from
190 from
191 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
191 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
192 where
192 where
193 i.status_id=s.id
193 i.status_id=s.id
194 and i.author_id=a.id
194 and i.author_id=a.id
195 and i.project_id=#{@project.id}
195 and i.project_id=#{@project.id}
196 group by s.id, s.is_closed, a.id")
196 group by s.id, s.is_closed, a.id")
197 end
197 end
198
198
199 def issues_by_subproject
199 def issues_by_subproject
200 @issues_by_subproject ||=
200 @issues_by_subproject ||=
201 ActiveRecord::Base.connection.select_all("select s.id as status_id,
201 ActiveRecord::Base.connection.select_all("select s.id as status_id,
202 s.is_closed as closed,
202 s.is_closed as closed,
203 i.project_id as project_id,
203 i.project_id as project_id,
204 count(i.id) as total
204 count(i.id) as total
205 from
205 from
206 #{Issue.table_name} i, #{IssueStatus.table_name} s
206 #{Issue.table_name} i, #{IssueStatus.table_name} s
207 where
207 where
208 i.status_id=s.id
208 i.status_id=s.id
209 and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')})
209 and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')})
210 group by s.id, s.is_closed, i.project_id") if @project.active_children.any?
210 group by s.id, s.is_closed, i.project_id") if @project.active_children.any?
211 @issues_by_subproject ||= []
211 @issues_by_subproject ||= []
212 end
212 end
213 end
213 end
@@ -1,199 +1,199
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 module ProjectsHelper
18 module ProjectsHelper
19 def link_to_version(version, options = {})
19 def link_to_version(version, options = {})
20 return '' unless version && version.is_a?(Version)
20 return '' unless version && version.is_a?(Version)
21 link_to version.name, {:controller => 'projects',
21 link_to version.name, {:controller => 'projects',
22 :action => 'roadmap',
22 :action => 'roadmap',
23 :id => version.project_id,
23 :id => version.project_id,
24 :completed => (version.completed? ? 1 : nil),
24 :completed => (version.completed? ? 1 : nil),
25 :anchor => version.name
25 :anchor => version.name
26 }, options
26 }, options
27 end
27 end
28
28
29 def project_settings_tabs
29 def project_settings_tabs
30 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
30 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
31 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
31 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
32 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
32 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
33 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
33 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
34 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
34 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
35 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
35 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
36 {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
36 {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
37 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
37 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
38 ]
38 ]
39 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
39 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
40 end
40 end
41
41
42 # Generates a gantt image
42 # Generates a gantt image
43 # Only defined if RMagick is avalaible
43 # Only defined if RMagick is avalaible
44 def gantt_image(events, date_from, months, zoom)
44 def gantt_image(events, date_from, months, zoom)
45 date_to = (date_from >> months)-1
45 date_to = (date_from >> months)-1
46 show_weeks = zoom > 1
46 show_weeks = zoom > 1
47 show_days = zoom > 2
47 show_days = zoom > 2
48
48
49 subject_width = 320
49 subject_width = 320
50 header_heigth = 18
50 header_heigth = 18
51 # width of one day in pixels
51 # width of one day in pixels
52 zoom = zoom*2
52 zoom = zoom*2
53 g_width = (date_to - date_from + 1)*zoom
53 g_width = (date_to - date_from + 1)*zoom
54 g_height = 20 * events.length + 20
54 g_height = 20 * events.length + 20
55 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
55 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
56 height = g_height + headers_heigth
56 height = g_height + headers_heigth
57
57
58 imgl = Magick::ImageList.new
58 imgl = Magick::ImageList.new
59 imgl.new_image(subject_width+g_width+1, height)
59 imgl.new_image(subject_width+g_width+1, height)
60 gc = Magick::Draw.new
60 gc = Magick::Draw.new
61
61
62 # Subjects
62 # Subjects
63 top = headers_heigth + 20
63 top = headers_heigth + 20
64 gc.fill('black')
64 gc.fill('black')
65 gc.stroke('transparent')
65 gc.stroke('transparent')
66 gc.stroke_width(1)
66 gc.stroke_width(1)
67 events.each do |i|
67 events.each do |i|
68 gc.text(4, top + 2, (i.is_a?(Issue) ? i.subject : i.name))
68 gc.text(4, top + 2, (i.is_a?(Issue) ? i.subject : i.name))
69 top = top + 20
69 top = top + 20
70 end
70 end
71
71
72 # Months headers
72 # Months headers
73 month_f = date_from
73 month_f = date_from
74 left = subject_width
74 left = subject_width
75 months.times do
75 months.times do
76 width = ((month_f >> 1) - month_f) * zoom
76 width = ((month_f >> 1) - month_f) * zoom
77 gc.fill('white')
77 gc.fill('white')
78 gc.stroke('grey')
78 gc.stroke('grey')
79 gc.stroke_width(1)
79 gc.stroke_width(1)
80 gc.rectangle(left, 0, left + width, height)
80 gc.rectangle(left, 0, left + width, height)
81 gc.fill('black')
81 gc.fill('black')
82 gc.stroke('transparent')
82 gc.stroke('transparent')
83 gc.stroke_width(1)
83 gc.stroke_width(1)
84 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
84 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
85 left = left + width
85 left = left + width
86 month_f = month_f >> 1
86 month_f = month_f >> 1
87 end
87 end
88
88
89 # Weeks headers
89 # Weeks headers
90 if show_weeks
90 if show_weeks
91 left = subject_width
91 left = subject_width
92 height = header_heigth
92 height = header_heigth
93 if date_from.cwday == 1
93 if date_from.cwday == 1
94 # date_from is monday
94 # date_from is monday
95 week_f = date_from
95 week_f = date_from
96 else
96 else
97 # find next monday after date_from
97 # find next monday after date_from
98 week_f = date_from + (7 - date_from.cwday + 1)
98 week_f = date_from + (7 - date_from.cwday + 1)
99 width = (7 - date_from.cwday + 1) * zoom
99 width = (7 - date_from.cwday + 1) * zoom
100 gc.fill('white')
100 gc.fill('white')
101 gc.stroke('grey')
101 gc.stroke('grey')
102 gc.stroke_width(1)
102 gc.stroke_width(1)
103 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
103 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
104 left = left + width
104 left = left + width
105 end
105 end
106 while week_f <= date_to
106 while week_f <= date_to
107 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
107 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
108 gc.fill('white')
108 gc.fill('white')
109 gc.stroke('grey')
109 gc.stroke('grey')
110 gc.stroke_width(1)
110 gc.stroke_width(1)
111 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
111 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
112 gc.fill('black')
112 gc.fill('black')
113 gc.stroke('transparent')
113 gc.stroke('transparent')
114 gc.stroke_width(1)
114 gc.stroke_width(1)
115 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
115 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
116 left = left + width
116 left = left + width
117 week_f = week_f+7
117 week_f = week_f+7
118 end
118 end
119 end
119 end
120
120
121 # Days details (week-end in grey)
121 # Days details (week-end in grey)
122 if show_days
122 if show_days
123 left = subject_width
123 left = subject_width
124 height = g_height + header_heigth - 1
124 height = g_height + header_heigth - 1
125 wday = date_from.cwday
125 wday = date_from.cwday
126 (date_to - date_from + 1).to_i.times do
126 (date_to - date_from + 1).to_i.times do
127 width = zoom
127 width = zoom
128 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
128 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
129 gc.stroke('grey')
129 gc.stroke('grey')
130 gc.stroke_width(1)
130 gc.stroke_width(1)
131 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
131 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
132 left = left + width
132 left = left + width
133 wday = wday + 1
133 wday = wday + 1
134 wday = 1 if wday > 7
134 wday = 1 if wday > 7
135 end
135 end
136 end
136 end
137
137
138 # border
138 # border
139 gc.fill('transparent')
139 gc.fill('transparent')
140 gc.stroke('grey')
140 gc.stroke('grey')
141 gc.stroke_width(1)
141 gc.stroke_width(1)
142 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
142 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
143 gc.stroke('black')
143 gc.stroke('black')
144 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
144 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
145
145
146 # content
146 # content
147 top = headers_heigth + 20
147 top = headers_heigth + 20
148 gc.stroke('transparent')
148 gc.stroke('transparent')
149 events.each do |i|
149 events.each do |i|
150 if i.is_a?(Issue)
150 if i.is_a?(Issue)
151 i_start_date = (i.start_date >= date_from ? i.start_date : date_from )
151 i_start_date = (i.start_date >= date_from ? i.start_date : date_from )
152 i_end_date = (i.due_date <= date_to ? i.due_date : date_to )
152 i_end_date = (i.due_date <= date_to ? i.due_date : date_to )
153 i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
153 i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
154 i_done_date = (i_done_date <= date_from ? date_from : i_done_date )
154 i_done_date = (i_done_date <= date_from ? date_from : i_done_date )
155 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
155 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
156 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
156 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
157
157
158 i_left = subject_width + ((i_start_date - date_from)*zoom).floor
158 i_left = subject_width + ((i_start_date - date_from)*zoom).floor
159 i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue
159 i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue
160 d_width = ((i_done_date - i_start_date)*zoom).floor # done width
160 d_width = ((i_done_date - i_start_date)*zoom).floor # done width
161 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width
161 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width
162
162
163 gc.fill('grey')
163 gc.fill('grey')
164 gc.rectangle(i_left, top, i_left + i_width, top - 6)
164 gc.rectangle(i_left, top, i_left + i_width, top - 6)
165 gc.fill('red')
165 gc.fill('red')
166 gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0
166 gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0
167 gc.fill('blue')
167 gc.fill('blue')
168 gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0
168 gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0
169 gc.fill('black')
169 gc.fill('black')
170 gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%")
170 gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%")
171 else
171 else
172 i_left = subject_width + ((i.start_date - date_from)*zoom).floor
172 i_left = subject_width + ((i.start_date - date_from)*zoom).floor
173 gc.fill('green')
173 gc.fill('green')
174 gc.rectangle(i_left, top, i_left + 6, top - 6)
174 gc.rectangle(i_left, top, i_left + 6, top - 6)
175 gc.fill('black')
175 gc.fill('black')
176 gc.text(i_left + 11, top + 1, i.name)
176 gc.text(i_left + 11, top + 1, i.name)
177 end
177 end
178 top = top + 20
178 top = top + 20
179 end
179 end
180
180
181 # today red line
181 # today red line
182 if Date.today >= @date_from and Date.today <= @date_to
182 if Date.today >= @date_from and Date.today <= @date_to
183 gc.stroke('red')
183 gc.stroke('red')
184 x = (Date.today-@date_from+1)*zoom + subject_width
184 x = (Date.today-@date_from+1)*zoom + subject_width
185 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
185 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
186 end
186 end
187
187
188 gc.draw(imgl)
188 gc.draw(imgl)
189 imgl
189 imgl
190 end if Object.const_defined?(:Magick)
190 end if Object.const_defined?(:Magick)
191
191
192 def new_issue_selector
192 def new_issue_selector
193 trackers = Tracker.find(:all, :order => 'position')
193 trackers = @project.trackers
194 # can't use form tag inside helper
194 # can't use form tag inside helper
195 content_tag('form',
195 content_tag('form',
196 select_tag('tracker_id', '<option></option>' + options_from_collection_for_select(trackers, 'id', 'name'), :onchange => "if (this.value != '') {this.form.submit()}"),
196 select_tag('tracker_id', '<option></option>' + options_from_collection_for_select(trackers, 'id', 'name'), :onchange => "if (this.value != '') {this.form.submit()}"),
197 :action => url_for(:controller => 'projects', :action => 'add_issue', :id => @project), :method => 'get')
197 :action => url_for(:controller => 'projects', :action => 'add_issue', :id => @project), :method => 'get')
198 end
198 end
199 end
199 end
@@ -1,39 +1,39
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 CustomValue < ActiveRecord::Base
18 class CustomValue < ActiveRecord::Base
19 belongs_to :custom_field
19 belongs_to :custom_field
20 belongs_to :customized, :polymorphic => true
20 belongs_to :customized, :polymorphic => true
21
21
22 protected
22 protected
23 def validate
23 def validate
24 errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.blank?
24 errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.blank?
25 errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
25 errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
26 errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length and value.length > 0
26 errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length and value.length > 0
27 errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
27 errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
28 case custom_field.field_format
28 case custom_field.field_format
29 when 'int'
29 when 'int'
30 errors.add(:value, :activerecord_error_not_a_number) unless value.blank? || value =~ /^[+-]?\d+$/
30 errors.add(:value, :activerecord_error_not_a_number) unless value.blank? || value =~ /^[+-]?\d+$/
31 when 'float'
31 when 'float'
32 begin; !value.blank? && Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
32 begin; !value.blank? && Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
33 when 'date'
33 when 'date'
34 errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.empty?
34 errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.blank?
35 when 'list'
35 when 'list'
36 errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include? value or value.empty?
36 errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value) or value.blank?
37 end
37 end
38 end
38 end
39 end
39 end
@@ -1,217 +1,221
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
30 has_many :time_entries, :dependent => :nullify
30 has_many :time_entries, :dependent => :nullify
31 has_many :custom_values, :dependent => :delete_all, :as => :customized
31 has_many :custom_values, :dependent => :delete_all, :as => :customized
32 has_many :custom_fields, :through => :custom_values
32 has_many :custom_fields, :through => :custom_values
33 has_and_belongs_to_many :changesets, :order => "revision ASC"
33 has_and_belongs_to_many :changesets, :order => "revision ASC"
34
34
35 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
37
37
38 acts_as_watchable
38 acts_as_watchable
39 acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
39 acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
42
42
43 validates_presence_of :subject, :description, :priority, :tracker, :author, :status
43 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
44 validates_length_of :subject, :maximum => 255
44 validates_length_of :subject, :maximum => 255
45 validates_inclusion_of :done_ratio, :in => 0..100
45 validates_inclusion_of :done_ratio, :in => 0..100
46 validates_numericality_of :estimated_hours, :allow_nil => true
46 validates_numericality_of :estimated_hours, :allow_nil => true
47 validates_associated :custom_values, :on => :update
47 validates_associated :custom_values, :on => :update
48
48
49 def after_initialize
49 def after_initialize
50 if new_record?
50 if new_record?
51 # set default values for new records only
51 # set default values for new records only
52 self.status ||= IssueStatus.default
52 self.status ||= IssueStatus.default
53 self.priority ||= Enumeration.default('IPRI')
53 self.priority ||= Enumeration.default('IPRI')
54 end
54 end
55 end
55 end
56
56
57 def copy_from(arg)
57 def copy_from(arg)
58 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
58 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
59 self.attributes = issue.attributes.dup
59 self.attributes = issue.attributes.dup
60 self.custom_values = issue.custom_values.collect {|v| v.clone}
60 self.custom_values = issue.custom_values.collect {|v| v.clone}
61 self
61 self
62 end
62 end
63
63
64 # Move an issue to a new project and tracker
64 # Move an issue to a new project and tracker
65 def move_to(new_project, new_tracker = nil)
65 def move_to(new_project, new_tracker = nil)
66 transaction do
66 transaction do
67 if new_project && project_id != new_project.id
67 if new_project && project_id != new_project.id
68 # delete issue relations
68 # delete issue relations
69 self.relations_from.clear
69 self.relations_from.clear
70 self.relations_to.clear
70 self.relations_to.clear
71 # issue is moved to another project
71 # issue is moved to another project
72 self.category = nil
72 self.category = nil
73 self.fixed_version = nil
73 self.fixed_version = nil
74 self.project = new_project
74 self.project = new_project
75 end
75 end
76 if new_tracker
76 if new_tracker
77 self.tracker = new_tracker
77 self.tracker = new_tracker
78 end
78 end
79 if save
79 if save
80 # Manually update project_id on related time entries
80 # Manually update project_id on related time entries
81 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
81 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
82 else
82 else
83 rollback_db_transaction
83 rollback_db_transaction
84 return false
84 return false
85 end
85 end
86 end
86 end
87 return true
87 return true
88 end
88 end
89
89
90 def priority_id=(pid)
90 def priority_id=(pid)
91 self.priority = nil
91 self.priority = nil
92 write_attribute(:priority_id, pid)
92 write_attribute(:priority_id, pid)
93 end
93 end
94
94
95 def validate
95 def validate
96 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
96 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
97 errors.add :due_date, :activerecord_error_not_a_date
97 errors.add :due_date, :activerecord_error_not_a_date
98 end
98 end
99
99
100 if self.due_date and self.start_date and self.due_date < self.start_date
100 if self.due_date and self.start_date and self.due_date < self.start_date
101 errors.add :due_date, :activerecord_error_greater_than_start_date
101 errors.add :due_date, :activerecord_error_greater_than_start_date
102 end
102 end
103
103
104 if start_date && soonest_start && start_date < soonest_start
104 if start_date && soonest_start && start_date < soonest_start
105 errors.add :start_date, :activerecord_error_invalid
105 errors.add :start_date, :activerecord_error_invalid
106 end
106 end
107 end
107 end
108
108
109 def validate_on_create
110 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
111 end
112
109 def before_create
113 def before_create
110 # default assignment based on category
114 # default assignment based on category
111 if assigned_to.nil? && category && category.assigned_to
115 if assigned_to.nil? && category && category.assigned_to
112 self.assigned_to = category.assigned_to
116 self.assigned_to = category.assigned_to
113 end
117 end
114 end
118 end
115
119
116 def before_save
120 def before_save
117 if @current_journal
121 if @current_journal
118 # attributes changes
122 # attributes changes
119 (Issue.column_names - %w(id description)).each {|c|
123 (Issue.column_names - %w(id description)).each {|c|
120 @current_journal.details << JournalDetail.new(:property => 'attr',
124 @current_journal.details << JournalDetail.new(:property => 'attr',
121 :prop_key => c,
125 :prop_key => c,
122 :old_value => @issue_before_change.send(c),
126 :old_value => @issue_before_change.send(c),
123 :value => send(c)) unless send(c)==@issue_before_change.send(c)
127 :value => send(c)) unless send(c)==@issue_before_change.send(c)
124 }
128 }
125 # custom fields changes
129 # custom fields changes
126 custom_values.each {|c|
130 custom_values.each {|c|
127 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
131 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
128 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
132 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
129 @current_journal.details << JournalDetail.new(:property => 'cf',
133 @current_journal.details << JournalDetail.new(:property => 'cf',
130 :prop_key => c.custom_field_id,
134 :prop_key => c.custom_field_id,
131 :old_value => @custom_values_before_change[c.custom_field_id],
135 :old_value => @custom_values_before_change[c.custom_field_id],
132 :value => c.value)
136 :value => c.value)
133 }
137 }
134 @current_journal.save
138 @current_journal.save
135 end
139 end
136 # Save the issue even if the journal is not saved (because empty)
140 # Save the issue even if the journal is not saved (because empty)
137 true
141 true
138 end
142 end
139
143
140 def after_save
144 def after_save
141 # Update start/due dates of following issues
145 # Update start/due dates of following issues
142 relations_from.each(&:set_issue_to_dates)
146 relations_from.each(&:set_issue_to_dates)
143
147
144 # Close duplicates if the issue was closed
148 # Close duplicates if the issue was closed
145 if @issue_before_change && !@issue_before_change.closed? && self.closed?
149 if @issue_before_change && !@issue_before_change.closed? && self.closed?
146 duplicates.each do |duplicate|
150 duplicates.each do |duplicate|
147 # Don't re-close it if it's already closed
151 # Don't re-close it if it's already closed
148 next if duplicate.closed?
152 next if duplicate.closed?
149 # Same user and notes
153 # Same user and notes
150 duplicate.init_journal(@current_journal.user, @current_journal.notes)
154 duplicate.init_journal(@current_journal.user, @current_journal.notes)
151 duplicate.update_attribute :status, self.status
155 duplicate.update_attribute :status, self.status
152 end
156 end
153 end
157 end
154 end
158 end
155
159
156 def custom_value_for(custom_field)
160 def custom_value_for(custom_field)
157 self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
161 self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
158 return nil
162 return nil
159 end
163 end
160
164
161 def init_journal(user, notes = "")
165 def init_journal(user, notes = "")
162 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
166 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
163 @issue_before_change = self.clone
167 @issue_before_change = self.clone
164 @custom_values_before_change = {}
168 @custom_values_before_change = {}
165 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
169 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
166 @current_journal
170 @current_journal
167 end
171 end
168
172
169 # Return true if the issue is closed, otherwise false
173 # Return true if the issue is closed, otherwise false
170 def closed?
174 def closed?
171 self.status.is_closed?
175 self.status.is_closed?
172 end
176 end
173
177
174 # Users the issue can be assigned to
178 # Users the issue can be assigned to
175 def assignable_users
179 def assignable_users
176 project.assignable_users
180 project.assignable_users
177 end
181 end
178
182
179 # Returns the mail adresses of users that should be notified for the issue
183 # Returns the mail adresses of users that should be notified for the issue
180 def recipients
184 def recipients
181 recipients = project.recipients
185 recipients = project.recipients
182 # Author and assignee are always notified
186 # Author and assignee are always notified
183 recipients << author.mail if author
187 recipients << author.mail if author
184 recipients << assigned_to.mail if assigned_to
188 recipients << assigned_to.mail if assigned_to
185 recipients.compact.uniq
189 recipients.compact.uniq
186 end
190 end
187
191
188 def spent_hours
192 def spent_hours
189 @spent_hours ||= time_entries.sum(:hours) || 0
193 @spent_hours ||= time_entries.sum(:hours) || 0
190 end
194 end
191
195
192 def relations
196 def relations
193 (relations_from + relations_to).sort
197 (relations_from + relations_to).sort
194 end
198 end
195
199
196 def all_dependent_issues
200 def all_dependent_issues
197 dependencies = []
201 dependencies = []
198 relations_from.each do |relation|
202 relations_from.each do |relation|
199 dependencies << relation.issue_to
203 dependencies << relation.issue_to
200 dependencies += relation.issue_to.all_dependent_issues
204 dependencies += relation.issue_to.all_dependent_issues
201 end
205 end
202 dependencies
206 dependencies
203 end
207 end
204
208
205 # Returns an array of the duplicate issues
209 # Returns an array of the duplicate issues
206 def duplicates
210 def duplicates
207 relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
211 relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
208 end
212 end
209
213
210 def duration
214 def duration
211 (start_date && due_date) ? due_date - start_date : 0
215 (start_date && due_date) ? due_date - start_date : 0
212 end
216 end
213
217
214 def soonest_start
218 def soonest_start
215 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
219 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
216 end
220 end
217 end
221 end
@@ -1,194 +1,195
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, :dependent => :delete_all, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
23 has_many :members, :dependent => :delete_all, :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 :custom_values, :dependent => :delete_all, :as => :customized
25 has_many :custom_values, :dependent => :delete_all, :as => :customized
26 has_many :enabled_modules, :dependent => :delete_all
26 has_many :enabled_modules, :dependent => :delete_all
27 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]
28 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
28 has_many :issue_changes, :through => :issues, :source => :journals
29 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"
30 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
31 has_many :time_entries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
32 has_many :queries, :dependent => :delete_all
32 has_many :documents, :dependent => :destroy
33 has_many :documents, :dependent => :destroy
33 has_many :news, :dependent => :delete_all, :include => :author
34 has_many :news, :dependent => :delete_all, :include => :author
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
35 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
35 has_many :boards, :order => "position ASC"
36 has_many :boards, :order => "position ASC"
36 has_one :repository, :dependent => :destroy
37 has_one :repository, :dependent => :destroy
37 has_many :changesets, :through => :repository
38 has_many :changesets, :through => :repository
38 has_one :wiki, :dependent => :destroy
39 has_one :wiki, :dependent => :destroy
39 # Custom field for the project issues
40 # Custom field for the project issues
40 has_and_belongs_to_many :custom_fields,
41 has_and_belongs_to_many :custom_fields,
41 :class_name => 'IssueCustomField',
42 :class_name => 'IssueCustomField',
42 :order => "#{CustomField.table_name}.position",
43 :order => "#{CustomField.table_name}.position",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 :association_foreign_key => 'custom_field_id'
45 :association_foreign_key => 'custom_field_id'
45
46
46 acts_as_tree :order => "name", :counter_cache => true
47 acts_as_tree :order => "name", :counter_cache => true
47
48
48 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id'
49 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id'
49 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}"},
50 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
51 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
51
52
52 attr_protected :status, :enabled_module_names
53 attr_protected :status, :enabled_module_names
53
54
54 validates_presence_of :name, :description, :identifier
55 validates_presence_of :name, :description, :identifier
55 validates_uniqueness_of :name, :identifier
56 validates_uniqueness_of :name, :identifier
56 validates_associated :custom_values, :on => :update
57 validates_associated :custom_values, :on => :update
57 validates_associated :repository, :wiki
58 validates_associated :repository, :wiki
58 validates_length_of :name, :maximum => 30
59 validates_length_of :name, :maximum => 30
59 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
60 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
60 validates_length_of :description, :maximum => 255
61 validates_length_of :description, :maximum => 255
61 validates_length_of :homepage, :maximum => 60
62 validates_length_of :homepage, :maximum => 60
62 validates_length_of :identifier, :in => 3..12
63 validates_length_of :identifier, :in => 3..12
63 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
64
65
65 def identifier=(identifier)
66 def identifier=(identifier)
66 super unless identifier_frozen?
67 super unless identifier_frozen?
67 end
68 end
68
69
69 def identifier_frozen?
70 def identifier_frozen?
70 errors[:identifier].nil? && !(new_record? || identifier.blank?)
71 errors[:identifier].nil? && !(new_record? || identifier.blank?)
71 end
72 end
72
73
73 def issues_with_subprojects(include_subprojects=false)
74 def issues_with_subprojects(include_subprojects=false)
74 conditions = nil
75 conditions = nil
75 if include_subprojects && !active_children.empty?
76 if include_subprojects && !active_children.empty?
76 ids = [id] + active_children.collect {|c| c.id}
77 ids = [id] + active_children.collect {|c| c.id}
77 conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"]
78 conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"]
78 end
79 end
79 conditions ||= ["#{Issue.table_name}.project_id = ?", id]
80 conditions ||= ["#{Issue.table_name}.project_id = ?", id]
80 Issue.with_scope :find => { :conditions => conditions } do
81 Issue.with_scope :find => { :conditions => conditions } do
81 yield
82 yield
82 end
83 end
83 end
84 end
84
85
85 # Return all issues status changes for the project between the 2 given dates
86 # Return all issues status changes for the project between the 2 given dates
86 def issues_status_changes(from, to)
87 def issues_status_changes(from, to)
87 Journal.find(:all, :include => [:issue, :details, :user],
88 Journal.find(:all, :include => [:issue, :details, :user],
88 :conditions => ["#{Journal.table_name}.journalized_type = 'Issue'" +
89 :conditions => ["#{Journal.table_name}.journalized_type = 'Issue'" +
89 " AND #{Issue.table_name}.project_id = ?" +
90 " AND #{Issue.table_name}.project_id = ?" +
90 " AND #{JournalDetail.table_name}.prop_key = 'status_id'" +
91 " AND #{JournalDetail.table_name}.prop_key = 'status_id'" +
91 " AND #{Journal.table_name}.created_on BETWEEN ? AND ?",
92 " AND #{Journal.table_name}.created_on BETWEEN ? AND ?",
92 id, from, to+1])
93 id, from, to+1])
93 end
94 end
94
95
95 # returns latest created projects
96 # returns latest created projects
96 # non public projects will be returned only if user is a member of those
97 # non public projects will be returned only if user is a member of those
97 def self.latest(user=nil, count=5)
98 def self.latest(user=nil, count=5)
98 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
99 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
99 end
100 end
100
101
101 def self.visible_by(user=nil)
102 def self.visible_by(user=nil)
102 if user && user.admin?
103 if user && user.admin?
103 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
104 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
104 elsif user && user.memberships.any?
105 elsif user && user.memberships.any?
105 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(',')}))"
106 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(',')}))"
106 else
107 else
107 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
108 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
108 end
109 end
109 end
110 end
110
111
111 def active?
112 def active?
112 self.status == STATUS_ACTIVE
113 self.status == STATUS_ACTIVE
113 end
114 end
114
115
115 def archive
116 def archive
116 # Archive subprojects if any
117 # Archive subprojects if any
117 children.each do |subproject|
118 children.each do |subproject|
118 subproject.archive
119 subproject.archive
119 end
120 end
120 update_attribute :status, STATUS_ARCHIVED
121 update_attribute :status, STATUS_ARCHIVED
121 end
122 end
122
123
123 def unarchive
124 def unarchive
124 return false if parent && !parent.active?
125 return false if parent && !parent.active?
125 update_attribute :status, STATUS_ACTIVE
126 update_attribute :status, STATUS_ACTIVE
126 end
127 end
127
128
128 def active_children
129 def active_children
129 children.select {|child| child.active?}
130 children.select {|child| child.active?}
130 end
131 end
131
132
132 # Users issues can be assigned to
133 # Users issues can be assigned to
133 def assignable_users
134 def assignable_users
134 members.select {|m| m.role.assignable?}.collect {|m| m.user}
135 members.select {|m| m.role.assignable?}.collect {|m| m.user}
135 end
136 end
136
137
137 # Returns the mail adresses of users that should be always notified on project events
138 # Returns the mail adresses of users that should be always notified on project events
138 def recipients
139 def recipients
139 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
140 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
140 end
141 end
141
142
142 # Returns an array of all custom fields enabled for project issues
143 # Returns an array of all custom fields enabled for project issues
143 # (explictly associated custom fields and custom fields enabled for all projects)
144 # (explictly associated custom fields and custom fields enabled for all projects)
144 def custom_fields_for_issues(tracker)
145 def custom_fields_for_issues(tracker)
145 all_custom_fields.select {|c| tracker.custom_fields.include? c }
146 all_custom_fields.select {|c| tracker.custom_fields.include? c }
146 end
147 end
147
148
148 def all_custom_fields
149 def all_custom_fields
149 @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
150 @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
150 end
151 end
151
152
152 def <=>(project)
153 def <=>(project)
153 name.downcase <=> project.name.downcase
154 name.downcase <=> project.name.downcase
154 end
155 end
155
156
156 def allows_to?(action)
157 def allows_to?(action)
157 if action.is_a? Hash
158 if action.is_a? Hash
158 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
159 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
159 else
160 else
160 allowed_permissions.include? action
161 allowed_permissions.include? action
161 end
162 end
162 end
163 end
163
164
164 def module_enabled?(module_name)
165 def module_enabled?(module_name)
165 module_name = module_name.to_s
166 module_name = module_name.to_s
166 enabled_modules.detect {|m| m.name == module_name}
167 enabled_modules.detect {|m| m.name == module_name}
167 end
168 end
168
169
169 def enabled_module_names=(module_names)
170 def enabled_module_names=(module_names)
170 enabled_modules.clear
171 enabled_modules.clear
171 module_names = [] unless module_names && module_names.is_a?(Array)
172 module_names = [] unless module_names && module_names.is_a?(Array)
172 module_names.each do |name|
173 module_names.each do |name|
173 enabled_modules << EnabledModule.new(:name => name.to_s)
174 enabled_modules << EnabledModule.new(:name => name.to_s)
174 end
175 end
175 end
176 end
176
177
177 protected
178 protected
178 def validate
179 def validate
179 errors.add(parent_id, " must be a root project") if parent and parent.parent
180 errors.add(parent_id, " must be a root project") if parent and parent.parent
180 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
181 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
181 end
182 end
182
183
183 private
184 private
184 def allowed_permissions
185 def allowed_permissions
185 @allowed_permissions ||= begin
186 @allowed_permissions ||= begin
186 module_names = enabled_modules.collect {|m| m.name}
187 module_names = enabled_modules.collect {|m| m.name}
187 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
188 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
188 end
189 end
189 end
190 end
190
191
191 def allowed_actions
192 def allowed_actions
192 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
193 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
193 end
194 end
194 end
195 end
@@ -1,36 +1,40
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 Tracker < ActiveRecord::Base
18 class Tracker < ActiveRecord::Base
19 before_destroy :check_integrity
19 before_destroy :check_integrity
20 has_many :issues
20 has_many :issues
21 has_many :workflows, :dependent => :delete_all
21 has_many :workflows, :dependent => :delete_all
22 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
22 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
23 acts_as_list
23 acts_as_list
24
24
25 validates_presence_of :name
25 validates_presence_of :name
26 validates_uniqueness_of :name
26 validates_uniqueness_of :name
27 validates_length_of :name, :maximum => 30
27 validates_length_of :name, :maximum => 30
28 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
28 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
29
29
30 def to_s; name end
30 def to_s; name end
31
31
32 def self.all
33 find(:all, :order => 'position')
34 end
35
32 private
36 private
33 def check_integrity
37 def check_integrity
34 raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
38 raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
35 end
39 end
36 end
40 end
@@ -1,18 +1,18
1 <% if authorize_for('projects', 'add_issue') %>
1 <% if authorize_for('projects', 'add_issue') && @project.trackers.any? %>
2 <h3><%= l(:label_issue_new) %></h3>
2 <h3><%= l(:label_issue_new) %></h3>
3 <%= l(:label_tracker) %>: <%= new_issue_selector %>
3 <%= l(:label_tracker) %>: <%= new_issue_selector %>
4 <% end %>
4 <% end %>
5
5
6 <h3><%= l(:label_issue_plural) %></h3>
6 <h3><%= l(:label_issue_plural) %></h3>
7 <%= link_to l(:label_issue_view_all), { :set_filter => 1 } %><br />
7 <%= link_to l(:label_issue_view_all), { :set_filter => 1 } %><br />
8 <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
8 <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
9 <%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %>
9 <%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %>
10
10
11 <h3><%= l(:label_query_plural) %></h3>
11 <h3><%= l(:label_query_plural) %></h3>
12
12
13 <% queries = @project.queries.find(:all,
13 <% queries = @project.queries.find(:all,
14 :order => "name ASC",
14 :order => "name ASC",
15 :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
15 :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
16 queries.each do |query| %>
16 queries.each do |query| %>
17 <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %><br />
17 <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %><br />
18 <% end %>
18 <% end %>
@@ -1,40 +1,40
1 <% back_to = url_for(:controller => 'issues', :action => 'index', :project_id => @project) %>
1 <% back_to = url_for(:controller => 'issues', :action => 'index', :project_id => @project) %>
2 <ul>
2 <ul>
3 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
3 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
4 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
4 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
5 <li class="folder">
5 <li class="folder">
6 <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
6 <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
7 <ul>
7 <ul>
8 <% @statuses.each do |s| %>
8 <% @statuses.each do |s| %>
9 <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'change_status', :id => @issue, :new_status_id => s},
9 <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'change_status', :id => @issue, :new_status_id => s},
10 :selected => (s == @issue.status), :disabled => !(@can[:change_status] && @allowed_statuses.include?(s)) %></li>
10 :selected => (s == @issue.status), :disabled => !(@can[:change_status] && @allowed_statuses.include?(s)) %></li>
11 <% end %>
11 <% end %>
12 </ul>
12 </ul>
13 </li>
13 </li>
14 <li class="folder">
14 <li class="folder">
15 <a href="#" class="submenu"><%= l(:field_priority) %></a>
15 <a href="#" class="submenu"><%= l(:field_priority) %></a>
16 <ul>
16 <ul>
17 <% @priorities.each do |p| %>
17 <% @priorities.each do |p| %>
18 <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => back_to}, :method => :post,
18 <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => back_to}, :method => :post,
19 :selected => (p == @issue.priority), :disabled => !@can[:edit] %></li>
19 :selected => (p == @issue.priority), :disabled => !@can[:edit] %></li>
20 <% end %>
20 <% end %>
21 </ul>
21 </ul>
22 </li>
22 </li>
23 <li class="folder">
23 <li class="folder">
24 <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
24 <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
25 <ul>
25 <ul>
26 <% @assignables.each do |u| %>
26 <% @assignables.each do |u| %>
27 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => back_to}, :method => :post,
27 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => back_to}, :method => :post,
28 :selected => (u == @issue.assigned_to), :disabled => !(@can[:edit] || @can[:change_status]) %></li>
28 :selected => (u == @issue.assigned_to), :disabled => !(@can[:edit] || @can[:change_status]) %></li>
29 <% end %>
29 <% end %>
30 <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => back_to}, :method => :post,
30 <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => back_to}, :method => :post,
31 :selected => @issue.assigned_to.nil?, :disabled => !(@can[:edit] || @can[:change_status]) %></li>
31 :selected => @issue.assigned_to.nil?, :disabled => !(@can[:edit] || @can[:change_status]) %></li>
32 </ul>
32 </ul>
33 </li>
33 </li>
34 <li><%= context_menu_link l(:button_copy), {:controller => 'projects', :action => 'add_issue', :id => @project, :copy_from => @issue},
34 <li><%= context_menu_link l(:button_copy), {:controller => 'projects', :action => 'add_issue', :id => @project, :copy_from => @issue},
35 :class => 'icon-copy', :disabled => !@can[:add] %></li>
35 :class => 'icon-copy', :disabled => !@can[:copy] %></li>
36 <li><%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id },
36 <li><%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id },
37 :class => 'icon-move', :disabled => !@can[:move] %>
37 :class => 'icon-move', :disabled => !@can[:move] %>
38 <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue},
38 <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue},
39 :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon-del', :disabled => !@can[:delete] %></li>
39 :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon-del', :disabled => !@can[:delete] %></li>
40 </ul>
40 </ul>
@@ -1,36 +1,52
1 <%= error_messages_for 'project' %>
1 <%= error_messages_for 'project' %>
2
2
3 <div class="box">
3 <div class="box">
4 <!--[form:project]-->
4 <!--[form:project]-->
5 <p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
5 <p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
6
6
7 <% if User.current.admin? and !@root_projects.empty? %>
7 <% if User.current.admin? and !@root_projects.empty? %>
8 <p><%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %></p>
8 <p><%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %></p>
9 <% end %>
9 <% end %>
10
10
11 <p><%= f.text_area :description, :required => true, :cols => 60, :rows => 5 %><em><%= l(:text_caracters_maximum, 255) %></em></p>
11 <p><%= f.text_area :description, :required => true, :cols => 60, :rows => 5 %><em><%= l(:text_caracters_maximum, 255) %></em></p>
12 <p><%= f.text_field :identifier, :required => true, :size => 15, :disabled => @project.identifier_frozen? %><br /><em><%= l(:text_length_between, 3, 12) %> <%= l(:text_project_identifier_info) unless @project.identifier_frozen? %></em></p>
12 <p><%= f.text_field :identifier, :required => true, :size => 15, :disabled => @project.identifier_frozen? %><br /><em><%= l(:text_length_between, 3, 12) %> <%= l(:text_project_identifier_info) unless @project.identifier_frozen? %></em></p>
13 <p><%= f.text_field :homepage, :size => 40 %></p>
13 <p><%= f.text_field :homepage, :size => 40 %></p>
14 <p><%= f.check_box :is_public %></p>
14 <p><%= f.check_box :is_public %></p>
15 <%= wikitoolbar_for 'project_description' %>
15 <%= wikitoolbar_for 'project_description' %>
16
16
17 <% for @custom_value in @custom_values %>
17 <% for @custom_value in @custom_values %>
18 <p><%= custom_field_tag_with_label @custom_value %></p>
18 <p><%= custom_field_tag_with_label @custom_value %></p>
19 <% end %>
19 <% end %>
20 </div>
21
22 <% unless @trackers.empty? %>
23 <fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend>
24 <% @trackers.each do |tracker| %>
25 <label class="floating">
26 <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %>
27 <%= tracker %>
28 </label>
29 <% end %>
30 <%= hidden_field_tag 'project[tracker_ids][]', '' %>
31 </fieldset>
32 <% end %>
20
33
21 <% unless @custom_fields.empty? %>
34 <% unless @custom_fields.empty? %>
22 <p><label><%=l(:label_custom_field_plural)%></label>
35 <fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
23 <% for custom_field in @custom_fields %>
36 <% for custom_field in @custom_fields %>
37 <label class="floating">
24 <%= check_box_tag "custom_field_ids[]", custom_field.id, ((@project.custom_fields.include? custom_field) or custom_field.is_for_all?), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
38 <%= check_box_tag "custom_field_ids[]", custom_field.id, ((@project.custom_fields.include? custom_field) or custom_field.is_for_all?), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
25 <%= custom_field.name %>
39 <%= custom_field.name %>
26 <% end %></p>
40 </label>
41 <% end %>
42 </fieldset>
27 <% end %>
43 <% end %>
28 <!--[eoform:project]-->
44 <!--[eoform:project]-->
29 </div>
45
30
46
31 <% content_for :header_tags do %>
47 <% content_for :header_tags do %>
32 <%= javascript_include_tag 'calendar/calendar' %>
48 <%= javascript_include_tag 'calendar/calendar' %>
33 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
49 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
34 <%= javascript_include_tag 'calendar/calendar-setup' %>
50 <%= javascript_include_tag 'calendar/calendar-setup' %>
35 <%= stylesheet_link_tag 'calendar' %>
51 <%= stylesheet_link_tag 'calendar' %>
36 <% end %>
52 <% end %>
@@ -1,15 +1,15
1 <h2><%=l(:label_project_new)%></h2>
1 <h2><%=l(:label_project_new)%></h2>
2
2
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %>
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5
5
6 <div class="box">
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
7 <p><label><%= l(:label_module_plural) %></label>
8 <% Redmine::AccessControl.available_project_modules.each do |m| %>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %> <%= m.to_s.humanize %>
8 <label class="floating">
10 <% end %></p>
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %> <%= m.to_s.humanize %>
11 </div>
10 </label>
12
11 <% end %>
12 </fieldset>
13
13
14 <%= submit_tag l(:button_save) %>
14 <%= submit_tag l(:button_save) %>
15 <% end %>
15 <% end %>
@@ -1,24 +1,29
1 <h2><%=l(:button_move)%></h2>
1 <h2><%=l(:button_move)%></h2>
2
2
3
3
4 <% form_tag({:action => 'move_issues', :id => @project}, :class => "tabular") do %>
4 <% form_tag({:action => 'move_issues', :id => @project}, :class => 'tabular', :id => 'move_form') do %>
5
5
6 <div class="box">
6 <div class="box">
7 <p><label><%= l(:label_issue_plural) %> :</label>
7 <p><label><%= l(:label_issue_plural) %> :</label>
8 <% for issue in @issues %>
8 <% for issue in @issues %>
9 <%= link_to_issue issue %>: <%=h issue.subject %>
9 <%= link_to_issue issue %>: <%=h issue.subject %>
10 <%= hidden_field_tag "issue_ids[]", issue.id %><br />
10 <%= hidden_field_tag "issue_ids[]", issue.id %><br />
11 <% end %>
11 <% end %>
12 <i>(<%= @issues.length%> <%= lwr(:label_issue, @issues.length)%>)</i></p>
12 <i>(<%= @issues.length%> <%= lwr(:label_issue, @issues.length)%>)</i></p>
13
13
14 &nbsp;
14 &nbsp;
15
15
16 <!--[form:issue]-->
16 <!--[form:issue]-->
17 <p><label for="new_project_id"><%=l(:field_project)%> :</label>
17 <p><label for="new_project_id"><%=l(:field_project)%> :</label>
18 <%= select_tag "new_project_id", options_from_collection_for_select(@projects, "id", "name", @project.id) %></p>
18 <%= select_tag "new_project_id",
19 options_from_collection_for_select(@projects, 'id', 'name', @target_project.id),
20 :onchange => remote_function(:url => {:action => 'move_issues' , :id => @project},
21 :method => :get,
22 :update => 'content',
23 :with => "Form.serialize('move_form')") %></p>
19
24
20 <p><label for="new_tracker_id"><%=l(:field_tracker)%> :</label>
25 <p><label for="new_tracker_id"><%=l(:field_tracker)%> :</label>
21 <%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p>
26 <%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p>
22 </div>
27 </div>
23 <%= submit_tag l(:button_move) %>
28 <%= submit_tag l(:button_move) %>
24 <% end %>
29 <% end %>
@@ -1,83 +1,83
1 <h2><%=l(:label_overview)%></h2>
1 <h2><%=l(:label_overview)%></h2>
2
2
3 <div class="splitcontentleft">
3 <div class="splitcontentleft">
4 <%= textilizable @project.description %>
4 <%= textilizable @project.description %>
5 <ul>
5 <ul>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link @project.homepage %></li><% end %>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link @project.homepage %></li><% end %>
7 <% if @subprojects.any? %>
7 <% if @subprojects.any? %>
8 <li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(p.name, :action => 'show', :id => p)}.join(", ") %></li>
8 <li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(p.name, :action => 'show', :id => p)}.join(", ") %></li>
9 <% end %>
9 <% end %>
10 <% if @project.parent %>
10 <% if @project.parent %>
11 <li><%=l(:field_parent)%>: <%= link_to @project.parent.name, :controller => 'projects', :action => 'show', :id => @project.parent %></li>
11 <li><%=l(:field_parent)%>: <%= link_to @project.parent.name, :controller => 'projects', :action => 'show', :id => @project.parent %></li>
12 <% end %>
12 <% end %>
13 <% for custom_value in @custom_values %>
13 <% for custom_value in @custom_values %>
14 <% if !custom_value.value.empty? %>
14 <% if !custom_value.value.empty? %>
15 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
15 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
16 <% end %>
16 <% end %>
17 <% end %>
17 <% end %>
18 </ul>
18 </ul>
19
19
20 <% if User.current.allowed_to?(:view_issues, @project) %>
20 <% if User.current.allowed_to?(:view_issues, @project) %>
21 <div class="box">
21 <div class="box">
22 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
22 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
23 <ul>
23 <ul>
24 <% for tracker in @trackers %>
24 <% for tracker in @trackers %>
25 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
25 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
26 :set_filter => 1,
26 :set_filter => 1,
27 "tracker_id" => tracker.id %>:
27 "tracker_id" => tracker.id %>:
28 <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %>
28 <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %>
29 <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li>
29 <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li>
30 <% end %>
30 <% end %>
31 </ul>
31 </ul>
32 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
32 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
33 </div>
33 </div>
34 <% end %>
34 <% end %>
35 </div>
35 </div>
36
36
37 <div class="splitcontentright">
37 <div class="splitcontentright">
38 <% if @members_by_role.any? %>
38 <% if @members_by_role.any? %>
39 <div class="box">
39 <div class="box">
40 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
40 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
41 <p><% @members_by_role.keys.sort.each do |role| %>
41 <p><% @members_by_role.keys.sort.each do |role| %>
42 <%= role.name %>:
42 <%= role.name %>:
43 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
43 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
44 <br />
44 <br />
45 <% end %></p>
45 <% end %></p>
46 </div>
46 </div>
47 <% end %>
47 <% end %>
48
48
49 <% if @news.any? && authorize_for('news', 'index') %>
49 <% if @news.any? && authorize_for('news', 'index') %>
50 <div class="box">
50 <div class="box">
51 <h3><%=l(:label_news_latest)%></h3>
51 <h3><%=l(:label_news_latest)%></h3>
52 <%= render :partial => 'news/news', :collection => @news %>
52 <%= render :partial => 'news/news', :collection => @news %>
53 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
53 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
54 </div>
54 </div>
55 <% end %>
55 <% end %>
56 </div>
56 </div>
57
57
58 <% content_for :sidebar do %>
58 <% content_for :sidebar do %>
59 <% if authorize_for('projects', 'add_issue') %>
59 <% if authorize_for('projects', 'add_issue') && @project.trackers.any? %>
60 <h3><%= l(:label_issue_new) %></h3>
60 <h3><%= l(:label_issue_new) %></h3>
61 <%= l(:label_tracker) %>: <%= new_issue_selector %>
61 <%= l(:label_tracker) %>: <%= new_issue_selector %>
62 <% end %>
62 <% end %>
63
63
64 <% planning_links = []
64 <% planning_links = []
65 planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :id => @project)
65 planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :id => @project)
66 planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :id => @project)
66 planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :id => @project)
67 planning_links.compact!
67 planning_links.compact!
68 unless planning_links.empty? %>
68 unless planning_links.empty? %>
69 <h3>Planning</h3>
69 <h3>Planning</h3>
70 <p><%= planning_links.join(' | ') %></p>
70 <p><%= planning_links.join(' | ') %></p>
71 <% end %>
71 <% end %>
72
72
73 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
73 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
74 <h3><%= l(:label_spent_time) %></h3>
74 <h3><%= l(:label_spent_time) %></h3>
75 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
75 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
76 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
76 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
77 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
77 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
78 <% end %>
78 <% end %>
79 <% end %>
79 <% end %>
80
80
81 <% content_for :header_tags do %>
81 <% content_for :header_tags do %>
82 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
82 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
83 <% end %>
83 <% end %>
@@ -1,149 +1,149
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 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'projects_controller'
19 require 'projects_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class ProjectsController; def rescue_action(e) raise e end; end
22 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < Test::Unit::TestCase
24 class ProjectsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :issues, :journals, :journal_details, :trackers, :issue_statuses, :enabled_modules, :enumerations
25 fixtures :projects, :users, :roles, :members, :issues, :journals, :journal_details, :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations
26
26
27 def setup
27 def setup
28 @controller = ProjectsController.new
28 @controller = ProjectsController.new
29 @request = ActionController::TestRequest.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
30 @response = ActionController::TestResponse.new
31 end
31 end
32
32
33 def test_index
33 def test_index
34 get :index
34 get :index
35 assert_response :success
35 assert_response :success
36 assert_template 'list'
36 assert_template 'list'
37 end
37 end
38
38
39 def test_list
39 def test_list
40 get :list
40 get :list
41 assert_response :success
41 assert_response :success
42 assert_template 'list'
42 assert_template 'list'
43 assert_not_nil assigns(:project_tree)
43 assert_not_nil assigns(:project_tree)
44 end
44 end
45
45
46 def test_show
46 def test_show
47 get :show, :id => 1
47 get :show, :id => 1
48 assert_response :success
48 assert_response :success
49 assert_template 'show'
49 assert_template 'show'
50 assert_not_nil assigns(:project)
50 assert_not_nil assigns(:project)
51 end
51 end
52
52
53 def test_list_documents
53 def test_list_documents
54 get :list_documents, :id => 1
54 get :list_documents, :id => 1
55 assert_response :success
55 assert_response :success
56 assert_template 'list_documents'
56 assert_template 'list_documents'
57 assert_not_nil assigns(:grouped)
57 assert_not_nil assigns(:grouped)
58 end
58 end
59
59
60 def test_bulk_edit_issues
60 def test_bulk_edit_issues
61 @request.session[:user_id] = 2
61 @request.session[:user_id] = 2
62 # update issues priority
62 # update issues priority
63 post :bulk_edit_issues, :id => 1, :issue_ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
63 post :bulk_edit_issues, :id => 1, :issue_ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
64 assert_response 302
64 assert_response 302
65 # check that the issues were updated
65 # check that the issues were updated
66 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
66 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
67 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
67 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
68 end
68 end
69
69
70 def test_list_files
70 def test_list_files
71 get :list_files, :id => 1
71 get :list_files, :id => 1
72 assert_response :success
72 assert_response :success
73 assert_template 'list_files'
73 assert_template 'list_files'
74 assert_not_nil assigns(:versions)
74 assert_not_nil assigns(:versions)
75 end
75 end
76
76
77 def test_changelog
77 def test_changelog
78 get :changelog, :id => 1
78 get :changelog, :id => 1
79 assert_response :success
79 assert_response :success
80 assert_template 'changelog'
80 assert_template 'changelog'
81 assert_not_nil assigns(:versions)
81 assert_not_nil assigns(:versions)
82 end
82 end
83
83
84 def test_roadmap
84 def test_roadmap
85 get :roadmap, :id => 1
85 get :roadmap, :id => 1
86 assert_response :success
86 assert_response :success
87 assert_template 'roadmap'
87 assert_template 'roadmap'
88 assert_not_nil assigns(:versions)
88 assert_not_nil assigns(:versions)
89 end
89 end
90
90
91 def test_activity
91 def test_activity
92 get :activity, :id => 1
92 get :activity, :id => 1
93 assert_response :success
93 assert_response :success
94 assert_template 'activity'
94 assert_template 'activity'
95 assert_not_nil assigns(:events_by_day)
95 assert_not_nil assigns(:events_by_day)
96
96
97 assert_tag :tag => "h3",
97 assert_tag :tag => "h3",
98 :content => /#{2.days.ago.to_date.day}/,
98 :content => /#{2.days.ago.to_date.day}/,
99 :sibling => { :tag => "ul",
99 :sibling => { :tag => "ul",
100 :child => { :tag => "li",
100 :child => { :tag => "li",
101 :child => { :tag => "p",
101 :child => { :tag => "p",
102 :content => /(#{IssueStatus.find(2).name})/,
102 :content => /(#{IssueStatus.find(2).name})/,
103 }
103 }
104 }
104 }
105 }
105 }
106 assert_tag :tag => "h3",
106 assert_tag :tag => "h3",
107 :content => /#{3.day.ago.to_date.day}/,
107 :content => /#{3.day.ago.to_date.day}/,
108 :sibling => { :tag => "ul", :child => { :tag => "li",
108 :sibling => { :tag => "ul", :child => { :tag => "li",
109 :child => { :tag => "p",
109 :child => { :tag => "p",
110 :content => /#{Issue.find(1).subject}/,
110 :content => /#{Issue.find(1).subject}/,
111 }
111 }
112 }
112 }
113 }
113 }
114 end
114 end
115
115
116 def test_archive
116 def test_archive
117 @request.session[:user_id] = 1 # admin
117 @request.session[:user_id] = 1 # admin
118 post :archive, :id => 1
118 post :archive, :id => 1
119 assert_redirected_to 'admin/projects'
119 assert_redirected_to 'admin/projects'
120 assert !Project.find(1).active?
120 assert !Project.find(1).active?
121 end
121 end
122
122
123 def test_unarchive
123 def test_unarchive
124 @request.session[:user_id] = 1 # admin
124 @request.session[:user_id] = 1 # admin
125 Project.find(1).archive
125 Project.find(1).archive
126 post :unarchive, :id => 1
126 post :unarchive, :id => 1
127 assert_redirected_to 'admin/projects'
127 assert_redirected_to 'admin/projects'
128 assert Project.find(1).active?
128 assert Project.find(1).active?
129 end
129 end
130
130
131 def test_add_issue
131 def test_add_issue
132 @request.session[:user_id] = 2
132 @request.session[:user_id] = 2
133 get :add_issue, :id => 1, :tracker_id => 1
133 get :add_issue, :id => 1, :tracker_id => 1
134 assert_response :success
134 assert_response :success
135 assert_template 'add_issue'
135 assert_template 'add_issue'
136 post :add_issue, :id => 1, :issue => {:tracker_id => 1, :subject => 'This is the test_add_issue issue', :description => 'This is the description', :priority_id => 5}
136 post :add_issue, :id => 1, :issue => {:tracker_id => 1, :subject => 'This is the test_add_issue issue', :description => 'This is the description', :priority_id => 5}
137 assert_redirected_to 'projects/1/issues'
137 assert_redirected_to 'projects/1/issues'
138 assert Issue.find_by_subject('This is the test_add_issue issue')
138 assert Issue.find_by_subject('This is the test_add_issue issue')
139 end
139 end
140
140
141 def test_copy_issue
141 def test_copy_issue
142 @request.session[:user_id] = 2
142 @request.session[:user_id] = 2
143 get :add_issue, :id => 1, :copy_from => 1
143 get :add_issue, :id => 1, :copy_from => 1
144 assert_template 'add_issue'
144 assert_template 'add_issue'
145 assert_not_nil assigns(:issue)
145 assert_not_nil assigns(:issue)
146 orig = Issue.find(1)
146 orig = Issue.find(1)
147 assert_equal orig.subject, assigns(:issue).subject
147 assert_equal orig.subject, assigns(:issue).subject
148 end
148 end
149 end
149 end
@@ -1,73 +1,73
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 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssueTest < Test::Unit::TestCase
20 class IssueTest < Test::Unit::TestCase
21 fixtures :projects, :users, :members, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries
21 fixtures :projects, :users, :members, :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries
22
22
23 def test_category_based_assignment
23 def test_category_based_assignment
24 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
24 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
25 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
25 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
26 end
26 end
27
27
28 def test_copy
28 def test_copy
29 issue = Issue.new.copy_from(1)
29 issue = Issue.new.copy_from(1)
30 assert issue.save
30 assert issue.save
31 issue.reload
31 issue.reload
32 orig = Issue.find(1)
32 orig = Issue.find(1)
33 assert_equal orig.subject, issue.subject
33 assert_equal orig.subject, issue.subject
34 assert_equal orig.tracker, issue.tracker
34 assert_equal orig.tracker, issue.tracker
35 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
35 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
36 end
36 end
37
37
38 def test_close_duplicates
38 def test_close_duplicates
39 # Create 3 issues
39 # Create 3 issues
40 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
40 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
41 assert issue1.save
41 assert issue1.save
42 issue2 = issue1.clone
42 issue2 = issue1.clone
43 assert issue2.save
43 assert issue2.save
44 issue3 = issue1.clone
44 issue3 = issue1.clone
45 assert issue3.save
45 assert issue3.save
46
46
47 # 2 is a dupe of 1
47 # 2 is a dupe of 1
48 IssueRelation.create(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
48 IssueRelation.create(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
49 # And 3 is a dupe of 2
49 # And 3 is a dupe of 2
50 IssueRelation.create(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES)
50 IssueRelation.create(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES)
51
51
52 assert issue1.reload.duplicates.include?(issue2)
52 assert issue1.reload.duplicates.include?(issue2)
53
53
54 # Closing issue 1
54 # Closing issue 1
55 issue1.init_journal(User.find(:first), "Closing issue1")
55 issue1.init_journal(User.find(:first), "Closing issue1")
56 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
56 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
57 assert issue1.save
57 assert issue1.save
58 # 2 and 3 should be also closed
58 # 2 and 3 should be also closed
59 assert issue2.reload.closed?
59 assert issue2.reload.closed?
60 assert issue3.reload.closed?
60 assert issue3.reload.closed?
61 end
61 end
62
62
63 def test_move_to_another_project
63 def test_move_to_another_project
64 issue = Issue.find(1)
64 issue = Issue.find(1)
65 assert issue.move_to(Project.find(2))
65 assert issue.move_to(Project.find(2))
66 issue.reload
66 issue.reload
67 assert_equal 2, issue.project_id
67 assert_equal 2, issue.project_id
68 # Category removed
68 # Category removed
69 assert_nil issue.category
69 assert_nil issue.category
70 # Make sure time entries were move to the target project
70 # Make sure time entries were move to the target project
71 assert_equal 2, issue.time_entries.first.project_id
71 assert_equal 2, issue.time_entries.first.project_id
72 end
72 end
73 end
73 end
General Comments 0
You need to be logged in to leave comments. Login now