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