##// END OF EJS Templates
add bulk edit and bulk update actions for time entries (#7996)....
Toshi MARUYAMA -
r5193:8a3151728875
parent child
Show More
@@ -1,268 +1,317
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_project, :only => [:new, :create]
20 before_filter :find_project, :only => [:new, :create]
21 before_filter :find_time_entry, :only => [:show, :edit, :update, :destroy]
21 before_filter :find_time_entry, :only => [:show, :edit, :update, :destroy]
22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update]
22 before_filter :authorize, :except => [:index]
23 before_filter :authorize, :except => [:index]
23 before_filter :find_optional_project, :only => [:index]
24 before_filter :find_optional_project, :only => [:index]
24 accept_key_auth :index, :show, :create, :update, :destroy
25 accept_key_auth :index, :show, :create, :update, :destroy
25
26
26 helper :sort
27 helper :sort
27 include SortHelper
28 include SortHelper
28 helper :issues
29 helper :issues
29 include TimelogHelper
30 include TimelogHelper
30 helper :custom_fields
31 helper :custom_fields
31 include CustomFieldsHelper
32 include CustomFieldsHelper
32
33
33 def index
34 def index
34 sort_init 'spent_on', 'desc'
35 sort_init 'spent_on', 'desc'
35 sort_update 'spent_on' => 'spent_on',
36 sort_update 'spent_on' => 'spent_on',
36 'user' => 'user_id',
37 'user' => 'user_id',
37 'activity' => 'activity_id',
38 'activity' => 'activity_id',
38 'project' => "#{Project.table_name}.name",
39 'project' => "#{Project.table_name}.name",
39 'issue' => 'issue_id',
40 'issue' => 'issue_id',
40 'hours' => 'hours'
41 'hours' => 'hours'
41
42
42 cond = ARCondition.new
43 cond = ARCondition.new
43 if @issue
44 if @issue
44 cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
45 cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
45 elsif @project
46 elsif @project
46 cond << @project.project_condition(Setting.display_subprojects_issues?)
47 cond << @project.project_condition(Setting.display_subprojects_issues?)
47 end
48 end
48
49
49 retrieve_date_range
50 retrieve_date_range
50 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
51 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
51
52
52 respond_to do |format|
53 respond_to do |format|
53 format.html {
54 format.html {
54 # Paginate results
55 # Paginate results
55 @entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions)
56 @entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions)
56 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
57 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
57 @entries = TimeEntry.visible.find(:all,
58 @entries = TimeEntry.visible.find(:all,
58 :include => [:project, :activity, :user, {:issue => :tracker}],
59 :include => [:project, :activity, :user, {:issue => :tracker}],
59 :conditions => cond.conditions,
60 :conditions => cond.conditions,
60 :order => sort_clause,
61 :order => sort_clause,
61 :limit => @entry_pages.items_per_page,
62 :limit => @entry_pages.items_per_page,
62 :offset => @entry_pages.current.offset)
63 :offset => @entry_pages.current.offset)
63 @total_hours = TimeEntry.visible.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f
64 @total_hours = TimeEntry.visible.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f
64
65
65 render :layout => !request.xhr?
66 render :layout => !request.xhr?
66 }
67 }
67 format.api {
68 format.api {
68 @entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions)
69 @entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions)
69 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
70 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
70 @entries = TimeEntry.visible.find(:all,
71 @entries = TimeEntry.visible.find(:all,
71 :include => [:project, :activity, :user, {:issue => :tracker}],
72 :include => [:project, :activity, :user, {:issue => :tracker}],
72 :conditions => cond.conditions,
73 :conditions => cond.conditions,
73 :order => sort_clause,
74 :order => sort_clause,
74 :limit => @entry_pages.items_per_page,
75 :limit => @entry_pages.items_per_page,
75 :offset => @entry_pages.current.offset)
76 :offset => @entry_pages.current.offset)
76 }
77 }
77 format.atom {
78 format.atom {
78 entries = TimeEntry.visible.find(:all,
79 entries = TimeEntry.visible.find(:all,
79 :include => [:project, :activity, :user, {:issue => :tracker}],
80 :include => [:project, :activity, :user, {:issue => :tracker}],
80 :conditions => cond.conditions,
81 :conditions => cond.conditions,
81 :order => "#{TimeEntry.table_name}.created_on DESC",
82 :order => "#{TimeEntry.table_name}.created_on DESC",
82 :limit => Setting.feeds_limit.to_i)
83 :limit => Setting.feeds_limit.to_i)
83 render_feed(entries, :title => l(:label_spent_time))
84 render_feed(entries, :title => l(:label_spent_time))
84 }
85 }
85 format.csv {
86 format.csv {
86 # Export all entries
87 # Export all entries
87 @entries = TimeEntry.visible.find(:all,
88 @entries = TimeEntry.visible.find(:all,
88 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
89 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
89 :conditions => cond.conditions,
90 :conditions => cond.conditions,
90 :order => sort_clause)
91 :order => sort_clause)
91 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
92 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
92 }
93 }
93 end
94 end
94 end
95 end
95
96
96 def show
97 def show
97 respond_to do |format|
98 respond_to do |format|
98 # TODO: Implement html response
99 # TODO: Implement html response
99 format.html { render :nothing => true, :status => 406 }
100 format.html { render :nothing => true, :status => 406 }
100 format.api
101 format.api
101 end
102 end
102 end
103 end
103
104
104 def new
105 def new
105 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
106 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
106 @time_entry.attributes = params[:time_entry]
107 @time_entry.attributes = params[:time_entry]
107
108
108 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
109 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
109 render :action => 'edit'
110 render :action => 'edit'
110 end
111 end
111
112
112 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
113 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
113 def create
114 def create
114 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
115 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
115 @time_entry.attributes = params[:time_entry]
116 @time_entry.attributes = params[:time_entry]
116
117
117 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
118 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
118
119
119 if @time_entry.save
120 if @time_entry.save
120 respond_to do |format|
121 respond_to do |format|
121 format.html {
122 format.html {
122 flash[:notice] = l(:notice_successful_update)
123 flash[:notice] = l(:notice_successful_update)
123 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
124 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
124 }
125 }
125 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
126 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
126 end
127 end
127 else
128 else
128 respond_to do |format|
129 respond_to do |format|
129 format.html { render :action => 'edit' }
130 format.html { render :action => 'edit' }
130 format.api { render_validation_errors(@time_entry) }
131 format.api { render_validation_errors(@time_entry) }
131 end
132 end
132 end
133 end
133 end
134 end
134
135
135 def edit
136 def edit
136 @time_entry.attributes = params[:time_entry]
137 @time_entry.attributes = params[:time_entry]
137
138
138 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
139 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
139 end
140 end
140
141
141 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
142 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
142 def update
143 def update
143 @time_entry.attributes = params[:time_entry]
144 @time_entry.attributes = params[:time_entry]
144
145
145 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
146 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
146
147
147 if @time_entry.save
148 if @time_entry.save
148 respond_to do |format|
149 respond_to do |format|
149 format.html {
150 format.html {
150 flash[:notice] = l(:notice_successful_update)
151 flash[:notice] = l(:notice_successful_update)
151 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
152 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
152 }
153 }
153 format.api { head :ok }
154 format.api { head :ok }
154 end
155 end
155 else
156 else
156 respond_to do |format|
157 respond_to do |format|
157 format.html { render :action => 'edit' }
158 format.html { render :action => 'edit' }
158 format.api { render_validation_errors(@time_entry) }
159 format.api { render_validation_errors(@time_entry) }
159 end
160 end
160 end
161 end
161 end
162 end
162
163
164 def bulk_edit
165 @available_activities = TimeEntryActivity.shared.active
166 @custom_fields = TimeEntry.first.available_custom_fields
167 end
168
169 def bulk_update
170 attributes = parse_params_for_bulk_time_entry_attributes(params)
171
172 unsaved_time_entry_ids = []
173 @time_entries.each do |time_entry|
174 time_entry.reload
175 time_entry.attributes = attributes
176 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
177 unless time_entry.save
178 # Keep unsaved time_entry ids to display them in flash error
179 unsaved_time_entry_ids << time_entry.id
180 end
181 end
182 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
183 redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @project})
184 end
185
163 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
186 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
164 def destroy
187 def destroy
165 if @time_entry.destroy && @time_entry.destroyed?
188 if @time_entry.destroy && @time_entry.destroyed?
166 respond_to do |format|
189 respond_to do |format|
167 format.html {
190 format.html {
168 flash[:notice] = l(:notice_successful_delete)
191 flash[:notice] = l(:notice_successful_delete)
169 redirect_to :back
192 redirect_to :back
170 }
193 }
171 format.api { head :ok }
194 format.api { head :ok }
172 end
195 end
173 else
196 else
174 respond_to do |format|
197 respond_to do |format|
175 format.html {
198 format.html {
176 flash[:error] = l(:notice_unable_delete_time_entry)
199 flash[:error] = l(:notice_unable_delete_time_entry)
177 redirect_to :back
200 redirect_to :back
178 }
201 }
179 format.api { render_validation_errors(@time_entry) }
202 format.api { render_validation_errors(@time_entry) }
180 end
203 end
181 end
204 end
182 rescue ::ActionController::RedirectBackError
205 rescue ::ActionController::RedirectBackError
183 redirect_to :action => 'index', :project_id => @time_entry.project
206 redirect_to :action => 'index', :project_id => @time_entry.project
184 end
207 end
185
208
186 private
209 private
187 def find_time_entry
210 def find_time_entry
188 @time_entry = TimeEntry.find(params[:id])
211 @time_entry = TimeEntry.find(params[:id])
189 unless @time_entry.editable_by?(User.current)
212 unless @time_entry.editable_by?(User.current)
190 render_403
213 render_403
191 return false
214 return false
192 end
215 end
193 @project = @time_entry.project
216 @project = @time_entry.project
194 rescue ActiveRecord::RecordNotFound
217 rescue ActiveRecord::RecordNotFound
195 render_404
218 render_404
196 end
219 end
197
220
221 def find_time_entries
222 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
223 raise ActiveRecord::RecordNotFound if @time_entries.empty?
224 @projects = @time_entries.collect(&:project).compact.uniq
225 @project = @projects.first if @projects.size == 1
226 rescue ActiveRecord::RecordNotFound
227 render_404
228 end
229
230 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
231 if unsaved_time_entry_ids.empty?
232 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
233 else
234 flash[:error] = l(:notice_failed_to_save_time_entries,
235 :count => unsaved_time_entry_ids.size,
236 :total => time_entries.size,
237 :ids => '#' + unsaved_time_entry_ids.join(', #'))
238 end
239 end
240
198 def find_project
241 def find_project
199 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
242 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
200 @issue = Issue.find(issue_id)
243 @issue = Issue.find(issue_id)
201 @project = @issue.project
244 @project = @issue.project
202 elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
245 elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
203 @project = Project.find(project_id)
246 @project = Project.find(project_id)
204 else
247 else
205 render_404
248 render_404
206 return false
249 return false
207 end
250 end
208 rescue ActiveRecord::RecordNotFound
251 rescue ActiveRecord::RecordNotFound
209 render_404
252 render_404
210 end
253 end
211
254
212 def find_optional_project
255 def find_optional_project
213 if !params[:issue_id].blank?
256 if !params[:issue_id].blank?
214 @issue = Issue.find(params[:issue_id])
257 @issue = Issue.find(params[:issue_id])
215 @project = @issue.project
258 @project = @issue.project
216 elsif !params[:project_id].blank?
259 elsif !params[:project_id].blank?
217 @project = Project.find(params[:project_id])
260 @project = Project.find(params[:project_id])
218 end
261 end
219 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
262 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
220 end
263 end
221
264
222 # Retrieves the date range based on predefined ranges or specific from/to param dates
265 # Retrieves the date range based on predefined ranges or specific from/to param dates
223 def retrieve_date_range
266 def retrieve_date_range
224 @free_period = false
267 @free_period = false
225 @from, @to = nil, nil
268 @from, @to = nil, nil
226
269
227 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
270 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
228 case params[:period].to_s
271 case params[:period].to_s
229 when 'today'
272 when 'today'
230 @from = @to = Date.today
273 @from = @to = Date.today
231 when 'yesterday'
274 when 'yesterday'
232 @from = @to = Date.today - 1
275 @from = @to = Date.today - 1
233 when 'current_week'
276 when 'current_week'
234 @from = Date.today - (Date.today.cwday - 1)%7
277 @from = Date.today - (Date.today.cwday - 1)%7
235 @to = @from + 6
278 @to = @from + 6
236 when 'last_week'
279 when 'last_week'
237 @from = Date.today - 7 - (Date.today.cwday - 1)%7
280 @from = Date.today - 7 - (Date.today.cwday - 1)%7
238 @to = @from + 6
281 @to = @from + 6
239 when '7_days'
282 when '7_days'
240 @from = Date.today - 7
283 @from = Date.today - 7
241 @to = Date.today
284 @to = Date.today
242 when 'current_month'
285 when 'current_month'
243 @from = Date.civil(Date.today.year, Date.today.month, 1)
286 @from = Date.civil(Date.today.year, Date.today.month, 1)
244 @to = (@from >> 1) - 1
287 @to = (@from >> 1) - 1
245 when 'last_month'
288 when 'last_month'
246 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
289 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
247 @to = (@from >> 1) - 1
290 @to = (@from >> 1) - 1
248 when '30_days'
291 when '30_days'
249 @from = Date.today - 30
292 @from = Date.today - 30
250 @to = Date.today
293 @to = Date.today
251 when 'current_year'
294 when 'current_year'
252 @from = Date.civil(Date.today.year, 1, 1)
295 @from = Date.civil(Date.today.year, 1, 1)
253 @to = Date.civil(Date.today.year, 12, 31)
296 @to = Date.civil(Date.today.year, 12, 31)
254 end
297 end
255 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
298 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
256 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
299 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
257 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
300 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
258 @free_period = true
301 @free_period = true
259 else
302 else
260 # default
303 # default
261 end
304 end
262
305
263 @from, @to = @to, @from if @from && @to && @from > @to
306 @from, @to = @to, @from if @from && @to && @from > @to
264 @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
307 @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
265 @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
308 @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
266 end
309 end
267
310
311 def parse_params_for_bulk_time_entry_attributes(params)
312 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
313 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
314 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
315 attributes
316 end
268 end
317 end
@@ -1,839 +1,845
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
24
24
25 # Maximum length for project identifiers
25 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
27
27
28 # Specific overidden Activities
28 # Specific overidden Activities
29 has_many :time_entry_activities
29 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
33 :include => :principal,
33 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members
35 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
37
37
38 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :destroy, :include => :author
46 has_many :news, :dependent => :destroy, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :dependent => :destroy
49 has_one :repository, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField',
54 :class_name => 'IssueCustomField',
55 :order => "#{CustomField.table_name}.position",
55 :order => "#{CustomField.table_name}.position",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_nested_set :order => 'name', :dependent => :destroy
59 acts_as_nested_set :order => 'name', :dependent => :destroy
60 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only
77 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words
79 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new )
80 validates_exclusion_of :identifier, :in => %w( new )
81
81
82 before_destroy :delete_all_members
82 before_destroy :delete_all_members
83
83
84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :all_public, { :conditions => { :is_public => true } }
86 named_scope :all_public, { :conditions => { :is_public => true } }
87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
88
88
89 def initialize(attributes = nil)
89 def initialize(attributes = nil)
90 super
90 super
91
91
92 initialized = (attributes || {}).stringify_keys
92 initialized = (attributes || {}).stringify_keys
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
94 self.identifier = Project.next_identifier
94 self.identifier = Project.next_identifier
95 end
95 end
96 if !initialized.key?('is_public')
96 if !initialized.key?('is_public')
97 self.is_public = Setting.default_projects_public?
97 self.is_public = Setting.default_projects_public?
98 end
98 end
99 if !initialized.key?('enabled_module_names')
99 if !initialized.key?('enabled_module_names')
100 self.enabled_module_names = Setting.default_projects_modules
100 self.enabled_module_names = Setting.default_projects_modules
101 end
101 end
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
103 self.trackers = Tracker.all
103 self.trackers = Tracker.all
104 end
104 end
105 end
105 end
106
106
107 def identifier=(identifier)
107 def identifier=(identifier)
108 super unless identifier_frozen?
108 super unless identifier_frozen?
109 end
109 end
110
110
111 def identifier_frozen?
111 def identifier_frozen?
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
113 end
113 end
114
114
115 # returns latest created projects
115 # returns latest created projects
116 # non public projects will be returned only if user is a member of those
116 # non public projects will be returned only if user is a member of those
117 def self.latest(user=nil, count=5)
117 def self.latest(user=nil, count=5)
118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
119 end
119 end
120
120
121 # Returns a SQL :conditions string used to find all active projects for the specified user.
121 # Returns a SQL :conditions string used to find all active projects for the specified user.
122 #
122 #
123 # Examples:
123 # Examples:
124 # Projects.visible_by(admin) => "projects.status = 1"
124 # Projects.visible_by(admin) => "projects.status = 1"
125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
126 def self.visible_by(user=nil)
126 def self.visible_by(user=nil)
127 user ||= User.current
127 user ||= User.current
128 if user && user.admin?
128 if user && user.admin?
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
130 elsif user && user.memberships.any?
130 elsif user && user.memberships.any?
131 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(',')}))"
131 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(',')}))"
132 else
132 else
133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
134 end
134 end
135 end
135 end
136
136
137 def self.allowed_to_condition(user, permission, options={})
137 def self.allowed_to_condition(user, permission, options={})
138 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
138 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
139 if perm = Redmine::AccessControl.permission(permission)
139 if perm = Redmine::AccessControl.permission(permission)
140 unless perm.project_module.nil?
140 unless perm.project_module.nil?
141 # If the permission belongs to a project module, make sure the module is enabled
141 # If the permission belongs to a project module, make sure the module is enabled
142 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
142 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
143 end
143 end
144 end
144 end
145 if options[:project]
145 if options[:project]
146 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
146 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
147 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
147 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
148 base_statement = "(#{project_statement}) AND (#{base_statement})"
148 base_statement = "(#{project_statement}) AND (#{base_statement})"
149 end
149 end
150
150
151 if user.admin?
151 if user.admin?
152 base_statement
152 base_statement
153 else
153 else
154 statement_by_role = {}
154 statement_by_role = {}
155 if user.logged?
155 if user.logged?
156 if Role.non_member.allowed_to?(permission) && !options[:member]
156 if Role.non_member.allowed_to?(permission) && !options[:member]
157 statement_by_role[Role.non_member] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
157 statement_by_role[Role.non_member] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
158 end
158 end
159 user.projects_by_role.each do |role, projects|
159 user.projects_by_role.each do |role, projects|
160 if role.allowed_to?(permission)
160 if role.allowed_to?(permission)
161 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
161 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
162 end
162 end
163 end
163 end
164 else
164 else
165 if Role.anonymous.allowed_to?(permission) && !options[:member]
165 if Role.anonymous.allowed_to?(permission) && !options[:member]
166 statement_by_role[Role.anonymous] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
166 statement_by_role[Role.anonymous] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
167 end
167 end
168 end
168 end
169 if statement_by_role.empty?
169 if statement_by_role.empty?
170 "1=0"
170 "1=0"
171 else
171 else
172 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
172 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
173 end
173 end
174 end
174 end
175 end
175 end
176
176
177 # Returns the Systemwide and project specific activities
177 # Returns the Systemwide and project specific activities
178 def activities(include_inactive=false)
178 def activities(include_inactive=false)
179 if include_inactive
179 if include_inactive
180 return all_activities
180 return all_activities
181 else
181 else
182 return active_activities
182 return active_activities
183 end
183 end
184 end
184 end
185
185
186 # Will create a new Project specific Activity or update an existing one
186 # Will create a new Project specific Activity or update an existing one
187 #
187 #
188 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
188 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
189 # does not successfully save.
189 # does not successfully save.
190 def update_or_create_time_entry_activity(id, activity_hash)
190 def update_or_create_time_entry_activity(id, activity_hash)
191 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
191 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
192 self.create_time_entry_activity_if_needed(activity_hash)
192 self.create_time_entry_activity_if_needed(activity_hash)
193 else
193 else
194 activity = project.time_entry_activities.find_by_id(id.to_i)
194 activity = project.time_entry_activities.find_by_id(id.to_i)
195 activity.update_attributes(activity_hash) if activity
195 activity.update_attributes(activity_hash) if activity
196 end
196 end
197 end
197 end
198
198
199 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
199 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
200 #
200 #
201 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
201 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
202 # does not successfully save.
202 # does not successfully save.
203 def create_time_entry_activity_if_needed(activity)
203 def create_time_entry_activity_if_needed(activity)
204 if activity['parent_id']
204 if activity['parent_id']
205
205
206 parent_activity = TimeEntryActivity.find(activity['parent_id'])
206 parent_activity = TimeEntryActivity.find(activity['parent_id'])
207 activity['name'] = parent_activity.name
207 activity['name'] = parent_activity.name
208 activity['position'] = parent_activity.position
208 activity['position'] = parent_activity.position
209
209
210 if Enumeration.overridding_change?(activity, parent_activity)
210 if Enumeration.overridding_change?(activity, parent_activity)
211 project_activity = self.time_entry_activities.create(activity)
211 project_activity = self.time_entry_activities.create(activity)
212
212
213 if project_activity.new_record?
213 if project_activity.new_record?
214 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
214 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
215 else
215 else
216 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
216 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
217 end
217 end
218 end
218 end
219 end
219 end
220 end
220 end
221
221
222 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
222 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
223 #
223 #
224 # Examples:
224 # Examples:
225 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
225 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
226 # project.project_condition(false) => "projects.id = 1"
226 # project.project_condition(false) => "projects.id = 1"
227 def project_condition(with_subprojects)
227 def project_condition(with_subprojects)
228 cond = "#{Project.table_name}.id = #{id}"
228 cond = "#{Project.table_name}.id = #{id}"
229 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
229 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
230 cond
230 cond
231 end
231 end
232
232
233 def self.find(*args)
233 def self.find(*args)
234 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
234 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
235 project = find_by_identifier(*args)
235 project = find_by_identifier(*args)
236 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
236 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
237 project
237 project
238 else
238 else
239 super
239 super
240 end
240 end
241 end
241 end
242
242
243 def to_param
243 def to_param
244 # id is used for projects with a numeric identifier (compatibility)
244 # id is used for projects with a numeric identifier (compatibility)
245 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
245 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
246 end
246 end
247
247
248 def active?
248 def active?
249 self.status == STATUS_ACTIVE
249 self.status == STATUS_ACTIVE
250 end
250 end
251
251
252 def archived?
252 def archived?
253 self.status == STATUS_ARCHIVED
253 self.status == STATUS_ARCHIVED
254 end
254 end
255
255
256 # Archives the project and its descendants
256 # Archives the project and its descendants
257 def archive
257 def archive
258 # Check that there is no issue of a non descendant project that is assigned
258 # Check that there is no issue of a non descendant project that is assigned
259 # to one of the project or descendant versions
259 # to one of the project or descendant versions
260 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
260 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
261 if v_ids.any? && Issue.find(:first, :include => :project,
261 if v_ids.any? && Issue.find(:first, :include => :project,
262 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
262 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
263 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
263 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
264 return false
264 return false
265 end
265 end
266 Project.transaction do
266 Project.transaction do
267 archive!
267 archive!
268 end
268 end
269 true
269 true
270 end
270 end
271
271
272 # Unarchives the project
272 # Unarchives the project
273 # All its ancestors must be active
273 # All its ancestors must be active
274 def unarchive
274 def unarchive
275 return false if ancestors.detect {|a| !a.active?}
275 return false if ancestors.detect {|a| !a.active?}
276 update_attribute :status, STATUS_ACTIVE
276 update_attribute :status, STATUS_ACTIVE
277 end
277 end
278
278
279 # Returns an array of projects the project can be moved to
279 # Returns an array of projects the project can be moved to
280 # by the current user
280 # by the current user
281 def allowed_parents
281 def allowed_parents
282 return @allowed_parents if @allowed_parents
282 return @allowed_parents if @allowed_parents
283 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
283 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
284 @allowed_parents = @allowed_parents - self_and_descendants
284 @allowed_parents = @allowed_parents - self_and_descendants
285 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
285 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
286 @allowed_parents << nil
286 @allowed_parents << nil
287 end
287 end
288 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
288 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
289 @allowed_parents << parent
289 @allowed_parents << parent
290 end
290 end
291 @allowed_parents
291 @allowed_parents
292 end
292 end
293
293
294 # Sets the parent of the project with authorization check
294 # Sets the parent of the project with authorization check
295 def set_allowed_parent!(p)
295 def set_allowed_parent!(p)
296 unless p.nil? || p.is_a?(Project)
296 unless p.nil? || p.is_a?(Project)
297 if p.to_s.blank?
297 if p.to_s.blank?
298 p = nil
298 p = nil
299 else
299 else
300 p = Project.find_by_id(p)
300 p = Project.find_by_id(p)
301 return false unless p
301 return false unless p
302 end
302 end
303 end
303 end
304 if p.nil?
304 if p.nil?
305 if !new_record? && allowed_parents.empty?
305 if !new_record? && allowed_parents.empty?
306 return false
306 return false
307 end
307 end
308 elsif !allowed_parents.include?(p)
308 elsif !allowed_parents.include?(p)
309 return false
309 return false
310 end
310 end
311 set_parent!(p)
311 set_parent!(p)
312 end
312 end
313
313
314 # Sets the parent of the project
314 # Sets the parent of the project
315 # Argument can be either a Project, a String, a Fixnum or nil
315 # Argument can be either a Project, a String, a Fixnum or nil
316 def set_parent!(p)
316 def set_parent!(p)
317 unless p.nil? || p.is_a?(Project)
317 unless p.nil? || p.is_a?(Project)
318 if p.to_s.blank?
318 if p.to_s.blank?
319 p = nil
319 p = nil
320 else
320 else
321 p = Project.find_by_id(p)
321 p = Project.find_by_id(p)
322 return false unless p
322 return false unless p
323 end
323 end
324 end
324 end
325 if p == parent && !p.nil?
325 if p == parent && !p.nil?
326 # Nothing to do
326 # Nothing to do
327 true
327 true
328 elsif p.nil? || (p.active? && move_possible?(p))
328 elsif p.nil? || (p.active? && move_possible?(p))
329 # Insert the project so that target's children or root projects stay alphabetically sorted
329 # Insert the project so that target's children or root projects stay alphabetically sorted
330 sibs = (p.nil? ? self.class.roots : p.children)
330 sibs = (p.nil? ? self.class.roots : p.children)
331 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
331 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
332 if to_be_inserted_before
332 if to_be_inserted_before
333 move_to_left_of(to_be_inserted_before)
333 move_to_left_of(to_be_inserted_before)
334 elsif p.nil?
334 elsif p.nil?
335 if sibs.empty?
335 if sibs.empty?
336 # move_to_root adds the project in first (ie. left) position
336 # move_to_root adds the project in first (ie. left) position
337 move_to_root
337 move_to_root
338 else
338 else
339 move_to_right_of(sibs.last) unless self == sibs.last
339 move_to_right_of(sibs.last) unless self == sibs.last
340 end
340 end
341 else
341 else
342 # move_to_child_of adds the project in last (ie.right) position
342 # move_to_child_of adds the project in last (ie.right) position
343 move_to_child_of(p)
343 move_to_child_of(p)
344 end
344 end
345 Issue.update_versions_from_hierarchy_change(self)
345 Issue.update_versions_from_hierarchy_change(self)
346 true
346 true
347 else
347 else
348 # Can not move to the given target
348 # Can not move to the given target
349 false
349 false
350 end
350 end
351 end
351 end
352
352
353 # Returns an array of the trackers used by the project and its active sub projects
353 # Returns an array of the trackers used by the project and its active sub projects
354 def rolled_up_trackers
354 def rolled_up_trackers
355 @rolled_up_trackers ||=
355 @rolled_up_trackers ||=
356 Tracker.find(:all, :joins => :projects,
356 Tracker.find(:all, :joins => :projects,
357 :select => "DISTINCT #{Tracker.table_name}.*",
357 :select => "DISTINCT #{Tracker.table_name}.*",
358 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
358 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
359 :order => "#{Tracker.table_name}.position")
359 :order => "#{Tracker.table_name}.position")
360 end
360 end
361
361
362 # Closes open and locked project versions that are completed
362 # Closes open and locked project versions that are completed
363 def close_completed_versions
363 def close_completed_versions
364 Version.transaction do
364 Version.transaction do
365 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
365 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
366 if version.completed?
366 if version.completed?
367 version.update_attribute(:status, 'closed')
367 version.update_attribute(:status, 'closed')
368 end
368 end
369 end
369 end
370 end
370 end
371 end
371 end
372
372
373 # Returns a scope of the Versions on subprojects
373 # Returns a scope of the Versions on subprojects
374 def rolled_up_versions
374 def rolled_up_versions
375 @rolled_up_versions ||=
375 @rolled_up_versions ||=
376 Version.scoped(:include => :project,
376 Version.scoped(:include => :project,
377 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
377 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
378 end
378 end
379
379
380 # Returns a scope of the Versions used by the project
380 # Returns a scope of the Versions used by the project
381 def shared_versions
381 def shared_versions
382 @shared_versions ||= begin
382 @shared_versions ||= begin
383 r = root? ? self : root
383 r = root? ? self : root
384 Version.scoped(:include => :project,
384 Version.scoped(:include => :project,
385 :conditions => "#{Project.table_name}.id = #{id}" +
385 :conditions => "#{Project.table_name}.id = #{id}" +
386 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
386 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
387 " #{Version.table_name}.sharing = 'system'" +
387 " #{Version.table_name}.sharing = 'system'" +
388 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
388 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
389 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
389 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
390 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
390 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
391 "))")
391 "))")
392 end
392 end
393 end
393 end
394
394
395 # Returns a hash of project users grouped by role
395 # Returns a hash of project users grouped by role
396 def users_by_role
396 def users_by_role
397 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
397 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
398 m.roles.each do |r|
398 m.roles.each do |r|
399 h[r] ||= []
399 h[r] ||= []
400 h[r] << m.user
400 h[r] << m.user
401 end
401 end
402 h
402 h
403 end
403 end
404 end
404 end
405
405
406 # Deletes all project's members
406 # Deletes all project's members
407 def delete_all_members
407 def delete_all_members
408 me, mr = Member.table_name, MemberRole.table_name
408 me, mr = Member.table_name, MemberRole.table_name
409 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
409 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
410 Member.delete_all(['project_id = ?', id])
410 Member.delete_all(['project_id = ?', id])
411 end
411 end
412
412
413 # Users issues can be assigned to
413 # Users issues can be assigned to
414 def assignable_users
414 def assignable_users
415 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
415 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
416 end
416 end
417
417
418 # Returns the mail adresses of users that should be always notified on project events
418 # Returns the mail adresses of users that should be always notified on project events
419 def recipients
419 def recipients
420 notified_users.collect {|user| user.mail}
420 notified_users.collect {|user| user.mail}
421 end
421 end
422
422
423 # Returns the users that should be notified on project events
423 # Returns the users that should be notified on project events
424 def notified_users
424 def notified_users
425 # TODO: User part should be extracted to User#notify_about?
425 # TODO: User part should be extracted to User#notify_about?
426 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
426 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
427 end
427 end
428
428
429 # Returns an array of all custom fields enabled for project issues
429 # Returns an array of all custom fields enabled for project issues
430 # (explictly associated custom fields and custom fields enabled for all projects)
430 # (explictly associated custom fields and custom fields enabled for all projects)
431 def all_issue_custom_fields
431 def all_issue_custom_fields
432 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
432 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
433 end
433 end
434
435 # Returns an array of all custom fields enabled for project time entries
436 # (explictly associated custom fields and custom fields enabled for all projects)
437 def all_time_entry_custom_fields
438 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
439 end
434
440
435 def project
441 def project
436 self
442 self
437 end
443 end
438
444
439 def <=>(project)
445 def <=>(project)
440 name.downcase <=> project.name.downcase
446 name.downcase <=> project.name.downcase
441 end
447 end
442
448
443 def to_s
449 def to_s
444 name
450 name
445 end
451 end
446
452
447 # Returns a short description of the projects (first lines)
453 # Returns a short description of the projects (first lines)
448 def short_description(length = 255)
454 def short_description(length = 255)
449 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
455 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
450 end
456 end
451
457
452 def css_classes
458 def css_classes
453 s = 'project'
459 s = 'project'
454 s << ' root' if root?
460 s << ' root' if root?
455 s << ' child' if child?
461 s << ' child' if child?
456 s << (leaf? ? ' leaf' : ' parent')
462 s << (leaf? ? ' leaf' : ' parent')
457 s
463 s
458 end
464 end
459
465
460 # The earliest start date of a project, based on it's issues and versions
466 # The earliest start date of a project, based on it's issues and versions
461 def start_date
467 def start_date
462 [
468 [
463 issues.minimum('start_date'),
469 issues.minimum('start_date'),
464 shared_versions.collect(&:effective_date),
470 shared_versions.collect(&:effective_date),
465 shared_versions.collect(&:start_date)
471 shared_versions.collect(&:start_date)
466 ].flatten.compact.min
472 ].flatten.compact.min
467 end
473 end
468
474
469 # The latest due date of an issue or version
475 # The latest due date of an issue or version
470 def due_date
476 def due_date
471 [
477 [
472 issues.maximum('due_date'),
478 issues.maximum('due_date'),
473 shared_versions.collect(&:effective_date),
479 shared_versions.collect(&:effective_date),
474 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
480 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
475 ].flatten.compact.max
481 ].flatten.compact.max
476 end
482 end
477
483
478 def overdue?
484 def overdue?
479 active? && !due_date.nil? && (due_date < Date.today)
485 active? && !due_date.nil? && (due_date < Date.today)
480 end
486 end
481
487
482 # Returns the percent completed for this project, based on the
488 # Returns the percent completed for this project, based on the
483 # progress on it's versions.
489 # progress on it's versions.
484 def completed_percent(options={:include_subprojects => false})
490 def completed_percent(options={:include_subprojects => false})
485 if options.delete(:include_subprojects)
491 if options.delete(:include_subprojects)
486 total = self_and_descendants.collect(&:completed_percent).sum
492 total = self_and_descendants.collect(&:completed_percent).sum
487
493
488 total / self_and_descendants.count
494 total / self_and_descendants.count
489 else
495 else
490 if versions.count > 0
496 if versions.count > 0
491 total = versions.collect(&:completed_pourcent).sum
497 total = versions.collect(&:completed_pourcent).sum
492
498
493 total / versions.count
499 total / versions.count
494 else
500 else
495 100
501 100
496 end
502 end
497 end
503 end
498 end
504 end
499
505
500 # Return true if this project is allowed to do the specified action.
506 # Return true if this project is allowed to do the specified action.
501 # action can be:
507 # action can be:
502 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
508 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
503 # * a permission Symbol (eg. :edit_project)
509 # * a permission Symbol (eg. :edit_project)
504 def allows_to?(action)
510 def allows_to?(action)
505 if action.is_a? Hash
511 if action.is_a? Hash
506 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
512 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
507 else
513 else
508 allowed_permissions.include? action
514 allowed_permissions.include? action
509 end
515 end
510 end
516 end
511
517
512 def module_enabled?(module_name)
518 def module_enabled?(module_name)
513 module_name = module_name.to_s
519 module_name = module_name.to_s
514 enabled_modules.detect {|m| m.name == module_name}
520 enabled_modules.detect {|m| m.name == module_name}
515 end
521 end
516
522
517 def enabled_module_names=(module_names)
523 def enabled_module_names=(module_names)
518 if module_names && module_names.is_a?(Array)
524 if module_names && module_names.is_a?(Array)
519 module_names = module_names.collect(&:to_s).reject(&:blank?)
525 module_names = module_names.collect(&:to_s).reject(&:blank?)
520 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
526 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
521 else
527 else
522 enabled_modules.clear
528 enabled_modules.clear
523 end
529 end
524 end
530 end
525
531
526 # Returns an array of the enabled modules names
532 # Returns an array of the enabled modules names
527 def enabled_module_names
533 def enabled_module_names
528 enabled_modules.collect(&:name)
534 enabled_modules.collect(&:name)
529 end
535 end
530
536
531 safe_attributes 'name',
537 safe_attributes 'name',
532 'description',
538 'description',
533 'homepage',
539 'homepage',
534 'is_public',
540 'is_public',
535 'identifier',
541 'identifier',
536 'custom_field_values',
542 'custom_field_values',
537 'custom_fields',
543 'custom_fields',
538 'tracker_ids',
544 'tracker_ids',
539 'issue_custom_field_ids'
545 'issue_custom_field_ids'
540
546
541 safe_attributes 'enabled_module_names',
547 safe_attributes 'enabled_module_names',
542 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
548 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
543
549
544 # Returns an array of projects that are in this project's hierarchy
550 # Returns an array of projects that are in this project's hierarchy
545 #
551 #
546 # Example: parents, children, siblings
552 # Example: parents, children, siblings
547 def hierarchy
553 def hierarchy
548 parents = project.self_and_ancestors || []
554 parents = project.self_and_ancestors || []
549 descendants = project.descendants || []
555 descendants = project.descendants || []
550 project_hierarchy = parents | descendants # Set union
556 project_hierarchy = parents | descendants # Set union
551 end
557 end
552
558
553 # Returns an auto-generated project identifier based on the last identifier used
559 # Returns an auto-generated project identifier based on the last identifier used
554 def self.next_identifier
560 def self.next_identifier
555 p = Project.find(:first, :order => 'created_on DESC')
561 p = Project.find(:first, :order => 'created_on DESC')
556 p.nil? ? nil : p.identifier.to_s.succ
562 p.nil? ? nil : p.identifier.to_s.succ
557 end
563 end
558
564
559 # Copies and saves the Project instance based on the +project+.
565 # Copies and saves the Project instance based on the +project+.
560 # Duplicates the source project's:
566 # Duplicates the source project's:
561 # * Wiki
567 # * Wiki
562 # * Versions
568 # * Versions
563 # * Categories
569 # * Categories
564 # * Issues
570 # * Issues
565 # * Members
571 # * Members
566 # * Queries
572 # * Queries
567 #
573 #
568 # Accepts an +options+ argument to specify what to copy
574 # Accepts an +options+ argument to specify what to copy
569 #
575 #
570 # Examples:
576 # Examples:
571 # project.copy(1) # => copies everything
577 # project.copy(1) # => copies everything
572 # project.copy(1, :only => 'members') # => copies members only
578 # project.copy(1, :only => 'members') # => copies members only
573 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
579 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
574 def copy(project, options={})
580 def copy(project, options={})
575 project = project.is_a?(Project) ? project : Project.find(project)
581 project = project.is_a?(Project) ? project : Project.find(project)
576
582
577 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
583 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
578 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
584 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
579
585
580 Project.transaction do
586 Project.transaction do
581 if save
587 if save
582 reload
588 reload
583 to_be_copied.each do |name|
589 to_be_copied.each do |name|
584 send "copy_#{name}", project
590 send "copy_#{name}", project
585 end
591 end
586 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
592 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
587 save
593 save
588 end
594 end
589 end
595 end
590 end
596 end
591
597
592
598
593 # Copies +project+ and returns the new instance. This will not save
599 # Copies +project+ and returns the new instance. This will not save
594 # the copy
600 # the copy
595 def self.copy_from(project)
601 def self.copy_from(project)
596 begin
602 begin
597 project = project.is_a?(Project) ? project : Project.find(project)
603 project = project.is_a?(Project) ? project : Project.find(project)
598 if project
604 if project
599 # clear unique attributes
605 # clear unique attributes
600 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
606 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
601 copy = Project.new(attributes)
607 copy = Project.new(attributes)
602 copy.enabled_modules = project.enabled_modules
608 copy.enabled_modules = project.enabled_modules
603 copy.trackers = project.trackers
609 copy.trackers = project.trackers
604 copy.custom_values = project.custom_values.collect {|v| v.clone}
610 copy.custom_values = project.custom_values.collect {|v| v.clone}
605 copy.issue_custom_fields = project.issue_custom_fields
611 copy.issue_custom_fields = project.issue_custom_fields
606 return copy
612 return copy
607 else
613 else
608 return nil
614 return nil
609 end
615 end
610 rescue ActiveRecord::RecordNotFound
616 rescue ActiveRecord::RecordNotFound
611 return nil
617 return nil
612 end
618 end
613 end
619 end
614
620
615 # Yields the given block for each project with its level in the tree
621 # Yields the given block for each project with its level in the tree
616 def self.project_tree(projects, &block)
622 def self.project_tree(projects, &block)
617 ancestors = []
623 ancestors = []
618 projects.sort_by(&:lft).each do |project|
624 projects.sort_by(&:lft).each do |project|
619 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
625 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
620 ancestors.pop
626 ancestors.pop
621 end
627 end
622 yield project, ancestors.size
628 yield project, ancestors.size
623 ancestors << project
629 ancestors << project
624 end
630 end
625 end
631 end
626
632
627 private
633 private
628
634
629 # Copies wiki from +project+
635 # Copies wiki from +project+
630 def copy_wiki(project)
636 def copy_wiki(project)
631 # Check that the source project has a wiki first
637 # Check that the source project has a wiki first
632 unless project.wiki.nil?
638 unless project.wiki.nil?
633 self.wiki ||= Wiki.new
639 self.wiki ||= Wiki.new
634 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
640 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
635 wiki_pages_map = {}
641 wiki_pages_map = {}
636 project.wiki.pages.each do |page|
642 project.wiki.pages.each do |page|
637 # Skip pages without content
643 # Skip pages without content
638 next if page.content.nil?
644 next if page.content.nil?
639 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
645 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
640 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
646 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
641 new_wiki_page.content = new_wiki_content
647 new_wiki_page.content = new_wiki_content
642 wiki.pages << new_wiki_page
648 wiki.pages << new_wiki_page
643 wiki_pages_map[page.id] = new_wiki_page
649 wiki_pages_map[page.id] = new_wiki_page
644 end
650 end
645 wiki.save
651 wiki.save
646 # Reproduce page hierarchy
652 # Reproduce page hierarchy
647 project.wiki.pages.each do |page|
653 project.wiki.pages.each do |page|
648 if page.parent_id && wiki_pages_map[page.id]
654 if page.parent_id && wiki_pages_map[page.id]
649 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
655 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
650 wiki_pages_map[page.id].save
656 wiki_pages_map[page.id].save
651 end
657 end
652 end
658 end
653 end
659 end
654 end
660 end
655
661
656 # Copies versions from +project+
662 # Copies versions from +project+
657 def copy_versions(project)
663 def copy_versions(project)
658 project.versions.each do |version|
664 project.versions.each do |version|
659 new_version = Version.new
665 new_version = Version.new
660 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
666 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
661 self.versions << new_version
667 self.versions << new_version
662 end
668 end
663 end
669 end
664
670
665 # Copies issue categories from +project+
671 # Copies issue categories from +project+
666 def copy_issue_categories(project)
672 def copy_issue_categories(project)
667 project.issue_categories.each do |issue_category|
673 project.issue_categories.each do |issue_category|
668 new_issue_category = IssueCategory.new
674 new_issue_category = IssueCategory.new
669 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
675 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
670 self.issue_categories << new_issue_category
676 self.issue_categories << new_issue_category
671 end
677 end
672 end
678 end
673
679
674 # Copies issues from +project+
680 # Copies issues from +project+
675 def copy_issues(project)
681 def copy_issues(project)
676 # Stores the source issue id as a key and the copied issues as the
682 # Stores the source issue id as a key and the copied issues as the
677 # value. Used to map the two togeather for issue relations.
683 # value. Used to map the two togeather for issue relations.
678 issues_map = {}
684 issues_map = {}
679
685
680 # Get issues sorted by root_id, lft so that parent issues
686 # Get issues sorted by root_id, lft so that parent issues
681 # get copied before their children
687 # get copied before their children
682 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
688 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
683 new_issue = Issue.new
689 new_issue = Issue.new
684 new_issue.copy_from(issue)
690 new_issue.copy_from(issue)
685 new_issue.project = self
691 new_issue.project = self
686 # Reassign fixed_versions by name, since names are unique per
692 # Reassign fixed_versions by name, since names are unique per
687 # project and the versions for self are not yet saved
693 # project and the versions for self are not yet saved
688 if issue.fixed_version
694 if issue.fixed_version
689 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
695 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
690 end
696 end
691 # Reassign the category by name, since names are unique per
697 # Reassign the category by name, since names are unique per
692 # project and the categories for self are not yet saved
698 # project and the categories for self are not yet saved
693 if issue.category
699 if issue.category
694 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
700 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
695 end
701 end
696 # Parent issue
702 # Parent issue
697 if issue.parent_id
703 if issue.parent_id
698 if copied_parent = issues_map[issue.parent_id]
704 if copied_parent = issues_map[issue.parent_id]
699 new_issue.parent_issue_id = copied_parent.id
705 new_issue.parent_issue_id = copied_parent.id
700 end
706 end
701 end
707 end
702
708
703 self.issues << new_issue
709 self.issues << new_issue
704 if new_issue.new_record?
710 if new_issue.new_record?
705 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
711 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
706 else
712 else
707 issues_map[issue.id] = new_issue unless new_issue.new_record?
713 issues_map[issue.id] = new_issue unless new_issue.new_record?
708 end
714 end
709 end
715 end
710
716
711 # Relations after in case issues related each other
717 # Relations after in case issues related each other
712 project.issues.each do |issue|
718 project.issues.each do |issue|
713 new_issue = issues_map[issue.id]
719 new_issue = issues_map[issue.id]
714 unless new_issue
720 unless new_issue
715 # Issue was not copied
721 # Issue was not copied
716 next
722 next
717 end
723 end
718
724
719 # Relations
725 # Relations
720 issue.relations_from.each do |source_relation|
726 issue.relations_from.each do |source_relation|
721 new_issue_relation = IssueRelation.new
727 new_issue_relation = IssueRelation.new
722 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
728 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
723 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
729 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
724 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
730 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
725 new_issue_relation.issue_to = source_relation.issue_to
731 new_issue_relation.issue_to = source_relation.issue_to
726 end
732 end
727 new_issue.relations_from << new_issue_relation
733 new_issue.relations_from << new_issue_relation
728 end
734 end
729
735
730 issue.relations_to.each do |source_relation|
736 issue.relations_to.each do |source_relation|
731 new_issue_relation = IssueRelation.new
737 new_issue_relation = IssueRelation.new
732 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
738 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
733 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
739 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
734 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
740 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
735 new_issue_relation.issue_from = source_relation.issue_from
741 new_issue_relation.issue_from = source_relation.issue_from
736 end
742 end
737 new_issue.relations_to << new_issue_relation
743 new_issue.relations_to << new_issue_relation
738 end
744 end
739 end
745 end
740 end
746 end
741
747
742 # Copies members from +project+
748 # Copies members from +project+
743 def copy_members(project)
749 def copy_members(project)
744 # Copy users first, then groups to handle members with inherited and given roles
750 # Copy users first, then groups to handle members with inherited and given roles
745 members_to_copy = []
751 members_to_copy = []
746 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
752 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
747 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
753 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
748
754
749 members_to_copy.each do |member|
755 members_to_copy.each do |member|
750 new_member = Member.new
756 new_member = Member.new
751 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
757 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
752 # only copy non inherited roles
758 # only copy non inherited roles
753 # inherited roles will be added when copying the group membership
759 # inherited roles will be added when copying the group membership
754 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
760 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
755 next if role_ids.empty?
761 next if role_ids.empty?
756 new_member.role_ids = role_ids
762 new_member.role_ids = role_ids
757 new_member.project = self
763 new_member.project = self
758 self.members << new_member
764 self.members << new_member
759 end
765 end
760 end
766 end
761
767
762 # Copies queries from +project+
768 # Copies queries from +project+
763 def copy_queries(project)
769 def copy_queries(project)
764 project.queries.each do |query|
770 project.queries.each do |query|
765 new_query = Query.new
771 new_query = Query.new
766 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
772 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
767 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
773 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
768 new_query.project = self
774 new_query.project = self
769 self.queries << new_query
775 self.queries << new_query
770 end
776 end
771 end
777 end
772
778
773 # Copies boards from +project+
779 # Copies boards from +project+
774 def copy_boards(project)
780 def copy_boards(project)
775 project.boards.each do |board|
781 project.boards.each do |board|
776 new_board = Board.new
782 new_board = Board.new
777 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
783 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
778 new_board.project = self
784 new_board.project = self
779 self.boards << new_board
785 self.boards << new_board
780 end
786 end
781 end
787 end
782
788
783 def allowed_permissions
789 def allowed_permissions
784 @allowed_permissions ||= begin
790 @allowed_permissions ||= begin
785 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
791 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
786 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
792 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
787 end
793 end
788 end
794 end
789
795
790 def allowed_actions
796 def allowed_actions
791 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
797 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
792 end
798 end
793
799
794 # Returns all the active Systemwide and project specific activities
800 # Returns all the active Systemwide and project specific activities
795 def active_activities
801 def active_activities
796 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
802 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
797
803
798 if overridden_activity_ids.empty?
804 if overridden_activity_ids.empty?
799 return TimeEntryActivity.shared.active
805 return TimeEntryActivity.shared.active
800 else
806 else
801 return system_activities_and_project_overrides
807 return system_activities_and_project_overrides
802 end
808 end
803 end
809 end
804
810
805 # Returns all the Systemwide and project specific activities
811 # Returns all the Systemwide and project specific activities
806 # (inactive and active)
812 # (inactive and active)
807 def all_activities
813 def all_activities
808 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
814 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
809
815
810 if overridden_activity_ids.empty?
816 if overridden_activity_ids.empty?
811 return TimeEntryActivity.shared
817 return TimeEntryActivity.shared
812 else
818 else
813 return system_activities_and_project_overrides(true)
819 return system_activities_and_project_overrides(true)
814 end
820 end
815 end
821 end
816
822
817 # Returns the systemwide active activities merged with the project specific overrides
823 # Returns the systemwide active activities merged with the project specific overrides
818 def system_activities_and_project_overrides(include_inactive=false)
824 def system_activities_and_project_overrides(include_inactive=false)
819 if include_inactive
825 if include_inactive
820 return TimeEntryActivity.shared.
826 return TimeEntryActivity.shared.
821 find(:all,
827 find(:all,
822 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
828 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
823 self.time_entry_activities
829 self.time_entry_activities
824 else
830 else
825 return TimeEntryActivity.shared.active.
831 return TimeEntryActivity.shared.active.
826 find(:all,
832 find(:all,
827 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
833 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
828 self.time_entry_activities.active
834 self.time_entry_activities.active
829 end
835 end
830 end
836 end
831
837
832 # Archives subprojects recursively
838 # Archives subprojects recursively
833 def archive!
839 def archive!
834 children.each do |subproject|
840 children.each do |subproject|
835 subproject.send :archive!
841 subproject.send :archive!
836 end
842 end
837 update_attribute :status, STATUS_ARCHIVED
843 update_attribute :status, STATUS_ARCHIVED
838 end
844 end
839 end
845 end
@@ -1,253 +1,260
1 ActionController::Routing::Routes.draw do |map|
1 ActionController::Routing::Routes.draw do |map|
2 # Add your own custom routes here.
2 # Add your own custom routes here.
3 # The priority is based upon order of creation: first created -> highest priority.
3 # The priority is based upon order of creation: first created -> highest priority.
4
4
5 # Here's a sample route:
5 # Here's a sample route:
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 # Keep in mind you can assign values other than :controller and :action
7 # Keep in mind you can assign values other than :controller and :action
8
8
9 map.home '', :controller => 'welcome'
9 map.home '', :controller => 'welcome'
10
10
11 map.signin 'login', :controller => 'account', :action => 'login'
11 map.signin 'login', :controller => 'account', :action => 'login'
12 map.signout 'logout', :controller => 'account', :action => 'logout'
12 map.signout 'logout', :controller => 'account', :action => 'logout'
13
13
14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
15 map.connect 'help/:ctrl/:page', :controller => 'help'
15 map.connect 'help/:ctrl/:page', :controller => 'help'
16
16
17 map.with_options :controller => 'time_entry_reports', :action => 'report',:conditions => {:method => :get} do |time_report|
17 map.with_options :controller => 'time_entry_reports', :action => 'report',:conditions => {:method => :get} do |time_report|
18 time_report.connect 'projects/:project_id/issues/:issue_id/time_entries/report'
18 time_report.connect 'projects/:project_id/issues/:issue_id/time_entries/report'
19 time_report.connect 'projects/:project_id/issues/:issue_id/time_entries/report.:format'
19 time_report.connect 'projects/:project_id/issues/:issue_id/time_entries/report.:format'
20 time_report.connect 'projects/:project_id/time_entries/report'
20 time_report.connect 'projects/:project_id/time_entries/report'
21 time_report.connect 'projects/:project_id/time_entries/report.:format'
21 time_report.connect 'projects/:project_id/time_entries/report.:format'
22 time_report.connect 'time_entries/report'
22 time_report.connect 'time_entries/report'
23 time_report.connect 'time_entries/report.:format'
23 time_report.connect 'time_entries/report.:format'
24 end
24 end
25
25
26 map.bulk_edit_time_entry 'time_entries/bulk_edit',
27 :controller => 'timelog', :action => 'bulk_edit', :conditions => { :method => :get }
28 map.bulk_update_time_entry 'time_entries/bulk_edit',
29 :controller => 'timelog', :action => 'bulk_update', :conditions => { :method => :post }
30 map.time_entries_context_menu '/time_entries/context_menu',
31 :controller => 'context_menus', :action => 'time_entries'
32 # TODO: wasteful since this is also nested under issues, projects, and projects/issues
26 map.resources :time_entries, :controller => 'timelog'
33 map.resources :time_entries, :controller => 'timelog'
27
34
28 map.connect 'projects/:id/wiki', :controller => 'wikis', :action => 'edit', :conditions => {:method => :post}
35 map.connect 'projects/:id/wiki', :controller => 'wikis', :action => 'edit', :conditions => {:method => :post}
29 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :get}
36 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :get}
30 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :post}
37 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :post}
31
38
32 map.with_options :controller => 'messages' do |messages_routes|
39 map.with_options :controller => 'messages' do |messages_routes|
33 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
40 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
34 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
41 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
35 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
42 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
36 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
43 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
37 end
44 end
38 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
45 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
39 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
46 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
40 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
47 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
41 messages_actions.connect 'boards/:board_id/topics/:id/:action', :action => /edit|destroy/
48 messages_actions.connect 'boards/:board_id/topics/:id/:action', :action => /edit|destroy/
42 end
49 end
43 end
50 end
44
51
45 map.with_options :controller => 'boards' do |board_routes|
52 map.with_options :controller => 'boards' do |board_routes|
46 board_routes.with_options :conditions => {:method => :get} do |board_views|
53 board_routes.with_options :conditions => {:method => :get} do |board_views|
47 board_views.connect 'projects/:project_id/boards', :action => 'index'
54 board_views.connect 'projects/:project_id/boards', :action => 'index'
48 board_views.connect 'projects/:project_id/boards/new', :action => 'new'
55 board_views.connect 'projects/:project_id/boards/new', :action => 'new'
49 board_views.connect 'projects/:project_id/boards/:id', :action => 'show'
56 board_views.connect 'projects/:project_id/boards/:id', :action => 'show'
50 board_views.connect 'projects/:project_id/boards/:id.:format', :action => 'show'
57 board_views.connect 'projects/:project_id/boards/:id.:format', :action => 'show'
51 board_views.connect 'projects/:project_id/boards/:id/edit', :action => 'edit'
58 board_views.connect 'projects/:project_id/boards/:id/edit', :action => 'edit'
52 end
59 end
53 board_routes.with_options :conditions => {:method => :post} do |board_actions|
60 board_routes.with_options :conditions => {:method => :post} do |board_actions|
54 board_actions.connect 'projects/:project_id/boards', :action => 'new'
61 board_actions.connect 'projects/:project_id/boards', :action => 'new'
55 board_actions.connect 'projects/:project_id/boards/:id/:action', :action => /edit|destroy/
62 board_actions.connect 'projects/:project_id/boards/:id/:action', :action => /edit|destroy/
56 end
63 end
57 end
64 end
58
65
59 map.with_options :controller => 'documents' do |document_routes|
66 map.with_options :controller => 'documents' do |document_routes|
60 document_routes.with_options :conditions => {:method => :get} do |document_views|
67 document_routes.with_options :conditions => {:method => :get} do |document_views|
61 document_views.connect 'projects/:project_id/documents', :action => 'index'
68 document_views.connect 'projects/:project_id/documents', :action => 'index'
62 document_views.connect 'projects/:project_id/documents/new', :action => 'new'
69 document_views.connect 'projects/:project_id/documents/new', :action => 'new'
63 document_views.connect 'documents/:id', :action => 'show'
70 document_views.connect 'documents/:id', :action => 'show'
64 document_views.connect 'documents/:id/edit', :action => 'edit'
71 document_views.connect 'documents/:id/edit', :action => 'edit'
65 end
72 end
66 document_routes.with_options :conditions => {:method => :post} do |document_actions|
73 document_routes.with_options :conditions => {:method => :post} do |document_actions|
67 document_actions.connect 'projects/:project_id/documents', :action => 'new'
74 document_actions.connect 'projects/:project_id/documents', :action => 'new'
68 document_actions.connect 'documents/:id/:action', :action => /destroy|edit/
75 document_actions.connect 'documents/:id/:action', :action => /destroy|edit/
69 end
76 end
70 end
77 end
71
78
72 map.resources :issue_moves, :only => [:new, :create], :path_prefix => '/issues', :as => 'move'
79 map.resources :issue_moves, :only => [:new, :create], :path_prefix => '/issues', :as => 'move'
73
80
74 # Misc issue routes. TODO: move into resources
81 # Misc issue routes. TODO: move into resources
75 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes', :action => 'issues'
82 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes', :action => 'issues'
76 map.preview_issue '/issues/preview/:id', :controller => 'previews', :action => 'issue' # TODO: would look nicer as /issues/:id/preview
83 map.preview_issue '/issues/preview/:id', :controller => 'previews', :action => 'issue' # TODO: would look nicer as /issues/:id/preview
77 map.issues_context_menu '/issues/context_menu', :controller => 'context_menus', :action => 'issues'
84 map.issues_context_menu '/issues/context_menu', :controller => 'context_menus', :action => 'issues'
78 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
85 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
79 map.bulk_edit_issue 'issues/bulk_edit', :controller => 'issues', :action => 'bulk_edit', :conditions => { :method => :get }
86 map.bulk_edit_issue 'issues/bulk_edit', :controller => 'issues', :action => 'bulk_edit', :conditions => { :method => :get }
80 map.bulk_update_issue 'issues/bulk_edit', :controller => 'issues', :action => 'bulk_update', :conditions => { :method => :post }
87 map.bulk_update_issue 'issues/bulk_edit', :controller => 'issues', :action => 'bulk_update', :conditions => { :method => :post }
81 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new', :id => /\d+/, :conditions => { :method => :post }
88 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new', :id => /\d+/, :conditions => { :method => :post }
82 map.connect '/issues/:id/destroy', :controller => 'issues', :action => 'destroy', :conditions => { :method => :post } # legacy
89 map.connect '/issues/:id/destroy', :controller => 'issues', :action => 'destroy', :conditions => { :method => :post } # legacy
83
90
84 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
91 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
85 gantts_routes.connect '/projects/:project_id/issues/gantt'
92 gantts_routes.connect '/projects/:project_id/issues/gantt'
86 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
93 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
87 gantts_routes.connect '/issues/gantt.:format'
94 gantts_routes.connect '/issues/gantt.:format'
88 end
95 end
89
96
90 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
97 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
91 calendars_routes.connect '/projects/:project_id/issues/calendar'
98 calendars_routes.connect '/projects/:project_id/issues/calendar'
92 calendars_routes.connect '/issues/calendar'
99 calendars_routes.connect '/issues/calendar'
93 end
100 end
94
101
95 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
102 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
96 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
103 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
97 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
104 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
98 end
105 end
99
106
100 # Following two routes conflict with the resources because #index allows POST
107 # Following two routes conflict with the resources because #index allows POST
101 map.connect '/issues', :controller => 'issues', :action => 'index', :conditions => { :method => :post }
108 map.connect '/issues', :controller => 'issues', :action => 'index', :conditions => { :method => :post }
102 map.connect '/issues/create', :controller => 'issues', :action => 'index', :conditions => { :method => :post }
109 map.connect '/issues/create', :controller => 'issues', :action => 'index', :conditions => { :method => :post }
103
110
104 map.resources :issues, :member => { :edit => :post }, :collection => {} do |issues|
111 map.resources :issues, :member => { :edit => :post }, :collection => {} do |issues|
105 issues.resources :time_entries, :controller => 'timelog'
112 issues.resources :time_entries, :controller => 'timelog'
106 end
113 end
107
114
108 map.resources :issues, :path_prefix => '/projects/:project_id', :collection => { :create => :post } do |issues|
115 map.resources :issues, :path_prefix => '/projects/:project_id', :collection => { :create => :post } do |issues|
109 issues.resources :time_entries, :controller => 'timelog'
116 issues.resources :time_entries, :controller => 'timelog'
110 end
117 end
111
118
112 map.with_options :controller => 'issue_relations', :conditions => {:method => :post} do |relations|
119 map.with_options :controller => 'issue_relations', :conditions => {:method => :post} do |relations|
113 relations.connect 'issues/:issue_id/relations/:id', :action => 'new'
120 relations.connect 'issues/:issue_id/relations/:id', :action => 'new'
114 relations.connect 'issues/:issue_id/relations/:id/destroy', :action => 'destroy'
121 relations.connect 'issues/:issue_id/relations/:id/destroy', :action => 'destroy'
115 end
122 end
116
123
117 map.connect 'projects/:id/members/new', :controller => 'members', :action => 'new'
124 map.connect 'projects/:id/members/new', :controller => 'members', :action => 'new'
118
125
119 map.with_options :controller => 'users' do |users|
126 map.with_options :controller => 'users' do |users|
120 users.connect 'users/:id/edit/:tab', :action => 'edit', :tab => nil, :conditions => {:method => :get}
127 users.connect 'users/:id/edit/:tab', :action => 'edit', :tab => nil, :conditions => {:method => :get}
121
128
122 users.with_options :conditions => {:method => :post} do |user_actions|
129 users.with_options :conditions => {:method => :post} do |user_actions|
123 user_actions.connect 'users/:id/memberships', :action => 'edit_membership'
130 user_actions.connect 'users/:id/memberships', :action => 'edit_membership'
124 user_actions.connect 'users/:id/memberships/:membership_id', :action => 'edit_membership'
131 user_actions.connect 'users/:id/memberships/:membership_id', :action => 'edit_membership'
125 user_actions.connect 'users/:id/memberships/:membership_id/destroy', :action => 'destroy_membership'
132 user_actions.connect 'users/:id/memberships/:membership_id/destroy', :action => 'destroy_membership'
126 end
133 end
127 end
134 end
128
135
129 map.resources :users, :member => {
136 map.resources :users, :member => {
130 :edit_membership => :post,
137 :edit_membership => :post,
131 :destroy_membership => :post
138 :destroy_membership => :post
132 }
139 }
133
140
134 # For nice "roadmap" in the url for the index action
141 # For nice "roadmap" in the url for the index action
135 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
142 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
136
143
137 map.all_news 'news', :controller => 'news', :action => 'index'
144 map.all_news 'news', :controller => 'news', :action => 'index'
138 map.formatted_all_news 'news.:format', :controller => 'news', :action => 'index'
145 map.formatted_all_news 'news.:format', :controller => 'news', :action => 'index'
139 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
146 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
140 map.connect 'news/:id/comments', :controller => 'comments', :action => 'create', :conditions => {:method => :post}
147 map.connect 'news/:id/comments', :controller => 'comments', :action => 'create', :conditions => {:method => :post}
141 map.connect 'news/:id/comments/:comment_id', :controller => 'comments', :action => 'destroy', :conditions => {:method => :delete}
148 map.connect 'news/:id/comments/:comment_id', :controller => 'comments', :action => 'destroy', :conditions => {:method => :delete}
142
149
143 map.resources :projects, :member => {
150 map.resources :projects, :member => {
144 :copy => [:get, :post],
151 :copy => [:get, :post],
145 :settings => :get,
152 :settings => :get,
146 :modules => :post,
153 :modules => :post,
147 :archive => :post,
154 :archive => :post,
148 :unarchive => :post
155 :unarchive => :post
149 } do |project|
156 } do |project|
150 project.resource :project_enumerations, :as => 'enumerations', :only => [:update, :destroy]
157 project.resource :project_enumerations, :as => 'enumerations', :only => [:update, :destroy]
151 project.resources :files, :only => [:index, :new, :create]
158 project.resources :files, :only => [:index, :new, :create]
152 project.resources :versions, :collection => {:close_completed => :put}, :member => {:status_by => :post}
159 project.resources :versions, :collection => {:close_completed => :put}, :member => {:status_by => :post}
153 project.resources :news, :shallow => true
160 project.resources :news, :shallow => true
154 project.resources :time_entries, :controller => 'timelog', :path_prefix => 'projects/:project_id'
161 project.resources :time_entries, :controller => 'timelog', :path_prefix => 'projects/:project_id'
155
162
156 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
163 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
157 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
164 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
158 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
165 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
159 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
166 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
160 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
167 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
161 project.resources :wiki, :except => [:new, :create], :member => {
168 project.resources :wiki, :except => [:new, :create], :member => {
162 :rename => [:get, :post],
169 :rename => [:get, :post],
163 :history => :get,
170 :history => :get,
164 :preview => :any,
171 :preview => :any,
165 :protect => :post,
172 :protect => :post,
166 :add_attachment => :post
173 :add_attachment => :post
167 }, :collection => {
174 }, :collection => {
168 :export => :get,
175 :export => :get,
169 :date_index => :get
176 :date_index => :get
170 }
177 }
171
178
172 end
179 end
173
180
174 # Destroy uses a get request to prompt the user before the actual DELETE request
181 # Destroy uses a get request to prompt the user before the actual DELETE request
175 map.project_destroy_confirm 'projects/:id/destroy', :controller => 'projects', :action => 'destroy', :conditions => {:method => :get}
182 map.project_destroy_confirm 'projects/:id/destroy', :controller => 'projects', :action => 'destroy', :conditions => {:method => :get}
176
183
177 # TODO: port to be part of the resources route(s)
184 # TODO: port to be part of the resources route(s)
178 map.with_options :controller => 'projects' do |project_mapper|
185 map.with_options :controller => 'projects' do |project_mapper|
179 project_mapper.with_options :conditions => {:method => :get} do |project_views|
186 project_mapper.with_options :conditions => {:method => :get} do |project_views|
180 project_views.connect 'projects/:id/settings/:tab', :controller => 'projects', :action => 'settings'
187 project_views.connect 'projects/:id/settings/:tab', :controller => 'projects', :action => 'settings'
181 project_views.connect 'projects/:project_id/issues/:copy_from/copy', :controller => 'issues', :action => 'new'
188 project_views.connect 'projects/:project_id/issues/:copy_from/copy', :controller => 'issues', :action => 'new'
182 end
189 end
183 end
190 end
184
191
185 map.with_options :controller => 'activities', :action => 'index', :conditions => {:method => :get} do |activity|
192 map.with_options :controller => 'activities', :action => 'index', :conditions => {:method => :get} do |activity|
186 activity.connect 'projects/:id/activity'
193 activity.connect 'projects/:id/activity'
187 activity.connect 'projects/:id/activity.:format'
194 activity.connect 'projects/:id/activity.:format'
188 activity.connect 'activity', :id => nil
195 activity.connect 'activity', :id => nil
189 activity.connect 'activity.:format', :id => nil
196 activity.connect 'activity.:format', :id => nil
190 end
197 end
191
198
192
199
193 map.with_options :controller => 'issue_categories' do |categories|
200 map.with_options :controller => 'issue_categories' do |categories|
194 categories.connect 'projects/:project_id/issue_categories/new', :action => 'new'
201 categories.connect 'projects/:project_id/issue_categories/new', :action => 'new'
195 end
202 end
196
203
197 map.with_options :controller => 'repositories' do |repositories|
204 map.with_options :controller => 'repositories' do |repositories|
198 repositories.with_options :conditions => {:method => :get} do |repository_views|
205 repositories.with_options :conditions => {:method => :get} do |repository_views|
199 repository_views.connect 'projects/:id/repository', :action => 'show'
206 repository_views.connect 'projects/:id/repository', :action => 'show'
200 repository_views.connect 'projects/:id/repository/edit', :action => 'edit'
207 repository_views.connect 'projects/:id/repository/edit', :action => 'edit'
201 repository_views.connect 'projects/:id/repository/statistics', :action => 'stats'
208 repository_views.connect 'projects/:id/repository/statistics', :action => 'stats'
202 repository_views.connect 'projects/:id/repository/revisions', :action => 'revisions'
209 repository_views.connect 'projects/:id/repository/revisions', :action => 'revisions'
203 repository_views.connect 'projects/:id/repository/revisions.:format', :action => 'revisions'
210 repository_views.connect 'projects/:id/repository/revisions.:format', :action => 'revisions'
204 repository_views.connect 'projects/:id/repository/revisions/:rev', :action => 'revision'
211 repository_views.connect 'projects/:id/repository/revisions/:rev', :action => 'revision'
205 repository_views.connect 'projects/:id/repository/revisions/:rev/diff', :action => 'diff'
212 repository_views.connect 'projects/:id/repository/revisions/:rev/diff', :action => 'diff'
206 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format', :action => 'diff'
213 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format', :action => 'diff'
207 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path', :action => 'entry', :format => 'raw', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
214 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path', :action => 'entry', :format => 'raw', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
208 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
215 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
209 repository_views.connect 'projects/:id/repository/raw/*path', :action => 'entry', :format => 'raw'
216 repository_views.connect 'projects/:id/repository/raw/*path', :action => 'entry', :format => 'raw'
210 # TODO: why the following route is required?
217 # TODO: why the following route is required?
211 repository_views.connect 'projects/:id/repository/entry/*path', :action => 'entry'
218 repository_views.connect 'projects/:id/repository/entry/*path', :action => 'entry'
212 repository_views.connect 'projects/:id/repository/:action/*path'
219 repository_views.connect 'projects/:id/repository/:action/*path'
213 end
220 end
214
221
215 repositories.connect 'projects/:id/repository/:action', :conditions => {:method => :post}
222 repositories.connect 'projects/:id/repository/:action', :conditions => {:method => :post}
216 end
223 end
217
224
218 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
225 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
219 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
226 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
220 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
227 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
221
228
222 map.resources :groups
229 map.resources :groups
223
230
224 #left old routes at the bottom for backwards compat
231 #left old routes at the bottom for backwards compat
225 map.connect 'projects/:project_id/queries/:action', :controller => 'queries'
232 map.connect 'projects/:project_id/queries/:action', :controller => 'queries'
226 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
233 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
227 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
234 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
228 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
235 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
229 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
236 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
230 map.connect 'wiki/:id/:page/:action', :page => nil, :controller => 'wiki'
237 map.connect 'wiki/:id/:page/:action', :page => nil, :controller => 'wiki'
231 map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
238 map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
232 map.connect 'projects/:project_id/news/:action', :controller => 'news'
239 map.connect 'projects/:project_id/news/:action', :controller => 'news'
233 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog', :project_id => /.+/
240 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog', :project_id => /.+/
234 map.with_options :controller => 'repositories' do |omap|
241 map.with_options :controller => 'repositories' do |omap|
235 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
242 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
236 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
243 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
237 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
244 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
238 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
245 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
239 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
246 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
240 omap.connect 'repositories/revision/:id/:rev', :action => 'revision'
247 omap.connect 'repositories/revision/:id/:rev', :action => 'revision'
241 end
248 end
242
249
243 map.with_options :controller => 'sys' do |sys|
250 map.with_options :controller => 'sys' do |sys|
244 sys.connect 'sys/projects.:format', :action => 'projects', :conditions => {:method => :get}
251 sys.connect 'sys/projects.:format', :action => 'projects', :conditions => {:method => :get}
245 sys.connect 'sys/projects/:id/repository.:format', :action => 'create_project_repository', :conditions => {:method => :post}
252 sys.connect 'sys/projects/:id/repository.:format', :action => 'create_project_repository', :conditions => {:method => :post}
246 end
253 end
247
254
248 # Install the default route as the lowest priority.
255 # Install the default route as the lowest priority.
249 map.connect ':controller/:action/:id'
256 map.connect ':controller/:action/:id'
250 map.connect 'robots.txt', :controller => 'welcome', :action => 'robots'
257 map.connect 'robots.txt', :controller => 'welcome', :action => 'robots'
251 # Used for OpenID
258 # Used for OpenID
252 map.root :controller => 'account', :action => 'login'
259 map.root :controller => 'account', :action => 'login'
253 end
260 end
@@ -1,235 +1,235
1 require 'redmine/access_control'
1 require 'redmine/access_control'
2 require 'redmine/menu_manager'
2 require 'redmine/menu_manager'
3 require 'redmine/activity'
3 require 'redmine/activity'
4 require 'redmine/search'
4 require 'redmine/search'
5 require 'redmine/custom_field_format'
5 require 'redmine/custom_field_format'
6 require 'redmine/mime_type'
6 require 'redmine/mime_type'
7 require 'redmine/core_ext'
7 require 'redmine/core_ext'
8 require 'redmine/themes'
8 require 'redmine/themes'
9 require 'redmine/hook'
9 require 'redmine/hook'
10 require 'redmine/plugin'
10 require 'redmine/plugin'
11 require 'redmine/notifiable'
11 require 'redmine/notifiable'
12 require 'redmine/wiki_formatting'
12 require 'redmine/wiki_formatting'
13 require 'redmine/scm/base'
13 require 'redmine/scm/base'
14
14
15 begin
15 begin
16 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
16 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
17 rescue LoadError
17 rescue LoadError
18 # RMagick is not available
18 # RMagick is not available
19 end
19 end
20
20
21 if RUBY_VERSION < '1.9'
21 if RUBY_VERSION < '1.9'
22 require 'faster_csv'
22 require 'faster_csv'
23 else
23 else
24 require 'csv'
24 require 'csv'
25 FCSV = CSV
25 FCSV = CSV
26 end
26 end
27
27
28 Redmine::Scm::Base.add "Subversion"
28 Redmine::Scm::Base.add "Subversion"
29 Redmine::Scm::Base.add "Darcs"
29 Redmine::Scm::Base.add "Darcs"
30 Redmine::Scm::Base.add "Mercurial"
30 Redmine::Scm::Base.add "Mercurial"
31 Redmine::Scm::Base.add "Cvs"
31 Redmine::Scm::Base.add "Cvs"
32 Redmine::Scm::Base.add "Bazaar"
32 Redmine::Scm::Base.add "Bazaar"
33 Redmine::Scm::Base.add "Git"
33 Redmine::Scm::Base.add "Git"
34 Redmine::Scm::Base.add "Filesystem"
34 Redmine::Scm::Base.add "Filesystem"
35
35
36 Redmine::CustomFieldFormat.map do |fields|
36 Redmine::CustomFieldFormat.map do |fields|
37 fields.register Redmine::CustomFieldFormat.new('string', :label => :label_string, :order => 1)
37 fields.register Redmine::CustomFieldFormat.new('string', :label => :label_string, :order => 1)
38 fields.register Redmine::CustomFieldFormat.new('text', :label => :label_text, :order => 2)
38 fields.register Redmine::CustomFieldFormat.new('text', :label => :label_text, :order => 2)
39 fields.register Redmine::CustomFieldFormat.new('int', :label => :label_integer, :order => 3)
39 fields.register Redmine::CustomFieldFormat.new('int', :label => :label_integer, :order => 3)
40 fields.register Redmine::CustomFieldFormat.new('float', :label => :label_float, :order => 4)
40 fields.register Redmine::CustomFieldFormat.new('float', :label => :label_float, :order => 4)
41 fields.register Redmine::CustomFieldFormat.new('list', :label => :label_list, :order => 5)
41 fields.register Redmine::CustomFieldFormat.new('list', :label => :label_list, :order => 5)
42 fields.register Redmine::CustomFieldFormat.new('date', :label => :label_date, :order => 6)
42 fields.register Redmine::CustomFieldFormat.new('date', :label => :label_date, :order => 6)
43 fields.register Redmine::CustomFieldFormat.new('bool', :label => :label_boolean, :order => 7)
43 fields.register Redmine::CustomFieldFormat.new('bool', :label => :label_boolean, :order => 7)
44 fields.register Redmine::CustomFieldFormat.new('user', :label => :label_user, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 8)
44 fields.register Redmine::CustomFieldFormat.new('user', :label => :label_user, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 8)
45 fields.register Redmine::CustomFieldFormat.new('version', :label => :label_version, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 9)
45 fields.register Redmine::CustomFieldFormat.new('version', :label => :label_version, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 9)
46 end
46 end
47
47
48 # Permissions
48 # Permissions
49 Redmine::AccessControl.map do |map|
49 Redmine::AccessControl.map do |map|
50 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true
50 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true
51 map.permission :search_project, {:search => :index}, :public => true
51 map.permission :search_project, {:search => :index}, :public => true
52 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
52 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
53 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
53 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
54 map.permission :select_project_modules, {:projects => :modules}, :require => :member
54 map.permission :select_project_modules, {:projects => :modules}, :require => :member
55 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
55 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
56 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
56 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
57 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
57 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
58
58
59 map.project_module :issue_tracking do |map|
59 map.project_module :issue_tracking do |map|
60 # Issue categories
60 # Issue categories
61 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:new, :edit, :destroy]}, :require => :member
61 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:new, :edit, :destroy]}, :require => :member
62 # Issues
62 # Issues
63 map.permission :view_issues, {:issues => [:index, :show],
63 map.permission :view_issues, {:issues => [:index, :show],
64 :auto_complete => [:issues],
64 :auto_complete => [:issues],
65 :context_menus => [:issues],
65 :context_menus => [:issues],
66 :versions => [:index, :show, :status_by],
66 :versions => [:index, :show, :status_by],
67 :journals => [:index, :diff],
67 :journals => [:index, :diff],
68 :queries => :index,
68 :queries => :index,
69 :reports => [:issue_report, :issue_report_details]}
69 :reports => [:issue_report, :issue_report_details]}
70 map.permission :add_issues, {:issues => [:new, :create, :update_form]}
70 map.permission :add_issues, {:issues => [:new, :create, :update_form]}
71 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]}
71 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]}
72 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
72 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
73 map.permission :manage_subtasks, {}
73 map.permission :manage_subtasks, {}
74 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]}
74 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]}
75 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
75 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
76 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
76 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
77 map.permission :move_issues, {:issue_moves => [:new, :create]}, :require => :loggedin
77 map.permission :move_issues, {:issue_moves => [:new, :create]}, :require => :loggedin
78 map.permission :delete_issues, {:issues => :destroy}, :require => :member
78 map.permission :delete_issues, {:issues => :destroy}, :require => :member
79 # Queries
79 # Queries
80 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
80 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
81 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
81 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
82 # Watchers
82 # Watchers
83 map.permission :view_issue_watchers, {}
83 map.permission :view_issue_watchers, {}
84 map.permission :add_issue_watchers, {:watchers => :new}
84 map.permission :add_issue_watchers, {:watchers => :new}
85 map.permission :delete_issue_watchers, {:watchers => :destroy}
85 map.permission :delete_issue_watchers, {:watchers => :destroy}
86 end
86 end
87
87
88 map.project_module :time_tracking do |map|
88 map.project_module :time_tracking do |map|
89 map.permission :log_time, {:timelog => [:new, :create, :edit, :update]}, :require => :loggedin
89 map.permission :log_time, {:timelog => [:new, :create, :edit, :update, :bulk_edit, :bulk_update]}, :require => :loggedin
90 map.permission :view_time_entries, :timelog => [:index, :show], :time_entry_reports => [:report]
90 map.permission :view_time_entries, :timelog => [:index, :show], :time_entry_reports => [:report]
91 map.permission :edit_time_entries, {:timelog => [:new, :create, :edit, :update, :destroy]}, :require => :member
91 map.permission :edit_time_entries, {:timelog => [:new, :create, :edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
92 map.permission :edit_own_time_entries, {:timelog => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
92 map.permission :edit_own_time_entries, {:timelog => [:new, :create, :edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
93 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
93 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
94 end
94 end
95
95
96 map.project_module :news do |map|
96 map.project_module :news do |map|
97 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
97 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
98 map.permission :view_news, {:news => [:index, :show]}, :public => true
98 map.permission :view_news, {:news => [:index, :show]}, :public => true
99 map.permission :comment_news, {:comments => :create}
99 map.permission :comment_news, {:comments => :create}
100 end
100 end
101
101
102 map.project_module :documents do |map|
102 map.project_module :documents do |map|
103 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
103 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
104 map.permission :view_documents, :documents => [:index, :show, :download]
104 map.permission :view_documents, :documents => [:index, :show, :download]
105 end
105 end
106
106
107 map.project_module :files do |map|
107 map.project_module :files do |map|
108 map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
108 map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
109 map.permission :view_files, :files => :index, :versions => :download
109 map.permission :view_files, :files => :index, :versions => :download
110 end
110 end
111
111
112 map.project_module :wiki do |map|
112 map.project_module :wiki do |map|
113 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
113 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
114 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
114 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
115 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
115 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
116 map.permission :view_wiki_pages, :wiki => [:index, :show, :special, :date_index]
116 map.permission :view_wiki_pages, :wiki => [:index, :show, :special, :date_index]
117 map.permission :export_wiki_pages, :wiki => [:export]
117 map.permission :export_wiki_pages, :wiki => [:export]
118 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
118 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
119 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
119 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
120 map.permission :delete_wiki_pages_attachments, {}
120 map.permission :delete_wiki_pages_attachments, {}
121 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
121 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
122 end
122 end
123
123
124 map.project_module :repository do |map|
124 map.project_module :repository do |map|
125 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
125 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
126 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
126 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
127 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
127 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
128 map.permission :commit_access, {}
128 map.permission :commit_access, {}
129 end
129 end
130
130
131 map.project_module :boards do |map|
131 map.project_module :boards do |map|
132 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
132 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
133 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
133 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
134 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
134 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
135 map.permission :edit_messages, {:messages => :edit}, :require => :member
135 map.permission :edit_messages, {:messages => :edit}, :require => :member
136 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
136 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
137 map.permission :delete_messages, {:messages => :destroy}, :require => :member
137 map.permission :delete_messages, {:messages => :destroy}, :require => :member
138 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
138 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
139 end
139 end
140
140
141 map.project_module :calendar do |map|
141 map.project_module :calendar do |map|
142 map.permission :view_calendar, :calendars => [:show, :update]
142 map.permission :view_calendar, :calendars => [:show, :update]
143 end
143 end
144
144
145 map.project_module :gantt do |map|
145 map.project_module :gantt do |map|
146 map.permission :view_gantt, :gantts => [:show, :update]
146 map.permission :view_gantt, :gantts => [:show, :update]
147 end
147 end
148 end
148 end
149
149
150 Redmine::MenuManager.map :top_menu do |menu|
150 Redmine::MenuManager.map :top_menu do |menu|
151 menu.push :home, :home_path
151 menu.push :home, :home_path
152 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
152 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
153 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
153 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
154 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
154 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
155 menu.push :help, Redmine::Info.help_url, :last => true
155 menu.push :help, Redmine::Info.help_url, :last => true
156 end
156 end
157
157
158 Redmine::MenuManager.map :account_menu do |menu|
158 Redmine::MenuManager.map :account_menu do |menu|
159 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
159 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
160 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
160 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
161 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
161 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
162 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
162 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
163 end
163 end
164
164
165 Redmine::MenuManager.map :application_menu do |menu|
165 Redmine::MenuManager.map :application_menu do |menu|
166 # Empty
166 # Empty
167 end
167 end
168
168
169 Redmine::MenuManager.map :admin_menu do |menu|
169 Redmine::MenuManager.map :admin_menu do |menu|
170 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
170 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
171 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
171 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
172 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
172 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
173 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
173 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
174 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
174 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
175 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
175 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
176 :html => {:class => 'issue_statuses'}
176 :html => {:class => 'issue_statuses'}
177 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
177 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
178 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
178 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
179 :html => {:class => 'custom_fields'}
179 :html => {:class => 'custom_fields'}
180 menu.push :enumerations, {:controller => 'enumerations'}
180 menu.push :enumerations, {:controller => 'enumerations'}
181 menu.push :settings, {:controller => 'settings'}
181 menu.push :settings, {:controller => 'settings'}
182 menu.push :ldap_authentication, {:controller => 'ldap_auth_sources', :action => 'index'},
182 menu.push :ldap_authentication, {:controller => 'ldap_auth_sources', :action => 'index'},
183 :html => {:class => 'server_authentication'}
183 :html => {:class => 'server_authentication'}
184 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
184 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
185 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
185 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
186 end
186 end
187
187
188 Redmine::MenuManager.map :project_menu do |menu|
188 Redmine::MenuManager.map :project_menu do |menu|
189 menu.push :overview, { :controller => 'projects', :action => 'show' }
189 menu.push :overview, { :controller => 'projects', :action => 'show' }
190 menu.push :activity, { :controller => 'activities', :action => 'index' }
190 menu.push :activity, { :controller => 'activities', :action => 'index' }
191 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
191 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
192 :if => Proc.new { |p| p.shared_versions.any? }
192 :if => Proc.new { |p| p.shared_versions.any? }
193 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
193 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
194 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
194 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
195 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
195 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
196 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
196 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
197 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
197 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
198 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
198 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
199 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
199 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
200 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
200 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
201 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
201 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
202 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
202 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
203 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
203 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
204 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
204 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
205 menu.push :repository, { :controller => 'repositories', :action => 'show' },
205 menu.push :repository, { :controller => 'repositories', :action => 'show' },
206 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
206 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
207 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
207 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
208 end
208 end
209
209
210 Redmine::Activity.map do |activity|
210 Redmine::Activity.map do |activity|
211 activity.register :issues, :class_name => %w(Issue Journal)
211 activity.register :issues, :class_name => %w(Issue Journal)
212 activity.register :changesets
212 activity.register :changesets
213 activity.register :news
213 activity.register :news
214 activity.register :documents, :class_name => %w(Document Attachment)
214 activity.register :documents, :class_name => %w(Document Attachment)
215 activity.register :files, :class_name => 'Attachment'
215 activity.register :files, :class_name => 'Attachment'
216 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
216 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
217 activity.register :messages, :default => false
217 activity.register :messages, :default => false
218 activity.register :time_entries, :default => false
218 activity.register :time_entries, :default => false
219 end
219 end
220
220
221 Redmine::Search.map do |search|
221 Redmine::Search.map do |search|
222 search.register :issues
222 search.register :issues
223 search.register :news
223 search.register :news
224 search.register :documents
224 search.register :documents
225 search.register :changesets
225 search.register :changesets
226 search.register :wiki_pages
226 search.register :wiki_pages
227 search.register :messages
227 search.register :messages
228 search.register :projects
228 search.register :projects
229 end
229 end
230
230
231 Redmine::WikiFormatting.map do |format|
231 Redmine::WikiFormatting.map do |format|
232 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
232 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
233 end
233 end
234
234
235 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
235 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
General Comments 0
You need to be logged in to leave comments. Login now